MySQL Query-Optimierung: Ein praktischer Leitfaden

Eine langsame Abfrage ist selten nur ein langsames Statement. In einer MySQL-Anwendung kann sie die API-Latenz erhöhen, Locks länger halten, Buffer Pool und CPU verbrauchen und Dashboards genau dann unzuverlässig wirken lassen, wenn Nutzer sie am meisten brauchen. Das Ziel ist nicht, Indizes hinzuzufügen, bis die Abfrage auf dem eigenen Laptop schnell aussieht. Das Ziel ist zu verstehen, was MySQL tut, die Arbeitslast zu reduzieren und dem Optimizer einen Zugriffspfad zu geben, der auch dann noch funktioniert, wenn die Tabelle zehnmal größer ist. Dieser Leitfaden verwendet MySQL-8-Beispiele, aber die meisten Konzepte gelten auch für aktuelle MySQL-kompatible Systeme.

MySQL-Abfrage-Optimierung: ein praktischer Leitfaden

Eine langsame Abfrage ist selten nur ein langsames Statement. In einer MySQL-Anwendung kann sie die API-Latenz erhöhen, Locks länger halten, Buffer Pool und CPU verbrauchen und Dashboards genau dann unzuverlässig wirken lassen, wenn Nutzer sie am meisten benötigen.

Das Ziel ist nicht, Indizes hinzuzufügen, bis die Abfrage auf dem eigenen Laptop schnell aussieht. Das Ziel ist zu verstehen, was MySQL tut, die Menge der Arbeit zu reduzieren, die es leisten muss, und dem Optimizer einen Zugriffspfad zu geben, der auch dann noch funktioniert, wenn die Tabelle zehnmal größer ist.

Dieser Leitfaden verwendet MySQL-8-Beispiele, aber die meisten Ideen gelten auch für aktuelle MySQL-kompatible Systeme.


Mit dem Plan beginnen, nicht mit der Vermutung

Wenn eine Abfrage langsam ist, beantworte zuerst drei Fragen:

  1. Wie viele Zeilen muss MySQL lesen?
  2. Welche Indizes, falls vorhanden, verwendet es?
  3. Sortiert, aggregiert oder materialisiert es ein großes Zwischenergebnis?

Verwende EXPLAIN für einen schnellen Plan und EXPLAIN ANALYZE, wenn tatsächliche Laufzeitdetails benötigt werden.

EXPLAIN ANALYZE
SELECT o.id, o.order_date, o.total_amount
FROM orders o
WHERE o.customer_id = 42
ORDER BY o.order_date DESC
LIMIT 10;

Achte in MySQL besonders auf diese Spalten und Meldungen:

  • type: const, ref und range sind normalerweise besser als ALL.
  • key: der Index, den MySQL gewählt hat. Wenn dies NULL ist, scannt MySQL ohne Index.
  • rows: die geschätzte Anzahl der Zeilen, die MySQL voraussichtlich untersucht.
  • filtered: der geschätzte Prozentsatz der Zeilen, die nach Anwendung der Bedingung übrig bleiben.
  • Extra: suche nach Using index, Using where, Using temporary und Using filesort.

Using filesort bedeutet nicht immer eine Festplatten-Sortierung und ist nicht immer schlecht. Es bedeutet, dass MySQL Zeilen nicht direkt in der erforderlichen Reihenfolge vom gewählten Index zurückgeben kann. Das wird teuer, wenn die Eingabe groß ist.

Eine nützliche erste Prüfung ist, ob sich die Plan-Form von einem breiten Scan zu einem indizierten Lookup ändert. Vor dem Hinzufügen des Kunden-/Datumsindex könnte der Plan ungefähr so aussehen:

type: ALL
key: NULL
rows: 1200000
Extra: Using where; Using filesort

Nach dem Hinzufügen eines Index, der zum Filter und zur Sortierung passt, möchtest du einen schmaleren Zugriffstyp und den erwarteten Indexnamen sehen:

type: ref
key: idx_orders_customer_date
rows: 42
Extra: Using where

Die genauen Zahlen hängen von den Daten ab, aber die Richtung ist wichtig: weniger untersuchte Zeilen, ein gewählter Index und weniger temporäre Sortierarbeit.


Ein wiederholbarer Optimierungs-Workflow

1. Die echte Abfrage messen

Führe die Abfrage mit realistischen Parametern und Datenvolumen aus. Eine Abfrage, die für einen Kunden schnell ist, kann für einen anderen langsam sein, wenn die Datenverteilung schief ist.

Nützliche Messungen:

  • Wanduhr-Laufzeit
  • zurückgegebene Zeilen
  • untersuchte Zeilen in EXPLAIN ANALYZE
  • Slow-Query-Log oder Performance-Schema-Daten für produktionsähnliche Workloads
  • ob die Abfrage temporäre Tabellen oder Filesorts erstellt
  • ob sie allein oder unter gleichzeitiger Schreiblast läuft

2. Die Zeilen früh reduzieren

Die meisten Abfrage-Optimierungen beginnen hier. Filtere vor dem Joinen wenn möglich, vermeide unnötige Spalten und mache Prädikate einfach für MySQL, damit es einen Index verwenden kann.

3. Den richtigen Index hinzufügen

Indizes helfen, wenn sie MySQL ermöglichen, den größten Teil der Tabelle zu überspringen oder eine große Sortierung zu vermeiden. Ein guter Index passt zur Abfrage-Form: Gleichheitsfilter zuerst, dann Bereichsfilter, dann Sortierungsspalten wenn möglich.

4. Den Plan erneut prüfen

Vergleiche nach jeder Änderung den alten und neuen Plan. Eine Abfrage-Umschreibung, die sauberer aussieht, ist nicht automatisch schneller, und ein Index, der einer Abfrage hilft, kann Schreibvorgänge verlangsamen oder vom Optimizer ignoriert werden.


Beispiel: neueste Bestellungen für einen Kunden

Stelle dir diese Dashboard-Abfrage vor:

SELECT id, order_date, total_amount, status
FROM orders
WHERE customer_id = 42
ORDER BY order_date DESC
LIMIT 10;

Ohne einen nützlichen Index kann MySQL viele Zeilen für customer_id = 42 scannen, sie nach order_date sortieren und dann nur zehn behalten.

Ein besserer Index passt zum Filter und zur Sortierung:

CREATE INDEX idx_orders_customer_date
ON orders (customer_id, order_date DESC, id);

Warum das hilft:

  • customer_id lässt MySQL zu den Bestellungen eines Kunden springen.
  • order_date DESC lässt MySQL die neuesten Zeilen zuerst lesen.
  • id gibt einen stabilen Tie-Breaker, wenn mehrere Bestellungen denselben Zeitstempel haben.

Jetzt kann die Abfrage normalerweise nach dem Lesen der ersten zehn passenden Index-Einträge stoppen, anstatt jede Bestellung für diesen Kunden zu sortieren.

Wenn die Abfrage kritisch ist und vom Index abgedeckt werden soll, füge die ausgewählten Spalten im Index hinzu:

CREATE INDEX idx_orders_customer_date_covering
ON orders (customer_id, order_date DESC, id, total_amount, status);

MySQL hat keine separaten INCLUDE-Spalten für abdeckende Indizes. Jede Spalte in einem sekundären Index ist Teil der Index-Struktur, daher müssen abdeckende Indizes bewusst verwendet werden. Sie können Lesevorgänge beschleunigen, aber sie benötigen auch Speicherplatz und verlangsamen Schreibvorgänge. Breite Textspalten sind normalerweise schlechte Kandidaten für dieses Muster; decke die kleinen Spalten ab, die eine kritische Abfrage günstiger machen, nicht jede Spalte, die die Anwendung möglicherweise anzeigt.


Prädikate schreiben, die MySQL verwenden kann

Funktionen auf indizierten Spalten vermeiden

Diese Abfrage sieht vernünftig aus, macht aber die indizierte Spalte schwerer nutzbar:

-- Vermeiden
SELECT id, total_amount
FROM orders
WHERE DATE(order_date) = '2026-05-01';

Bevorzuge ein Bereichsprädikat:

-- Bevorzugen
SELECT id, total_amount
FROM orders
WHERE order_date >= '2026-05-01'
  AND order_date <  '2026-05-02';

Das ist wichtig, wenn order_date ein DATETIME oder TIMESTAMP ist. Der Index ist nach den ursprünglichen Zeitstempelwerten geordnet, wie 2026-05-01 09:30:00, nicht nach dem Ergebnis von DATE(order_date). Wenn die Abfrage die Spalte in eine Funktion einwickelt, muss MySQL diese Funktion möglicherweise für viele Zeilen berechnen, bevor es das Datum vergleichen kann.

Die Bereichsversion lässt die indizierte Spalte unverändert. MySQL kann zum ersten Wert am 2026-05-01 springen, durch diesen Tag vorwärts scannen und vor 2026-05-02 stoppen. Das bedeutet normalerweise weniger untersuchte Zeilen, weniger CPU und weniger Druck auf den Buffer Pool.

Der Punkt ist nicht, dass = schlecht ist. Gleichheit ist ausgezeichnet, wenn sie direkt mit der indizierten Spalte verglichen wird. Wenn order_date nur eine DATE-Spalte wäre, wäre = völlig in Ordnung:

-- Gutes Gleichheitsprädikat
WHERE customer_id = 42
-- Gut, wenn order_date eine DATE-Spalte ist
WHERE order_date = '2026-05-01'

Verwende das Bereichsmuster, wenn eine Zeitstempelspalte einen ganzen Tag, Monat oder ein anderes Zeitfenster matchen muss.

Implizite Typkonvertierungen vermeiden

Wenn customer_id eine Ganzzahl ist, vergleiche sie mit einer Ganzzahl:

-- Vermeiden
WHERE customer_id = '42'

-- Bevorzugen
WHERE customer_id = 42

Implizite Konvertierungen können überraschende Pläne erzeugen, besonders wenn der Spaltentyp und der Literaltyp nicht übereinstimmen.

Vorsicht mit führenden Wildcards

Normale B-Tree-Indizes können bei Präfix-Suchen helfen, aber nicht bei beliebigen Teilstring-Suchen:

-- Kann in vielen Fällen einen Index auf sku verwenden
WHERE sku LIKE 'ABC%'

-- Kann normalerweise keinen normalen B-Tree-Index effektiv verwenden
WHERE sku LIKE '%ABC%'

Wenn Teilstring-Suche eine echte Funktion ist, erwäge eine dedizierte Such-Strategie, anstatt zu erwarten, dass ein normaler Index sie löst.


Zusammengesetzte Indizes: Reihenfolge ist wichtig

MySQL verwendet zusammengesetzte Indizes von links nach rechts. Dies wird oft als Leftmost-Prefix-Regel bezeichnet.

Für diese Abfrage:

SELECT id, order_date, total_amount
FROM orders
WHERE tenant_id = 'foo'
  AND customer_id = 42
  AND order_date >= '2026-01-01'
ORDER BY order_date DESC
LIMIT 20;

Ein nützlicher Index ist:

CREATE INDEX idx_orders_tenant_customer_date
ON orders (tenant_id, customer_id, order_date DESC, id);

Die Gleichheitsspalten kommen zuerst: tenant_id, dann customer_id. Die Bereichs- und Sortierspalte order_date kommt nach ihnen. Das lässt MySQL die Suche eingrenzen, bevor es den Datumsbereich scannt.

Vermeide es, Indizes Spalte für Spalte zu erstellen und zu hoffen, dass MySQL sie gut kombiniert:

CREATE INDEX idx_orders_tenant ON orders (tenant_id);
CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE INDEX idx_orders_date ON orders (order_date);

MySQL kann manchmal Index-Merge verwenden, aber ein zusammengesetzter Index, der zur Abfrage passt, ist normalerweise vorhersehbarer und schneller.


Sortieren und Gruppieren: große temporäre Arbeit vermeiden

In MySQL zeigen teure ORDER BY- und GROUP BY-Abfragen oft Using temporary oder Using filesort in EXPLAIN.

Betrachte ein Dashboard-Widget, das die größten Kunden dieses Jahres zeigt:

SELECT customer_id, SUM(total_amount) AS revenue
FROM orders
WHERE order_date >= '2026-01-01'
  AND order_date <  '2027-01-01'
GROUP BY customer_id
ORDER BY revenue DESC
LIMIT 10;

MySQL muss alle passenden Bestellungen aggregieren, bevor es die Top-Ten-Kunden kennt. Ein Index kann beim Datumsfilter helfen, aber er kann das Berechnen der Summen nicht magisch vermeiden.

Das ORDER BY revenue DESC ist besonders wichtig: revenue wird nach GROUP BY berechnet, daher kann ein Index auf (order_date, customer_id) die Zeilen, die in die Aggregation eingehen, reduzieren, aber er kann das Endergebnis nicht bereits nach Umsatz sortiert zurückgeben.

Gute Optionen hängen von der Produktanforderung ab:

  • Wenn das Widget oft angezeigt wird, pflege eine Zusammenfassungstabelle wie customer_revenue_daily.
  • Wenn exakte Aktualität nicht erforderlich ist, berechne das Ergebnis nach einem Zeitplan vor.
  • Wenn der Datumsbereich klein ist, indiziere den Filter, damit MySQL weniger Zeilen liest.

Für die rohe Abfrage kann dieser Index die Eingabe reduzieren:

CREATE INDEX idx_orders_date_customer
ON orders (order_date, customer_id);

Aber wenn diese Abfrage ständig über Millionen von Zeilen ausgeführt wird, ist eine Zusammenfassungstabelle oft die echte Lösung. Indizes reduzieren Arbeit; sie beseitigen nicht die Notwendigkeit, große Datensätze zu aggregieren.


LIMIT hilft nur, nachdem MySQL Zeilen günstig finden kann

LIMIT 10 macht eine Abfrage nicht automatisch günstig. Es hilft, wenn MySQL einen Index verwenden kann, um die ersten zehn Zeilen in der gewünschten Reihenfolge zu finden.

Schnelles Muster:

SELECT id, order_date, total_amount
FROM orders
WHERE customer_id = 42
ORDER BY order_date DESC
LIMIT 10;

Passender Index:

CREATE INDEX idx_orders_customer_date
ON orders (customer_id, order_date DESC, id);

Langsameres Muster:

SELECT id, order_date, total_amount
FROM orders
WHERE status = 'paid'
ORDER BY total_amount DESC
LIMIT 10;

Wenn status = 'paid' die meisten Zeilen matched und es keinen nützlichen ordnungserhaltenden Index gibt, muss MySQL möglicherweise immer noch einen großen Teil der Tabelle untersuchen und sortieren, bevor es zehn Zeilen zurückgibt.

Für Paginierung vermeide tiefe Offsets auf großen Tabellen:

-- Teuer bei hohen Seitenzahlen
SELECT id, order_date
FROM orders
WHERE customer_id = 42
ORDER BY order_date DESC, id DESC
LIMIT 50 OFFSET 50000;

Bevorzuge Keyset-Paginierung:

SELECT id, order_date
FROM orders
WHERE customer_id = 42
  AND (order_date, id) < ('2026-05-01 12:00:00', 987654)
ORDER BY order_date DESC, id DESC
LIMIT 50;

Mit einem Index auf (customer_id, order_date DESC, id DESC) kann MySQL von der zuletzt gesehenen Zeile fortfahren, anstatt durch Tausende übersprungener Zeilen zu zählen.

Da die Abfrage nach beiden Spalten absteigend sortiert, fragt die nächste Seite nach Zeilen, die vor dem zuletzt gesehenen (order_date, id)-Paar kommen. Wenn der Tupel-Vergleich zu kompakt wirkt, kann dieselbe Idee expliziter geschrieben werden:

WHERE customer_id = 42
  AND (
    order_date < '2026-05-01 12:00:00'
    OR (order_date = '2026-05-01 12:00:00' AND id < 987654)
  )

Existenzprüfungen, Counts und Abrufen von Zeilen sind unterschiedliche Aufgaben

Verwende EXISTS für boolesche Prüfungen

Wenn nur geprüft werden muss, ob eine Zeile existiert, zähle nicht alle Treffer:

-- Vermeiden
SELECT COUNT(*)
FROM orders
WHERE customer_id = 42
  AND status = 'open';
-- Bevorzugen
SELECT EXISTS (
  SELECT 1
  FROM orders
  WHERE customer_id = 42
    AND status = 'open'
);

EXISTS kann stoppen, sobald MySQL die erste passende Zeile findet.

Verwende COUNT(*) wenn die Anzahl wirklich benötigt wird

Zählen ist echte Arbeit. Halte das Prädikat index-freundlich:

SELECT COUNT(*)
FROM orders
WHERE order_date >= '2026-01-01'
  AND order_date <  '2027-01-01';

Ein Index auf order_date kann MySQL helfen, den passenden Bereich zu zählen, aber es muss immer noch die passenden Index-Einträge untersuchen.

Hole eine Zeile ohne Sortierung ab, es sei denn die Reihenfolge ist wichtig

Wenn irgendeine passende Zeile in Ordnung ist:

SELECT id
FROM orders
WHERE customer_id = 42
LIMIT 1;

Wenn die neueste Zeile wichtig ist, mache die Reihenfolge explizit und indiziere dafür:

SELECT id, order_date
FROM orders
WHERE customer_id = 42
ORDER BY order_date DESC, id DESC
LIMIT 1;

Joins: vor dem Multiplizieren reduzieren

Joins werden teuer, wenn große Zeilenmengen aufeinander treffen. Der häufigste Fehler ist, zuerst Detail-Tabellen zu joinen und später zu filtern oder aggregieren.

Angenommen, Umsatz nach Produktkategorie für einen Monat wird benötigt:

SELECT p.category_id, SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.order_date >= '2026-05-01'
  AND o.order_date <  '2026-06-01'
  AND o.status = 'paid'
GROUP BY p.category_id;

Hilfreiche Indizes:

CREATE INDEX idx_orders_status_date_id
ON orders (status, order_date, id);

CREATE INDEX idx_order_items_order_product
ON order_items (order_id, product_id);

Der erste Index hilft MySQL, die bezahlten Bestellungen im Monat zu finden. Der zweite hilft beim Join von diesen Bestellungen zu ihren Artikeln.

Wenn order_items riesig ist und dieser Report oft läuft, kann das Voraggregieren des monatlichen Umsatzes besser sein als wiederholtes Joinen und Gruppieren roher Bestellzeilen.

Achte auch auf versehentliche Many-to-Many-Joins. Wenn ein Join unerwartet Zeilen multipliziert, kann ein späteres GROUP BY das Korrektheitsproblem verbergen, während es MySQL trotzdem zwingt, ein viel größeres Zwischenergebnis zu verarbeiten.


OR vs Joins für Index-Lookups

OR ist nicht automatisch schlecht. Es ist in Ordnung, wenn die Bedingungen einfach sind und MySQL immer noch einen guten Zugriffspfad verwenden kann. Aber OR wird riskant, wenn jeder Zweig einen anderen Index will oder wenn die Bedingung innerhalb eines Join-Prädikats erscheint.

Beispiel:

SELECT id, order_date, total_amount
FROM orders
WHERE tenant_id = 'foo'
  AND (external_order_id = 'A-10001' OR marketplace_order_id = 'B-90001');

Wenn separate Indizes für jede Lookup-Form vorhanden sind, wählt MySQL möglicherweise nicht den besten Pfad für beide Seiten des OR:

CREATE INDEX idx_orders_tenant_external
ON orders (tenant_id, external_order_id);

CREATE INDEX idx_orders_tenant_marketplace
ON orders (tenant_id, marketplace_order_id);

Eine klare Umschreibung ist, den Lookup in zwei index-freundliche Abfragen aufzuteilen:

SELECT id, order_date, total_amount
FROM orders
WHERE tenant_id = 'foo'
  AND external_order_id = 'A-10001'

UNION ALL

SELECT id, order_date, total_amount
FROM orders
WHERE tenant_id = 'foo'
  AND marketplace_order_id = 'B-90001';

Jeder Zweig kann den Index verwenden, der zu seinem Prädikat passt. Verwende UNION ALL, wenn Duplikate unmöglich oder akzeptabel sind. Verwende UNION oder dedupliziere nach Primärschlüssel in der Anwendung, wenn dieselbe Bestellung beide Lookup-Werte matchen kann.

Dasselbe Problem kann in Joins auftreten. Dieses Muster ist schwer zu optimieren, weil der Join über zwei verschiedene Spalten matchen kann:

-- Nach Möglichkeit vermeiden
SELECT o.id, m.mapping_id
FROM orders o
JOIN marketplace_mappings m
  ON m.tenant_id = o.tenant_id
 AND (m.external_order_id = o.external_order_id
      OR m.marketplace_order_id = o.marketplace_order_id)
WHERE o.tenant_id = 'foo';

Wenn das Datenmodell zwei alternative Join-Keys hat, teile den Join in einen Lookup-Typ pro Zweig auf:

SELECT o.id, m.mapping_id
FROM orders o
JOIN marketplace_mappings m
  ON m.tenant_id = o.tenant_id
 AND m.external_order_id = o.external_order_id
WHERE o.tenant_id = 'foo'

UNION ALL

SELECT o.id, m.mapping_id
FROM orders o
JOIN marketplace_mappings m
  ON m.tenant_id = o.tenant_id
 AND m.marketplace_order_id = o.marketplace_order_id
WHERE o.tenant_id = 'foo';

Das gibt MySQL in jedem Zweig eine einfachere Wahl: verwende den Index für den External-Order-Lookup, dann verwende den Index für den Marketplace-Order-Lookup.

Für größere oder wiederholte Lookup-Workloads ist das sauberere Design oft eine normalisierte Lookup-Tabelle mit einer indizierten Key-Form:

CREATE TABLE order_lookup_keys (
  tenant_id VARCHAR(16) NOT NULL,
  lookup_type VARCHAR(32) NOT NULL,
  lookup_value VARCHAR(128) NOT NULL,
  order_id BIGINT NOT NULL,
  PRIMARY KEY (tenant_id, lookup_type, lookup_value),
  INDEX idx_order_lookup_order (order_id)
);

Dann wird der Lookup zu einem einfachen indizierten Join:

SELECT o.id, o.order_date, o.total_amount
FROM order_lookup_keys lk
JOIN orders o ON o.id = lk.order_id
WHERE lk.tenant_id = 'foo'
  AND lk.lookup_type = 'external'
  AND lk.lookup_value = 'A-10001';

Das ist wichtig, weil MySQL einen vorhersehbaren zusammengesetzten Index-Lookup verwenden kann, anstatt über mehrere optionale Pfade im selben Prädikat nachzudenken. Wie immer mit EXPLAIN bestätigen: manchmal behandelt MySQL das OR gut, und manchmal entfernt die Umschreibung einen großen Scan.


UNION vs UNION ALL

UNION entfernt Duplikate. Das bedeutet, dass MySQL möglicherweise zusätzliche temporäre Arbeit benötigt. Wenn Duplikate akzeptabel oder unmöglich sind, verwende UNION ALL.

-- Vermeiden, wenn Deduplizierung nicht benötigt wird
SELECT customer_id FROM orders_2025
UNION
SELECT customer_id FROM orders_2026;
-- Bevorzugen, wenn Duplikate akzeptabel oder unmöglich sind
SELECT customer_id FROM orders_2025
UNION ALL
SELECT customer_id FROM orders_2026;

Statistiken frisch halten

Der Optimizer wählt einen Plan basierend auf Statistiken. Wenn diese Statistiken veraltet oder für schiefe Daten zu grob sind, kann MySQL eine schlechte Join-Reihenfolge oder den falschen Index wählen.

Das kann nach normalem Produktwachstum passieren. Stelle dir eine orders-Tabelle vor, die mehrere Wochen lang nur einen Tenant hat. Während dieser Zeit matched tenant_id = 'foo' fast jede Zeile, sodass MySQL möglicherweise lernt, dass tenant_id nicht selektiv ist und einen Index bevorzugt, der mit order_date beginnt.

CREATE INDEX idx_orders_date ON orders (order_date);
CREATE INDEX idx_orders_tenant_date ON orders (tenant_id, order_date);

Dann wird ein zweiter Tenant hinzugefügt und beginnt, eine große Datenmenge zu importieren. Eine tenant-gefilterte Dashboard-Abfrage benötigt jetzt die Zeilen eines Tenants für einen Datumsbereich:

SELECT COUNT(*)
FROM orders
WHERE tenant_id = 'bar'
  AND order_date >= '2026-05-01'
  AND order_date <  '2026-06-01';

Wenn MySQL noch Statistiken aus der Ein-Tenant-Phase hat, unterschätzt es möglicherweise, wie nützlich tenant_id geworden ist, und wählt idx_orders_date. Das kann es zwingen, alle Tenants im Datumsbereich zu scannen und danach zu filtern. Nach der Aktualisierung der Statistiken hat MySQL eine bessere Chance, idx_orders_tenant_date zu wählen und direkt zum Tenant und Datumsbereich zu springen, der benötigt wird.

ANALYZE TABLE zwingt MySQL nicht, einen bestimmten Index zu wählen. Es gibt dem Optimizer bessere Schätzungen, sodass seine Kostenberechnung weniger wahrscheinlich auf alten Daten basiert.

Aktualisiere Tabellenstatistiken mit:

ANALYZE TABLE orders;

Für schiefe Spalten können MySQL-8-Histogramme manchmal dem Optimizer helfen, die Selektivität besser zu schätzen:

ANALYZE TABLE orders
UPDATE HISTOGRAM ON status, tenant_id;

Verwende Histogramme selektiv. Sie können für Spalten helfen, die oft gefiltert werden, aber keine guten Index-Kandidaten sind, besonders wenn Werte ungleichmäßig verteilt sind.


Eine MySQL-Tuning-Checkliste

  • Habe ich die Abfrage mit EXPLAIN oder EXPLAIN ANALYZE untersucht?
  • Scannt MySQL zu viele Zeilen (type = ALL, hohes rows, niedriges filtered)?
  • Kann ich weniger Spalten zurückgeben?
  • Kann ich vor dem Joinen oder Aggregieren filtern?
  • Sind Prädikate sargable, ohne Funktionen auf indizierten Spalten?
  • Stimmen Literaltypen mit Spaltentypen überein?
  • Passt ein zusammengesetzter Index zu den Gleichheitsfiltern, Bereichsfiltern und der Sortierung?
  • Wird Using temporary oder Using filesort durch eine große Eingabe verursacht?
  • Wird LIMIT durch einen Index gestützt, der Zeilen in der gewünschten Reihenfolge zurückgibt?
  • Zähle ich Zeilen, wenn ich nur wissen muss, ob eine existiert?
  • Habe ich mit realistischen Parametern und Datenvolumen erneut getestet?

Abschließende Gedanken

Gute MySQL-Optimierung geht hauptsächlich darum, Arbeit zu reduzieren. Beginne mit dem Ausführungsplan, mache die Abfrage leicht nachvollziehbar und füge dann Indizes hinzu, die zu echten Zugriffsmustern passen. Wenn eine Abfrage immer noch Millionen von Zeilen für jede Anfrage aggregieren oder sortieren muss, ist die bessere Lösung möglicherweise eine Zusammenfassungstabelle, ein gecachtes Ergebnis oder eine andere Produktinteraktion.

Schnelle Abfragen sind nicht die mit den meisten Indizes. Es sind die, bei denen MySQL einen kurzen, vorhersehbaren Pfad zu den Zeilen hat, die die Anwendung tatsächlich benötigt.