diff --git a/mariadb-optimization.py b/mariadb-optimization.py new file mode 100644 index 0000000..f81c21c --- /dev/null +++ b/mariadb-optimization.py @@ -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() \ No newline at end of file