mariadb-optimization.py hinzugefügt
This commit is contained in:
394
mariadb-optimization.py
Normal file
394
mariadb-optimization.py
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
JTL Shop MariaDB Index-Optimierung
|
||||||
|
==================================
|
||||||
|
Erstellt fehlende Indizes und aktiviert FULLTEXT-Suche auf allen JTL-Shops.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Backup aller Sucheinstellungen vor Änderungen
|
||||||
|
- Erstellt 5 Index-Typen auf allen betroffenen Shops
|
||||||
|
- Aktiviert FULLTEXT-Suche
|
||||||
|
- Generiert Rollback-Skript für Notfälle
|
||||||
|
|
||||||
|
Autor: Claude
|
||||||
|
Datum: 2025-12-12
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# Konfiguration
|
||||||
|
TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
BACKUP_FILE = f"/root/jtl_search_backup_{TIMESTAMP}.json"
|
||||||
|
ROLLBACK_FILE = f"/root/jtl_search_rollback_{TIMESTAMP}.py"
|
||||||
|
LOG_FILE = f"/root/jtl_index_log_{TIMESTAMP}.txt"
|
||||||
|
|
||||||
|
# Index-Definitionen
|
||||||
|
INDEXES = [
|
||||||
|
{
|
||||||
|
"name": "idx_kLieferscheinPos",
|
||||||
|
"table": "tlieferscheinposinfo",
|
||||||
|
"columns": "kLieferscheinPos",
|
||||||
|
"check_column": "kLieferscheinPos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_kLieferschein",
|
||||||
|
"table": "tlieferscheinpos",
|
||||||
|
"columns": "kLieferschein",
|
||||||
|
"check_column": "kLieferschein"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_kKampagneDef_kKey",
|
||||||
|
"table": "tkampagnevorgang",
|
||||||
|
"columns": "kKampagneDef, kKey",
|
||||||
|
"check_column": "kKampagneDef"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_kXSellArtikel",
|
||||||
|
"table": "txsellkauf",
|
||||||
|
"columns": "kXSellArtikel",
|
||||||
|
"check_column": "kXSellArtikel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
FULLTEXT_INDEX = {
|
||||||
|
"name": "idx_tartikel_fulltext",
|
||||||
|
"table": "tartikel",
|
||||||
|
"columns": "cName, cSeo, cSuchbegriffe, cArtNr, cKurzBeschreibung, cBeschreibung, cBarcode, cISBN, cHAN"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Logger:
|
||||||
|
def __init__(self, log_file):
|
||||||
|
self.log_file = log_file
|
||||||
|
self.messages = []
|
||||||
|
|
||||||
|
def log(self, message, level="INFO"):
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
formatted = f"[{timestamp}] [{level}] {message}"
|
||||||
|
print(formatted)
|
||||||
|
self.messages.append(formatted)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
with open(self.log_file, 'w') as f:
|
||||||
|
f.write("\n".join(self.messages))
|
||||||
|
print(f"\nLog gespeichert: {self.log_file}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_mysql(query, database=None):
|
||||||
|
"""Führt MySQL-Query aus und gibt Ergebnis zurück."""
|
||||||
|
cmd = ["mysql", "-N", "-e", query]
|
||||||
|
if database:
|
||||||
|
cmd.extend(["-D", database])
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None, result.stderr
|
||||||
|
return result.stdout.strip(), None
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_shops():
|
||||||
|
"""Ermittelt alle JTL-Shop-Datenbanken."""
|
||||||
|
query = """
|
||||||
|
SELECT DISTINCT table_schema
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_name = 'tartikel'
|
||||||
|
AND table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
|
||||||
|
ORDER BY table_schema;
|
||||||
|
"""
|
||||||
|
result, err = run_mysql(query)
|
||||||
|
if err:
|
||||||
|
return []
|
||||||
|
return [line.strip() for line in result.split('\n') if line.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def check_index_exists(shop, table, check_column):
|
||||||
|
"""Prüft ob ein Index für die Spalte bereits existiert."""
|
||||||
|
query = f"""
|
||||||
|
SELECT COUNT(*) FROM information_schema.statistics
|
||||||
|
WHERE table_schema = '{shop}'
|
||||||
|
AND table_name = '{table}'
|
||||||
|
AND column_name = '{check_column}'
|
||||||
|
AND index_name != 'PRIMARY';
|
||||||
|
"""
|
||||||
|
result, _ = run_mysql(query)
|
||||||
|
return result and int(result) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def check_fulltext_exists(shop):
|
||||||
|
"""Prüft ob FULLTEXT-Index bereits existiert."""
|
||||||
|
query = f"""
|
||||||
|
SELECT COUNT(*) FROM information_schema.statistics
|
||||||
|
WHERE table_schema = '{shop}'
|
||||||
|
AND table_name = 'tartikel'
|
||||||
|
AND index_type = 'FULLTEXT';
|
||||||
|
"""
|
||||||
|
result, _ = run_mysql(query)
|
||||||
|
return result and int(result) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def check_table_exists(shop, table):
|
||||||
|
"""Prüft ob Tabelle existiert."""
|
||||||
|
query = f"""
|
||||||
|
SELECT COUNT(*) FROM information_schema.tables
|
||||||
|
WHERE table_schema = '{shop}'
|
||||||
|
AND table_name = '{table}';
|
||||||
|
"""
|
||||||
|
result, _ = run_mysql(query)
|
||||||
|
return result and int(result) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_search_setting(shop):
|
||||||
|
"""Liest aktuelle suche_fulltext Einstellung."""
|
||||||
|
query = f"SELECT cWert FROM `{shop}`.teinstellungen WHERE cName = 'suche_fulltext' LIMIT 1;"
|
||||||
|
result, _ = run_mysql(query)
|
||||||
|
return result if result else "NICHT_GESETZT"
|
||||||
|
|
||||||
|
|
||||||
|
def set_search_setting(shop, value):
|
||||||
|
"""Setzt suche_fulltext Einstellung."""
|
||||||
|
query = f"UPDATE `{shop}`.teinstellungen SET cWert = '{value}' WHERE cName = 'suche_fulltext';"
|
||||||
|
_, err = run_mysql(query)
|
||||||
|
return err is None
|
||||||
|
|
||||||
|
|
||||||
|
def create_index(shop, index_name, table, columns, is_fulltext=False):
|
||||||
|
"""Erstellt einen Index."""
|
||||||
|
if is_fulltext:
|
||||||
|
query = f"CREATE FULLTEXT INDEX `{index_name}` ON `{shop}`.`{table}` ({columns});"
|
||||||
|
else:
|
||||||
|
query = f"CREATE INDEX `{index_name}` ON `{shop}`.`{table}` ({columns});"
|
||||||
|
|
||||||
|
_, err = run_mysql(query)
|
||||||
|
return err
|
||||||
|
|
||||||
|
|
||||||
|
def generate_rollback_script(backup_data):
|
||||||
|
"""Generiert ein Rollback-Skript."""
|
||||||
|
script = f'''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
JTL Search Settings Rollback Script
|
||||||
|
====================================
|
||||||
|
Generiert am: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
||||||
|
|
||||||
|
Dieses Skript stellt die ursprünglichen suche_fulltext Einstellungen wieder her.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
BACKUP_DATA = {json.dumps(backup_data, indent=4)}
|
||||||
|
|
||||||
|
def run_mysql(query):
|
||||||
|
result = subprocess.run(["mysql", "-N", "-e", query], capture_output=True, text=True)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== JTL Search Settings Rollback ===")
|
||||||
|
print(f"Stelle {{len(BACKUP_DATA)}} Einstellungen wieder her...\\n")
|
||||||
|
|
||||||
|
success = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for shop, value in BACKUP_DATA.items():
|
||||||
|
if value == "NICHT_GESETZT":
|
||||||
|
print(f" [SKIP] {{shop}}: Keine Einstellung vorhanden")
|
||||||
|
continue
|
||||||
|
|
||||||
|
query = f"UPDATE `{{shop}}`.teinstellungen SET cWert = '{{value}}' WHERE cName = 'suche_fulltext';"
|
||||||
|
if run_mysql(query):
|
||||||
|
print(f" [OK] {{shop}}: suche_fulltext = '{{value}}'")
|
||||||
|
success += 1
|
||||||
|
else:
|
||||||
|
print(f" [FEHLER] {{shop}}")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print(f"\\n=== Rollback abgeschlossen ===")
|
||||||
|
print(f"Erfolgreich: {{success}}")
|
||||||
|
print(f"Fehlgeschlagen: {{failed}}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
'''
|
||||||
|
|
||||||
|
with open(ROLLBACK_FILE, 'w') as f:
|
||||||
|
f.write(script)
|
||||||
|
|
||||||
|
# Ausführbar machen
|
||||||
|
subprocess.run(["chmod", "+x", ROLLBACK_FILE])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger = Logger(LOG_FILE)
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("JTL Shop MariaDB Index-Optimierung")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Phase 0: Shops ermitteln
|
||||||
|
logger.log("Ermittle alle JTL-Shop-Datenbanken...")
|
||||||
|
shops = get_all_shops()
|
||||||
|
logger.log(f"Gefunden: {len(shops)} Shops")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not shops:
|
||||||
|
logger.log("Keine Shops gefunden. Abbruch.", "ERROR")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Phase 1: Backup der Sucheinstellungen
|
||||||
|
print("=" * 60)
|
||||||
|
print("PHASE 1: Backup der Sucheinstellungen")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
backup_data = {}
|
||||||
|
for shop in shops:
|
||||||
|
setting = get_search_setting(shop)
|
||||||
|
backup_data[shop] = setting
|
||||||
|
logger.log(f" {shop}: suche_fulltext = '{setting}'")
|
||||||
|
|
||||||
|
# Backup speichern
|
||||||
|
with open(BACKUP_FILE, 'w') as f:
|
||||||
|
json.dump(backup_data, f, indent=2)
|
||||||
|
logger.log(f"Backup gespeichert: {BACKUP_FILE}")
|
||||||
|
|
||||||
|
# Rollback-Skript generieren
|
||||||
|
generate_rollback_script(backup_data)
|
||||||
|
logger.log(f"Rollback-Skript generiert: {ROLLBACK_FILE}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Phase 2: Standard-Indizes erstellen
|
||||||
|
print("=" * 60)
|
||||||
|
print("PHASE 2: Standard-Indizes erstellen")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
index_stats = {"created": 0, "skipped": 0, "failed": 0}
|
||||||
|
|
||||||
|
for idx in INDEXES:
|
||||||
|
logger.log(f"\n--- Index: {idx['name']} auf {idx['table']} ---")
|
||||||
|
|
||||||
|
for shop in shops:
|
||||||
|
# Prüfen ob Tabelle existiert
|
||||||
|
if not check_table_exists(shop, idx['table']):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Prüfen ob Index existiert
|
||||||
|
if check_index_exists(shop, idx['table'], idx['check_column']):
|
||||||
|
logger.log(f" [SKIP] {shop}: Index existiert bereits")
|
||||||
|
index_stats["skipped"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Index erstellen
|
||||||
|
err = create_index(shop, idx['name'], idx['table'], idx['columns'])
|
||||||
|
if err:
|
||||||
|
logger.log(f" [FEHLER] {shop}: {err}", "ERROR")
|
||||||
|
index_stats["failed"] += 1
|
||||||
|
else:
|
||||||
|
logger.log(f" [OK] {shop}: Index erstellt")
|
||||||
|
index_stats["created"] += 1
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Phase 3: FULLTEXT-Index erstellen
|
||||||
|
print("=" * 60)
|
||||||
|
print("PHASE 3: FULLTEXT-Index erstellen")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
fulltext_stats = {"created": 0, "skipped": 0, "failed": 0}
|
||||||
|
|
||||||
|
for shop in shops:
|
||||||
|
# Prüfen ob FULLTEXT bereits existiert
|
||||||
|
if check_fulltext_exists(shop):
|
||||||
|
logger.log(f" [SKIP] {shop}: FULLTEXT existiert bereits")
|
||||||
|
fulltext_stats["skipped"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# FULLTEXT-Index erstellen
|
||||||
|
logger.log(f" [WORK] {shop}: Erstelle FULLTEXT-Index (kann dauern)...")
|
||||||
|
err = create_index(
|
||||||
|
shop,
|
||||||
|
FULLTEXT_INDEX['name'],
|
||||||
|
FULLTEXT_INDEX['table'],
|
||||||
|
FULLTEXT_INDEX['columns'],
|
||||||
|
is_fulltext=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if err:
|
||||||
|
logger.log(f" [FEHLER] {shop}: {err}", "ERROR")
|
||||||
|
fulltext_stats["failed"] += 1
|
||||||
|
else:
|
||||||
|
logger.log(f" [OK] {shop}: FULLTEXT-Index erstellt")
|
||||||
|
fulltext_stats["created"] += 1
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Phase 4: FULLTEXT-Suche aktivieren
|
||||||
|
print("=" * 60)
|
||||||
|
print("PHASE 4: FULLTEXT-Suche aktivieren")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
search_stats = {"updated": 0, "skipped": 0, "failed": 0}
|
||||||
|
|
||||||
|
for shop in shops:
|
||||||
|
current = backup_data.get(shop, "NICHT_GESETZT")
|
||||||
|
|
||||||
|
if current == "Y":
|
||||||
|
logger.log(f" [SKIP] {shop}: Bereits aktiviert")
|
||||||
|
search_stats["skipped"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current == "NICHT_GESETZT":
|
||||||
|
logger.log(f" [SKIP] {shop}: Keine Einstellung vorhanden")
|
||||||
|
search_stats["skipped"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Einstellung ändern
|
||||||
|
if set_search_setting(shop, "Y"):
|
||||||
|
logger.log(f" [OK] {shop}: suche_fulltext = 'Y' (war: '{current}')")
|
||||||
|
search_stats["updated"] += 1
|
||||||
|
else:
|
||||||
|
logger.log(f" [FEHLER] {shop}", "ERROR")
|
||||||
|
search_stats["failed"] += 1
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Zusammenfassung
|
||||||
|
print("=" * 60)
|
||||||
|
print("ZUSAMMENFASSUNG")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print("Standard-Indizes:")
|
||||||
|
print(f" Erstellt: {index_stats['created']}")
|
||||||
|
print(f" Übersprungen: {index_stats['skipped']}")
|
||||||
|
print(f" Fehlgeschlagen: {index_stats['failed']}")
|
||||||
|
print()
|
||||||
|
print("FULLTEXT-Index:")
|
||||||
|
print(f" Erstellt: {fulltext_stats['created']}")
|
||||||
|
print(f" Übersprungen: {fulltext_stats['skipped']}")
|
||||||
|
print(f" Fehlgeschlagen: {fulltext_stats['failed']}")
|
||||||
|
print()
|
||||||
|
print("Sucheinstellung (suche_fulltext = 'Y'):")
|
||||||
|
print(f" Aktualisiert: {search_stats['updated']}")
|
||||||
|
print(f" Übersprungen: {search_stats['skipped']}")
|
||||||
|
print(f" Fehlgeschlagen: {search_stats['failed']}")
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("WICHTIGE DATEIEN")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f" Backup: {BACKUP_FILE}")
|
||||||
|
print(f" Rollback: {ROLLBACK_FILE}")
|
||||||
|
print(f" Log: {LOG_FILE}")
|
||||||
|
print()
|
||||||
|
print("Bei Problemen Rollback ausführen mit:")
|
||||||
|
print(f" python3 {ROLLBACK_FILE}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Log speichern
|
||||||
|
logger.save()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user