Eine Shop-Datenbank ohne Indizes: Jeder Filter und jeder Join wurde zum Full Scan, bis die richtigen Indizes Mikrosekunden brachten

Eine Shop-Datenbank ohne Indizes funktionierte, solange die Tabellen klein waren. Bei 500 000 Bestellungen und 1,75 Millionen Positionen lief jeder Filter und jeder Join auf einen Full Scan hinaus. Wir haben genau gemessen, was die fehlenden Indizes kosten, die richtigen angelegt und die Fallen dokumentiert, durch die ein Index nutzlos bleibt, obwohl er existiert.

  • Ein Join über drei Tabellen: 406 ms, mit Index 0,075 ms
  • Bestellungen filtern: 500 000 gelesene Zeilen, mit Index 5
  • Index vorhanden und trotzdem übergangen: wir zeigen, wann und warum
Eine Tabelle aus zwölf Zeilen; links läuft ein langer Pfeil über alle Zeilen unter der Beschriftung ALL mit 500 000 darunter, rechts schickt eine Indexkarte einen orangen Pfeil zu einer einzigen Zeile unter der Beschriftung ref mit 5 darunter

Die Datenbank eines Onlineshops auf MariaDB: Kunden, Produkte, Bestellungen, Positionen, Berichte und Kundenhistorie

Ausgangslage

Die Shop-Datenbank war in Eile entworfen worden und hatte nichts als Primärschlüssel. Bei kleinen Tabellen war alles sofort da, also fielen die fehlenden Indizes nicht auf. Mit wachsenden Daten brauchten Abfragen, die früher Millisekunden dauerten, Hunderte von Millisekunden, und Listen und Berichte unter Last beschäftigten die Datenbank für lange Momente.

Die Aufgabe: durch Messung zeigen, was die fehlenden Indizes auf dem echten Datenbestand kosten, die richtigen Indizes für die typischen Abfragen eines Shops anlegen, einen Filter nach Kunde, die Bestellhistorie mit Join, einen Verkaufsbericht und die Seitenweise-Anzeige, und auf die Fallen hinweisen, durch die ein Index nutzlos bleibt, obwohl er angelegt wurde.

Lösung

Die Datenbank, wie wir sie vorfanden: vier Tabellen, Kunden, Produkte, Bestellungen und Positionen, nur mit Primärschlüsseln. Die Fremdschlüssel zwischen ihnen existierten nur im Anwendungscode, nicht als Constraints in der Datenbank, also hatte nie etwas einen Index auf ihnen angelegt. Für jede typische Abfrage des Shops haben wir den Plan, die Zahl der per Scan gelesenen Zeilen und die Medianzeit festgehalten, dann die richtigen Indizes einen nach dem anderen angelegt und dieselbe Abfrage erneut gemessen.

Die Fallen bekamen eigene Messungen: Fälle, in denen ein Index existiert und die Datenbank ihn nicht oder schlecht nutzt. Und weil ein Index kein Gewinn zum Nulltarif ist, haben wir gemessen, wie stark fünf Indizes das Schreiben verlangsamen und wie viel Platz sie brauchen. Die Daten des Shops zu diesem Zeitpunkt: 100 000 Kunden, 50 000 Produkte, 500 000 Bestellungen und 1,75 Millionen Positionen auf MariaDB 10.11 mit InnoDB, Stand 2026-10.

Ergebnisse

Kundenhistorie, ein Join über drei Tabellen
Von 406,8 ms auf 0,075 ms
Die Bestellungen eines Kunden
Von 500 000 gelesenen Zeilen auf 5
Suche nach E-Mail-Adresse
Von 5,9 ms auf 0,024 ms
Die zehn neuesten Bestellungen
Von 39,3 ms auf 0,009 ms
Verkaufsbericht nach Status und Zeitraum
Von 34 268 Zeilen auf 3 336 mit einem zusammengesetzten Index
Der Preis von fünf Indizes auf der Bestelltabelle
Einfügen 3,1-mal langsamer, Indizes bei 237 Prozent der Tabellengröße

So sind wir vorgegangen

  1. Die Ausgangslage, wie wir sie vorfanden

    Vier Tabellen nur mit Primärschlüsseln; die Fremdschlüssel zwischen ihnen lebten im Anwendungscode und hatten in der Datenbank keinen Index.

  2. Der Ausgangswert für jede Abfrage

    Der Plan aus EXPLAIN, der Zähler der per Scan gelesenen Zeilen, der nicht vom Cache abhängt, und die Medianzeit vieler Läufe.

  3. Die richtigen Indizes, dieselbe Messung

    Ein Index für jede Spalte in einem Filter, einem Join oder einer Sortierung, einer nach dem anderen auf der Datenbank des Shops angelegt, dann die identische Abfrage erneut.

  4. Die Fallen einzeln gemessen

    Ein Index, der existiert und übergangen wird, eine Funktion auf der Spalte, ein Typkonflikt, die Spaltenreihenfolge eines zusammengesetzten Index, ein abdeckender Index.

  5. Die Kosten gezählt

    Platz auf der Platte und die Verlangsamung des Schreibens mit fünf Indizes auf der Bestelltabelle.

  6. Eine Checkliste für das Team

    Welche Abfragen zu prüfen sind, welche Spalten zu indexieren, welche Indizes zu löschen, weil kein Plan sie je wählt.

Herausforderungen, die wir gelöst haben

Dass ein Index eine Abfrage beschleunigt, ist nicht der interessante Teil. Interessant ist, wann ein Index existiert und nicht hilft, und wie man den Plan liest, um das zu sehen.

„Index benutzt“ ist nicht dasselbe wie schnell

Der Plan zeigt einen Schlüsselnamen auch dann, wenn die Datenbank den ganzen Index liest. Die Spalte, die man lesen muss, ist der Zugriffstyp: ref, eq_ref und range bedeuten ein Nachschlagen über den Index, index einen vollständigen Scan des Index, ALL einen vollständigen Scan der Tabelle. Ein Plan kann einen Schlüssel nennen und Using index melden und darunter trotzdem eine Million Einträge lesen. Der Schlüsselname ist nicht der Beleg; der Zugriffstyp und die Zeilenzahl sind es.

Der Index existiert, und die Datenbank ignoriert ihn

Auf der Statusspalte, fünf verschiedene Werte, haben wir einen Index angelegt, und eine Summe der Bestellbeträge bezahlter Bestellungen lief trotzdem als vollständiger Scan der Tabelle: 500 000 Zeilen, 35,7 ms. Das ist kein Fehler des Optimierers, sondern eine Entscheidung: Bezahlte Bestellungen sind 20 Prozent der Tabelle, und der Betrag steht nicht im Index, also hieße der Index 100 000 zufällige Sprünge zu Zeilen, was mehr kostet als ein sequenzieller Scan. Ein Index auf einer Spalte mit wenigen verschiedenen Werten ändert oft nichts.

Eine Funktion auf der Spalte schaltet den Index ab

Mit einem Index auf dem Bestelldatum verhalten sich zwei Abfragen nach demselben Tag völlig verschieden. Die Bedingung mit dem Datum in einer Funktion liest den ganzen Index, 500 000 Einträge, 32,7 ms. Die Bedingung als Bereich von Mitternacht bis Mitternacht liest 556 Zeilen in 0,073 ms. Der Unterschied von etwa 447-mal kommt allein von der Funktion um die Spalte:

WHERE DATE(created_at) = '2026-06-15'                              vollständiger Index-Scan
WHERE created_at >= '2026-06-15' AND created_at < '2026-06-16'      Bereichssuche

Eine indexierte Spalte in irgendetwas zu hüllen, eine Funktion, eine Verkettung, eine Umwandlung, nimmt die Bereichssuche weg. Die Regel schreibt man auf der Seite des Werts, nicht der Spalte.

Eine implizite Typumwandlung: ein falsches Ergebnis, und langsamer

Die gefährlichste Falle, weil sie die Korrektheit trifft, nicht nur die Geschwindigkeit. Die Statusspalte ist Text. Mit dem Textwert verglichen liefert die Abfrage 100 000 Zeilen in 8,5 ms über den Index. Mit der Zahl 0 statt eines Textes verglichen liefert sie alle 500 000 Zeilen in 84,6 ms mit einem vollständigen Scan des Index. MariaDB wandelt für den Vergleich jeden Text in eine Zahl, jeder Status wird 0, und die Bedingung passt auf jede Zeile. Die Abfrage ist langsamer, liefert das falsche Ergebnis und gibt nur eine Warnung aus, die niemand liest. Die Typen in einer Bedingung müssen zusammenpassen.

Ein zusammengesetzter Index wirkt nur von links

Ein zusammengesetzter Index auf Produkt und Menge in den Positionen bedient eine Bedingung auf das Produkt in 0,020 ms. Eine Bedingung auf die Menge allein kann ihn nicht zum Nachschlagen nutzen und scannt den ganzen Index, 1,75 Millionen Einträge, 96,1 ms. Ein mehrspaltiger Index bedient Bedingungen nur von seinem linken Rand aus, also ist die Spaltenreihenfolge eine Entwurfsentscheidung, gewählt für die echten Abfragen.

Ein abdeckender Index beseitigt die Sprünge zu Zeilen

Eine Liste der tausend neuesten Bestellungen mit Nummer und Betrag: Mit einem Index auf dem Datum allein liest die Datenbank den Index in Reihenfolge, muss aber für den Betrag zu jeder Zeile springen, 0,75 ms. Mit einem Index auf Datum und Betrag steht alles, was die Abfrage braucht, im Index, keine Sprünge, 0,24 ms. Bei größeren Ergebnissen und kaltem Cache ist der Unterschied weit größer als dieses Dreifache.

Zwei Bahnen: Die obere Bedingung DATE(created_at) = … endet an einem Kreuz vor einer Indexkarte; die untere Bedingung created_at >= … AND < … erreicht ihre Indexkarte auf einer orangen Bahn

Gemessen ohne und mit Index

Abfrage Ohne Index Mit Index Beschleunigung
Suche nach E-Mail-Adresse Full Scan, 100 000 Zeilen, 5,9 ms Nachschlagen, 1 Zeile, 0,024 ms Etwa 245-mal
Die Bestellungen eines Kunden Full Scan, 500 000 Zeilen, 24,1 ms Nachschlagen, 5 Zeilen, 0,020 ms Etwa 1 200-mal
Kundenhistorie, Join über 3 Tabellen Scan der Positionen, 1,75 Millionen Zeilen, 406,8 ms Nachschlagen, 0,075 ms Etwa 5 400-mal
Die zehn neuesten Bestellungen Full Scan und Sortierung über Datei, 39,3 ms Aus dem Index gelesen, 10 Zeilen, 0,009 ms Etwa 4 360-mal
Verkaufsbericht, Status und Zeitraum Index auf dem Datum, 34 268 Zeilen, 9,7 ms Zusammengesetzter Index, 3 336 Zeilen, 2,2 ms Etwa 4,4-mal
Kosten: 200 000 Bestellungen einfügen 0,53 s ohne Indizes 1,64 s mit fünf Indizes, 60,6 MB Indizes für 25,6 MB Daten 3,1-mal langsamer

Was wir daraus mitnehmen

  • Die Spalten von Bedingungen und Joins indexieren. Spalten in einem Filter, in einer Join-Bedingung und in einer Sortierung sind die ersten Kandidaten. Ein logischer Fremdschlüssel ohne Index ist die häufigste Ursache langsamer Joins.
  • Den Plan lesen, vor allem den Zugriffstyp. ALL und index sind Scans; das Ziel ist ref, eq_ref oder ein enger range. Der Schlüsselname allein beweist nichts.
  • Indexierte Spalten nicht in Funktionen hüllen. Die Operation auf die Seite des Werts verschieben oder eine berechnete Spalte mit eigenem Index erwägen.
  • Auf die Typen in Bedingungen achten. Ein Vergleich von Text mit Zahl kann ein falsches Ergebnis liefern und den Index abschalten.
  • Die Spaltenreihenfolge eines zusammengesetzten Index nach den Abfragen wählen. Nur das linke Präfix wirkt.
  • Abdeckende Indizes für heiße Abfragen erwägen, und sie gegen die Kosten von Schreibzugriffen und Platz abwägen.
  • Mit Maß indexieren. Jeder Index verlangsamt das Schreiben und braucht Platz: Fünf Indizes auf der Bestelltabelle brauchten 237 Prozent ihrer Größe und machten das Einfügen 3,1-mal langsamer. Die anlegen, die genutzt werden, die löschen, die kein Plan je wählt, und auf den echten Daten erneut messen. Dieselbe Disziplin gilt auf der Anwendungsseite, wo ein großes Abfrageergebnis im Speicher einen Server zu Fall bringen kann.

Quellen

Auf einer Grundlinie ein schwarzer Block mit der Beschriftung 26 MB für die Daten der Bestelltabelle und daneben ein höherer oranger Stapel aus fünf Blöcken mit der Beschriftung 61 MB für ihre Indizes

Verwandte Ausgangslagen

  • Ihr System läuft, aber niemand kennt es genau. Wir erfassen Anwendung, Datenbank, Server und E-Mail, bewerten Risiken, empfehlen schriftlich den weiteren Weg und übernehmen auf Wunsch den Betrieb.

  • Ihr Entwickler hat gekündigt oder ist bereits ausgeschieden, und das System muss weiterlaufen. Wir prüfen das System und legen Ihnen 48 Stunden nach Zugang einen schriftlichen Befund vor.

Weitere Projekte

  • Eine Punkteskala mit gestrichelter Schwelle: Der Phishing-Umschlag wandert in einem orangen Bogen über die Schwelle

    Ein Spamfilter, der lief, aber nicht schützte

    Dienste liefen, die Konfiguration war gültig, im Systemstatus stand kein Fehler. Trotzdem landete Phishing im Posteingang, während Newsletter von Partnern im Spam lagen. Unsere Ingenieure haben gemessen, welche Erkennungsmechanismen tatsächlich ansprangen, die Ursachen behoben und drei Monate später eine Regression gefunden und behoben, die sich nur stundenweise zeigte.

  • Vier Briefumschläge passieren die Tore SPF, DKIM und DMARC, jeweils orange abgehakt, auf dem Weg in den Posteingang

    E-Mail-Zustellbarkeit für vier Firmendomains

    Ein Mailserver für vier Firmendomains signierte nichts, veröffentlichte keine DMARC-Richtlinie und wurde von Microsoft abgewiesen. Unsere Ingenieure haben ihn geprüft, die Änderung an einer Domain erprobt und in Gruppen ausgerollt. Das Ergebnis: Alle drei Prüfungen bestehen bei Gmail, die Microsoft-Sperre ist aufgehoben, und die Nutzer haben nichts bemerkt.

In 20 Minuten klären wir, welcher erste Schritt zu Ihrem System passt

Sie schildern Ziel und Stand, wir sagen offen, ob und wie wir helfen können. Antwort innerhalb eines Werktags, unverbindlich.

Telefon (Deutsch, Polnisch, Englisch) +48 501 250 220
E-Mail E-Mail

Erreichbar: Montag bis Freitag, 9:00 bis 17:00 Uhr (MEZ); Support und Notfälle rund um die Uhr (24/7)