...

MySQL Histograms – Bessere Query-Pläne ohne Index

MySQL Histograms geben dem Optimizer echte Verteilungsdaten an die Hand, damit er Selektivitäten korrekt schätzt und schnellere Query-Pläne produziert – oft sogar ohne zusätzlichen Index. Ich zeige, wie ich Histogramme in MySQL 8+ mit ANALYZE TABLE aufsetze, kontrolliere und für bessere Entscheidungen bei Joins, Filtern und Scans nutze.

Zentrale Punkte

Kurzfokus: Die folgenden Stichpunkte zeigen, worauf ich beim Einsatz von Histogrammen besonders achte.

  • Selektivität statt Bauchgefühl: realistischere Kardinalitäts-Schätzungen
  • Ohne Index schneller: bessere Planwahl bei schiefen Verteilungen
  • Typen verstehen: Singleton vs. Equi-Height gezielt einsetzen
  • Buckets steuern: Auflösung gegen Metadaten-Kosten abwägen
  • Pflege im Blick: Aktualisieren, prüfen, bei Bedarf löschen

Warum Histogramme ohne Index wirken

Ich nutze Histogramme, weil der Optimizer sonst häufig von einer gleichmäßigen Verteilung ausgeht und dadurch schlechte Pläne wählt. Ein Histogramm bildet die Werteverteilung einer Spalte angenähert ab und liefert damit realistische Selektivitäts-Schätzungen für Prädikate wie =, >, BETWEEN, IN oder IS NULL. Der Optimizer entscheidet daraufhin, ob ein Index-Range-Scan, ein Table-Scan oder eine Join-Strategie mit Nested Loops günstiger ist. Trifft eine Bedingung beispielsweise nur 0,1 % der Zeilen, bevorzuge ich einen zielgerichteten Zugriff statt eines breiten Scans. Erfasst ein Filter hingegen fast alle Zeilen, verzichte ich auf teure Index-Zugriffe, die keinen Vorteil bringen und erhöhe so die Effizienz jedes Plans.

Histogramm-Typen in MySQL 8.0

Ich unterscheide zwei Typen: Singleton und Equi-Height. Singleton-Histogramme fassen häufig vorkommende Einzelwerte in separaten Buckets zusammen – ideal für Spalten mit wenigen dominanten Kategorien wie „aktiv“, „inaktiv“ oder „archiviert“. Equi-Height-Histogramme teilen den Wertebereich so auf, dass jeder Bucket ähnlich viele Zeilen enthält; das eignet sich für kontinuierliche oder verzerrte Verteilungen wie Preise, Zeitstempel oder „löchrige“ ID-Ranges. Beide Varianten liefern dem Optimizer genauere Trefferquoten für Filter. Ich wähle den Typ immer nach Dateneigenschaften, nicht nach persönlicher Vorliebe.

Technische Grundlagen: Typwahl in MySQL steuern

MySQL entscheidet die konkrete Histogramm-Variante automatisch anhand der Datenverteilung. Praktisch heißt das: Ist die Anzahl unterschiedlicher Werte (NDV) klein genug im Verhältnis zur Bucket-Anzahl, entsteht effektiv ein Singleton-Histogramm; andernfalls wird ein Equi-Height-Histogramm erzeugt. Ich „wähle“ den Typ daher indirekt, indem ich die geeignete Spalte und eine passende Bucket-Zahl festlege. Für Spalten mit sehr wenigen, aber stark dominanten Kategorien setze ich bewusst wenige Buckets, um Singleton-artige Präzision für diese Werte zu erhalten. Bei fein gestreuten, kontinuierlichen Daten erhöhe ich Buckets schrittweise, bis EXPLAIN die gewünschte Selektivität widerspiegelt.

Wichtig: Histogramme sind einspaltig. Abhängigkeiten zwischen Spalten (z. B. status und country) können sie nicht direkt abbilden. In solchen Fällen hilft es, die selektivste Spalte mit einem Histogramm zu versehen und die Join-Reihenfolge entsprechend zu gestalten.

Buckets richtig wählen

MySQL verwendet standardmäßig 100 Buckets, erlaubt aber 1 bis 1024 über WITH N BUCKETS. Mehr Buckets erhöhen die Auflösung, jedoch wachsen Metadaten und Analyseaufwand. Ich starte meist konservativ, messe die Wirkung auf EXPLAIN, und erhöhe schrittweise, wenn der Plan weiterhin unpassend erscheint. Bei stark konzentrierten Werten (z. B. 90 % ein Status) reichen oft wenige Buckets; bei fein gestreuten Preisen oder Timestamps lohnen mehr Buckets. Ziel ist eine sinnvolle Granularität, die Fehleinschätzungen spürbar reduziert, ohne den Verwaltungsaufwand unnötig zu steigern.

Praxis: Workflow mit ANALYZE TABLE

Ich folge einem klaren Workflow: Zuerst identifiziere ich Spalten, die oft in WHERE- oder JOIN-Bedingungen stehen und erkennbar schiefe Verteilungen aufweisen. Dann erzeuge ich ein Histogramm mit ANALYZE TABLE tbl UPDATE HISTOGRAM ON col WITH N BUCKETS; und prüfe es über INFORMATION_SCHEMA.COLUMN_STATISTICS. Nach Datenverschiebungen aktualisiere ich mit ANALYZE TABLE erneut. Passt eine Statistik nicht, entferne ich sie mit ANALYZE TABLE tbl DROP HISTOGRAM ON col;. Zur Bewertung der Planwirkung lese ich EXPLAIN ANALYZE interpretieren und gleiche Schätzungen gegen tatsächliche Zeilen ab.

Konkrete Befehle und Kontrolle

Ich arbeite reproduzierbar mit wenigen, klaren Schritten und überprüfe die erzeugten JSON-Statistiken.

-- Histogramme auf einzelnen Spalten anlegen
ANALYZE TABLE orders UPDATE HISTOGRAM ON status WITH 32 BUCKETS;
ANALYZE TABLE orders UPDATE HISTOGRAM ON created_at WITH 128 BUCKETS;

-- Mehrere Spalten in einem Lauf mit gleicher Bucket-Zahl
ANALYZE TABLE orders UPDATE HISTOGRAM ON status, payment_method WITH 64 BUCKETS;

-- Histogramme gezielt löschen
ANALYZE TABLE orders DROP HISTOGRAM ON status;
-- Sichtprüfung der Statistik
SELECT
  SCHEMA_NAME, TABLE_NAME, COLUMN_NAME,
  JSON_PRETTY(HISTOGRAM) AS histogram
FROM INFORMATION_SCHEMA.COLUMN_STATISTICS
WHERE SCHEMA_NAME = DATABASE()
  AND TABLE_NAME = 'orders'
  AND COLUMN_NAME IN ('status','created_at');

Ich bewerte die Wirkung unmittelbar mit EXPLAIN ANALYZE:

EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE status = 'canceled'
  AND created_at >= NOW() - INTERVAL 7 DAY;

Verbessert sich die Schätzung rows spürbar und kippt der Plan z. B. von Full Scan auf Index-Range-Scan oder ändert die Join-Reihenfolge, war die Maßnahme erfolgreich. Bleibt die Abweichung groß, erhöhe oder reduziere ich Buckets und vergleiche erneut.

Beispiel: Order-Status und seltene Werte

In einer orders-Tabelle dominiert oft der Status „completed“, während „pending“ mittelhäufig und „canceled“ sehr selten ist; diese Schieflage führt ohne Histogramm leicht zu falschen Selektivitäten. Fragt eine API „canceled“ ab, kann der Optimizer fälschlich einen Full Table Scan wählen, obwohl ein enger Indexzugriff genügt. Mit einem Singleton-Histogramm erkennt MySQL, dass „canceled“ nur einen winzigen Anteil ausmacht und wechselt auf Index-Range-Scan oder optimiert die Join-Reihenfolge. So sinkt die Latenz, und ich brauche keinen zusätzlichen Index für jede Variante eines Filters. In Dashboards mit harten SLOs bringt diese Korrektur oft spürbare Reaktionsvorteile.

Zeitreihen und Timestamps

Bei Zeitreihen liegen viele Zugriffe auf frischen Daten; ältere Zeitfenster bleiben meist kalt. Ein Equi-Height-Histogramm auf created_at oder updated_at unterscheidet stark frequentierte Zeitabschnitte von selten genutzten. Der Optimizer schätzt dann korrekt, ob ein Range-Scan sinnvoll ist oder ein Table-Scan schneller ans Ziel führt. Besonders bei partiellen Zeitfiltern über große Tabellen erkenne ich deutliche Planwechsel und geringere I/O-Kosten. Ich halte die Statistiken hier häufiger aktuell, weil sich der Schwerpunkt mit dem Tagesgeschäft verschiebt.

Partitionen, Datentypen und Kollationen

Auf partitionierten Tabellen betrachte ich die Datenverteilung über alle Partitionen. Starke Unterschiede (z. B. nach Monaten) können globale Histogramme glätten. Falls einzelne Partitionen extrem selektiv oder extrem breit sind, teste ich zusätzlich mit Partition-Pruning-Filtern im WHERE, ob die Planqualität trotzdem passt. Insgesamt achte ich darauf, Filter so zu formulieren, dass MySQL Partitionen früh ausschließen kann.

Histogramme funktionieren am besten auf skalaren, vergleichbaren Datentypen (Zahlen, Datums-/Zeitwerte, VARCHAR/CHAR mit passender Kollation). Bei LOB-/JSON-Daten setze ich eher auf Generated Columns mit extrahierten, typisierten Werten und versiehe diese bei Bedarf mit Histogrammen oder Indizes. Bei Strings bestimmt die Kollation die Vergleichslogik; je nach Collation können Werte zusammenfallen (z. B. Groß-/Kleinschreibung). Ich halte die Collation konsistent mit den Abfragen, um realistische Selektivitäten zu erhalten.

Grenzen und Fehlgriffe

Histogramme schätzen vor allem Einzelspalten mit Konstanten gut; mehrspaltige Abhängigkeiten bilden sie nur begrenzt ab. Bei stark korrelierten Spalten oder dynamischen Parametern (z. B. anwendungsseitig befüllt) stoßen sie an Grenzen. Boolean-Felder oder Spalten mit nahezu gleichmäßiger Verteilung profitieren selten von zusätzlicher Statistik. Zu viele Buckets und übermäßige Pflege können wiederum Verwaltungs- und Analysezeit erhöhen. Ich setze Histogramme deshalb gezielt ein und prüfe regelmäßig die Wirkung auf reale Ausführungen.

Optimizer-Kontrolle und Aktualisierung

Ich kontrolliere den Einsatz von Histogrammen über ANALYZE TABLE und relevante Optimizer-Optionen, damit der Planer die Statistiken sinnvoll nutzt. In arbeitsreichen Systemen plane ich die Aktualisierung in ruhigen Zeitfenstern oder batchweise nach größeren Loads. Vorher und nachher vergleiche ich EXPLAIN- und EXPLAIN ANALYZE-Ausgaben, um geänderte Join-Reihenfolgen, Filterschritte und Kostenmodelle zu bewerten. Bei negativen Effekten reagiere ich sofort und rolle eine Statistik zurück. Für weiterführende Steuerung der Optimizer-Optionen achte ich darauf, dass Abhängigkeiten mit anderen Statistiken nicht unbemerkt falsche Annahmen erzeugen.

Überwachung, Regressionsschutz und Playbook

Ich baue mir ein leichtgewichtiges Playbook für den Produktivbetrieb:

  • Baseline festlegen: vor Änderungen EXPLAIN ANALYZE, Laufzeit, „rows examined“, Handler-Zähler aufnehmen.
  • Histogramm anlegen/ändern: gezielt auf die Filterspalten, konservative Buckets.
  • Direkt danach messen: Plan, geschätzte vs. tatsächliche Zeilen; Faktor >10 Abweichung ist für mich ein Warnsignal.
  • Feinjustierung: Buckets hoch/runter; ggf. Filterreihenfolge im Query anpassen.
  • Rollback parat halten: DROP HISTOGRAM, falls Latenzen steigen.
  • Automatisierung: nach ETL-Loads oder größeren DML-Wellen ANALYZE in Wartungsfenstern.

Zur Ursachenanalyse nutze ich Optimizer-Traces und EXPLAIN ANALYZE, um zu erkennen, ob der Planer auf Basis der Histogramme die richtige selektive Tabelle „nach vorne“ zieht. Für A/B-Tests fixe ich testweise die Join-Reihenfolge (STRAIGHT_JOIN) oder erzwinge/unterbinde einzelne Indizes, um den Effekt der Statistik isoliert zu bewerten.

Organisatorisch bewährt sich ein kurzes Change-Log pro Tabelle: Spalte, Bucket-Anzahl, Zeitpunkt, Messwerte vorher/nachher. Das erleichtert spätere Korrekturen und verhindert unklare Wechselwirkungen.

Operative Aspekte: Sperren, Kosten, Portabilität

ANALYZE TABLE nimmt eine Metadaten-Sperre auf der Tabelle, blockiert übliche Lese-/Schreibvorgänge aber nicht dauerhaft. Auf sehr großen Tabellen plane ich genug Zeit ein; Histogramm-Generierung arbeitet mit Stichproben und ist speicherbegrenzt (Stichwort: interner Arbeitsspeicher für die Berechnung). Der Platzbedarf der Statistik selbst bleibt moderat: Ein paar Dutzend bis wenige Hundert Kilobyte pro Spalte mit 100–256 Buckets sind ein realistischer Richtwert. In Summe kalkuliere ich trotzdem, denn viele Spalten mal viele Tabellen ergeben sichtbare Metadaten.

Bei Logical Dumps (mysqldump) wandern Histogramme nicht als Daten mit; nach einem Restore erstelle ich sie gezielt neu. Beim In-Place-Upgrade bleiben sie erhalten. Rechte-seitig benötige ich ausreichende Privilegien für ANALYZE TABLE auf den jeweiligen Objekten; in streng regulierten Umgebungen binde ich die Pflege in Wartungspipelines ein.

Wann Histogramme nichts bringen

Ich spare mir Histogramme auf Spalten, die sehr wenige Werte besitzen und ohnehin gut eingeschätzt werden. Auch dort, wo ein guter Index bereits minimale Treffermengen abdeckt, liefert ein Histogramm selten zusätzlichen Gewinn. Gleichmäßige Verteilungen erfordern keine aufwendige Feinkörnigkeit. In hochdynamischen, schreibintensiven Systemen kann die Pflege unnötige Last erzeugen, wenn ich sie zu häufig anstoße. In solchen Lagen setze ich die Energie lieber in Index-Strategien, Query-Design und Caching.

Tabellarischer Spickzettel

Ich nutze den folgenden Überblick für schnelle Entscheidungen: Welcher Histogramm-Typ passt, wie setze ich Buckets, und welche Kosten entstehen. Die Tabelle dient als Gedächtnisstütze bei Reviews von Problem-Queries. Ich aktualisiere sie nach Lerneffekten aus EXPLAIN ANALYZE und Produktionsmetriken. Dabei beachte ich, dass Datenverteilungen sich ändern und historische Annahmen veralten. Entscheidend bleibt, die Planqualität mit realen Messungen zu bestätigen.

Aspekt Empfehlung Nutzen Trade-off Beispiel
Typ Singleton bei wenigen, dominanten Werten Genaue Trefferquoten für häufige Kategorien Wenig hilfreich bei kontinuierlichen Bereichen order_status
Typ Equi-Height bei verzerrten, kontinuierlichen Daten Bessere Schätzung entlang des Wertebereichs Mehr Metadaten bei vielen Buckets created_at, price
Buckets Start bei 100, dann anpassen Ausgewogene Auflösung Höhere Analyse- und Speicherlast bei 512–1024 WITH 100 BUCKETS
Pflege Nach größeren Datenänderungen ANALYZE Aktuelle Selektivitäten Wartungsfenster einplanen ANALYZE TABLE … UPDATE HISTOGRAM
Kontrolle Über COLUMN_STATISTICS prüfen Transparenz und Audit JSON-Interpretation erforderlich INFORMATION_SCHEMA.COLUMN_STATISTICS

Einordnung ins Tuning-Gesamtbild

Ich behandle Histogramme als Baustein neben Indizes, Query-Design, Caching und Hardware-Parametern. Häufig verlagert ein gutes Histogramm die Join-Reihenfolge, senkt I/O und sorgt für konstante Antwortzeiten. Trotzdem ersetze ich damit keine sauberen Index-Strategien und kein effizientes Schema. Wer tiefer in Planentscheidungen schaut, profitiert von Execution-Pläne verstehen und vergleicht Kostenmodelle mit realen Laufzeiten. Ich prüfe regelmäßig, ob die Workloads noch zu den Statistiken passen oder ob Anpassungen fällig sind.

Fortgeschrittene Join-Szenarien

Besonders lohnend sind Histogramme, wenn mehrere Tabellen mit Filtern beteiligt sind. Beispiel:

SELECT o.id, o.amount
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'DE'
  AND o.status = 'canceled'
  AND o.created_at >= NOW() - INTERVAL 30 DAY;

Ohne Histogramme unterschätzt der Optimizer ggf. die Selektivität von o.status=’canceled‘ oder überschätzt den Anteil deutscher Nutzer. Mit einem Histogramm auf u.country und o.status (ggf. auch auf o.created_at) erkennt der Planer meist, dass die Kombination extrem selektiv ist. In der Praxis sehe ich dann, dass MySQL zuerst die kleinere Teilmenge bestimmt (z. B. via Index auf users(country) oder orders(status, created_at)) und erst danach den Join ausführt – statt die große Tabelle zu scannen. Das spart I/O, Puffer und CPU und stabilisiert die Latenz auch unter Last.

Weil Histogramme nur einspaltig sind, bleiben Index-Strategien wichtig: Ein zusammengesetzter Index auf (status, created_at) kann den Range-Scan weiter beschleunigen. Das Histogramm sorgt hier vor allem dafür, dass der Optimizer diese Strategie überhaupt als günstig erkennt.

Zusammenfassung für die Praxis

Ich setze MySQL-Histogramme ein, wenn der Optimizer mit Standardstatistiken danebenliegt und schiefe Verteilungen falsche Pläne erzeugen. Mit ANALYZE TABLE baue, aktualisiere und entferne ich gezielt Statistiken auf den Spalten, die in Filtern und Joins dominieren. Die Wahl zwischen Singleton und Equi-Height treffe ich anhand der Daten, die Bucket-Anzahl kalibriere ich mit Messungen. Über EXPLAIN ANALYZE kontrolliere ich, ob sich Join-Reihenfolgen, Filterpositionen und Scans wie gewünscht ändern. So erreiche ich mit wenig Overhead spürbar schnellere Queries – oft ohne weitere Indizes.

Aktuelle Artikel

Rechenzentrum mit Serverracks und stilisierter Datenvisualisierung zur MySQL-Performanceoptimierung
Datenbanken

MySQL Histograms – Bessere Query-Pläne ohne Index

Erfahre, wie MySQL Histograms den Optimizer mit präzisen optimizer statistics versorgen, bessere Query-Pläne ermöglichen und dein sql tuning ohne zusätzliche Indexe deutlich verbessern.