Datenbank-Performance

Langsame Datenbank-Queries in WordPress:
So machen Sie sie schnell

Ihre Site lädt in 4 Sekunden. Sie haben alles gecacht. Die Bilder sind optimiert. Die Datenbank ist trotzdem der Engpass.

Warum langsames SQL der versteckte Engpass von WordPress ist

WordPress führt Dutzende SQL-Queries pro Seitenaufruf aus. Die meisten sind in unter 1 ms fertig. Aber eine einzige langsame Query—ein 800-ms-JOIN auf wp_postmeta—reicht, damit sich Ihre ganze Seite kaputt anfühlt.

Das Muster

PageSpeed sagt 95. Nutzer sagen „ist langsam". Das Frontend ist schnell. Der Server braucht über 2 Sekunden, bevor er überhaupt anfängt, HTML zu senden.

Das Schlimmste daran: WordPress protokolliert langsame Queries standardmäßig nicht. Es gibt keine Warnung, keinen Hinweis im Dashboard. Queries werden still schlechter, während Ihre Datenbank wächst—und Sie merken es erst, wenn Nutzer sich beschweren.

Was langsame Datenbank-Queries in WordPress verursacht

1. Postmeta-JOINs von WooCommerce

WooCommerce legt Produktdaten in wp_postmeta als Schlüssel-Wert-Paare ab. Produkte nach Preis, Lagerbestand oder Attributen zu filtern erfordert mehrere JOINs auf eine Tabelle ohne passenden Index:

SELECT p.ID FROM wp_posts p
INNER JOIN wp_postmeta pm1 ON p.ID = pm1.post_id
INNER JOIN wp_postmeta pm2 ON p.ID = pm2.post_id
WHERE pm1.meta_key = '_price' AND pm1.meta_value BETWEEN 10 AND 50
AND pm2.meta_key = '_stock_status' AND pm2.meta_value = 'instock'

Bei 10.000 Produkten und 500.000 postmeta-Zeilen kann diese Query ohne passende Indizes 2–5 Sekunden dauern.

2. meta_key-Abfragen ohne Index

Die Standardtabelle wp_postmeta hat nur einen Index auf post_id. Jede Query, die nach meta_key + meta_value filtert, macht einen vollständigen Table Scan:

SELECT post_id FROM wp_postmeta
WHERE meta_key = '_thumbnail_id'
-- Full table scan on 500K+ rows

3. LIKE-Queries auf großen Textspalten

Such-Plugins und die Suche im Backend führen oft LIKE '%term%' auf post_content aus. Das umgeht jeden Index und scannt die ganze Spalte:

SELECT ID FROM wp_posts
WHERE post_content LIKE '%shipping policy%'
-- Scans every row, every time

4. COUNT-Queries mit komplexem WHERE

Listentabellen im Backend führen COUNT-Queries für die Seitennummerierung aus. Zusammen mit Taxonomie-Filtern und Meta-Queries kann das auf großen Sites überraschend teuer werden.

Langsame Queries in WordPress finden

  1. SAVEQUERIES aktivieren — Tragen Sie define('SAVEQUERIES', true); in die wp-config.php ein. WordPress protokolliert dann jede Query mit Laufzeit und aufrufender Funktion. Sehen Sie sich nach dem Seitenaufruf $wpdb->queries an.
  2. Slow-Query-Log von MySQL prüfen — Mit Serverzugriff: SET GLOBAL slow_query_log = 'ON'; und SET GLOBAL long_query_time = 0.5;, um Queries über 500 ms einzufangen.
  3. EXPLAIN auf verdächtige Queries anwenden — Stellen Sie jeder langsamen Query ein EXPLAIN voran, um den Ausführungsplan zu sehen. Achten Sie auf type: ALL (vollständiger Table Scan) und rows:-Werte im Hunderttausenderbereich.
  4. Eine bestimmte Seite profilen — Nutzen Sie Query Monitor oder aktivieren Sie SAVEQUERIES vorübergehend. Sortieren Sie nach Laufzeit. Die drei größten Queries machen meist 80 % Ihrer TTFB aus.
  5. Zur Spitzenlast messen — Langsame Queries, die bei 10 gleichzeitigen Nutzern noch „in Ordnung" sind, werden bei 100 zur Katastrophe. Testen Sie unter Last, nicht nur in Ihrer Entwicklungsumgebung.

Einen EXPLAIN-Plan für eine echte WordPress-Query lesen

EXPLAIN ist das, was MySQL einem Debugger am nächsten kommt. Sie stellen es einem beliebigen SELECT voran, und MySQL sagt Ihnen, wie es die Query ausführen will – welche Tabellen es scannt, welche Indizes es nutzt, wie viele Zeilen es erwartet. Hier ist die WooCommerce-Filter-Query von oben, mit einem typischen ORDER BY ergänzt, so wie eine echte Shop-Seite sie ausführt:

EXPLAIN SELECT p.ID FROM wp_posts p
INNER JOIN wp_postmeta pm1 ON p.ID = pm1.post_id
INNER JOIN wp_postmeta pm2 ON p.ID = pm2.post_id
WHERE pm1.meta_key = '_price' AND pm1.meta_value BETWEEN 10 AND 50
  AND pm2.meta_key = '_stock_status' AND pm2.meta_value = 'instock'
ORDER BY p.post_date DESC LIMIT 20;

In einem Shop mit rund 800.000 postmeta-Zeilen sieht der Plan so aus:

idselect_typetabletypepossible_keyskeyrowsExtra
1SIMPLEpm1ALLpost_id, meta_keyNULL812441Using where; Using temporary; Using filesort
1SIMPLEpeq_refPRIMARYPRIMARY1Using where
1SIMPLEpm2refpost_id, meta_keypost_id4Using where

Die erste Zeile ist das ganze Problem. So lesen Sie sie:

  • type: ALL – ein vollständiger Table Scan. MySQL liest jede Zeile in wp_postmeta und prüft die WHERE-Bedingung gegen jede einzelne. Bei 812.000 Zeilen, bei jedem Seitenaufruf. Das ist der schlechteste Zugriffstyp, den EXPLAIN Ihnen zeigen kann.
  • key: NULL – es wurde kein Index benutzt. Beachten Sie: possible_keys listet meta_key auf, ein Index existiert also – MySQL hat ihn nur verworfen. Warum? Weil '_price' auf einen riesigen Teil der Tabelle passt (jedes Produkt hat einen Preis), sodass der Optimizer einen Scan für billiger hielt als einen Index, der fast nichts herausfiltert. Der Index fehlt nicht, er ist für diese Query nur nutzlos.
  • Using temporary – MySQL muss eine interne temporäre Tabelle für Zwischenergebnisse anlegen, bevor es sortieren kann. Bei großen Ergebnismengen wandert diese temporäre Tabelle vom Speicher auf die Festplatte, und aus Millisekunden werden Sekunden.
  • Using filesort – das ORDER BY lässt sich durch keinen Index bedienen, also sortiert MySQL die Ergebnismenge selbst. Der Name führt in die Irre – es landet nicht immer auf der Platte –, aber zusammen mit 812.000 gescannten Zeilen heißt das: bei jedem Request einen riesigen Datenberg sortieren.

Die Lösung ist ein Index, den der Optimizer auch wirklich will: einer, der auf meta_key UND meta_value zusammen filtert, sodass '_price' plus Wertebereich auf ein paar tausend Zeilen eingrenzt, statt auf die halbe Tabelle zu passen.

ALTER TABLE wp_postmeta
ADD INDEX wpmt_key_value (meta_key(191), meta_value(32));

Führen Sie EXPLAIN erneut aus, und die erste Zeile sieht völlig anders aus:

idselect_typetabletypepossible_keyskeyrowsExtra
1SIMPLEpm1rangepost_id, meta_key, wpmt_key_valuewpmt_key_value2874Using where
1SIMPLEpeq_refPRIMARYPRIMARY1Using where
1SIMPLEpm2refpost_id, meta_key, wpmt_key_valuewpmt_key_value2Using where

type: range heißt, MySQL geht nur die Index-Einträge zwischen den beiden Preisgrenzen durch – 2.874 Zeilen statt 812.441. Das ist 280-mal weniger Arbeit, noch bevor die Query überhaupt ans Sortieren kommt. Die Gleichheitsabfrage auf pm2 bekommt über denselben Index type: ref, mit 2 Zeilen pro Produkt. In Extra kann weiterhin ein filesort auftauchen, aber 20 Kandidatenzeilen zu sortieren ist Rauschen; 800.000 zu sortieren war der Ausfall.

Mehr ist es nicht: EXPLAIN ausführen, für jede Tabelle auf type, key und rows schauen und die Zeile beheben, die am meisten Arbeit macht. Der Slow Query Analyzer von WP Multitool führt genau diese Analyse für Sie aus – lokal, für jede langsame Query, die er einfängt – und nennt Ihnen den fehlenden Index.

Richtwerte für WordPress-Query-Performance

Nicht jede langsame Query ist gleich. Das sind die Zielwerte:

<50ms
Pro Query (gesund)
<30
Queries pro Seite
<200ms
Gesamte DB-Zeit

Braucht eine einzelne Query über 100 ms, lohnt sich ein genauer Blick. Übersteigt Ihre gesamte Datenbankzeit 500 ms, spüren Ihre Nutzer das—auch mit Page Caching.

WordPress-Datenbank-Queries beschleunigen (praktische Maßnahmen)

Die langsame Query zu finden ist die halbe Arbeit. Eine Regel vorweg: Optimieren Sie die Query, nicht das Symptom. Setzen Sie nicht blind Indizes, sondern verstehen Sie, warum die Query langsam ist. Manchmal ist die Lösung, die Daten umzubauen (eigene Tabellen statt postmeta). Manchmal ist es, die Query ganz zu vermeiden (Transient-Caching für teure Aggregationen). Hier steht, was Queries wirklich schneller macht, in der Reihenfolge, in der ich es versuchen würde.

1. Die Indizes anlegen, nach denen Ihr EXPLAIN-Plan verlangt

Der Index, auf den die EXPLAIN-Analyse oben hinausläuft, ist der, den ich zuerst anlege – ein zusammengesetzter Index auf wp_postmeta, der beide Spalten abdeckt, nach denen WordPress-Meta-Queries filtern:

ALTER TABLE wp_postmeta
ADD INDEX wpmt_key_value (meta_key(191), meta_value(32));

meta_key und meta_value gemeinsam abzudecken erlaubt MySQL, beide Bedingungen in einem Index-Durchlauf einzugrenzen. In der Analyse oben wurde daraus statt type: ALL ein type: range, und die geprüften Zeilen sanken von 812.441 auf 2.874. Die Präfixlängen (191) und (32) halten den Index kompakt und decken echte Abfragen trotzdem ab. Setzen Sie Indizes nicht auf Verdacht: erst EXPLAIN, dann genau den Index anlegen, der dem Plan fehlt.

Eine Randbemerkung: Im Netz wird oft ein Index nur auf meta_value empfohlen. Der hilft einer Query nicht, die auf meta_key und meta_value zusammen filtert – der zusammengesetzte Index oben deckt diesen Fall ab, fangen Sie also damit an.

2. Produkt- und Bestell-Queries von WooCommerce beschleunigen

Die weiter oben behandelten Postmeta-JOINs von WooCommerce sind der Klassiker: Jede meta_query-Bedingung fügt einen JOIN auf eine EAV-Tabelle mit 4 Spalten hinzu. Hören Sie auf, nach postmeta zu filtern, wenn Sie denormalisieren können. Für heiße Felder, nach denen Sie ständig filtern – Preis, Lagerbestand, Bewertung –, kopieren Sie den Wert in eine dafür gebaute Lookup-Tabelle mit richtigen Spalten und Indizes. WooCommerce liefert wc_product_meta_lookup genau dafür mit; nutzen Sie sie oder bauen Sie dasselbe Muster für Ihre eigenen Felder.

3. Schluss mit LIKE '%term%' und unbegrenzten WP_Query-Aufrufen

Ein führender Platzhalter kann nie einen B-Tree-Index nutzen – MySQL scannt immer jede Zeile. Legen Sie einen FULLTEXT-Index auf post_content an und fragen Sie mit MATCH() AGAINST() ab, oder holen Sie die Suche ganz aus der posts-Tabelle heraus, hin zu einem eigenen Such-Plugin oder einer externen Suchmaschine. Alles ist besser, als longtext zu scannen.

Fragen Sie nie mit posts_per_page => -1 ab. Unbegrenzte Queries laufen mit 200 Beiträgen prima und brechen bei 20.000 zusammen. Setzen Sie ein echtes Limit und paginieren Sie, oder arbeiten Sie in Cron-Jobs mit Offsets in Stapeln. Die langsame Query, die Sie lokal nicht nachstellen können, ist meist eine davon auf Produktionsdatenmengen.

Und vermeiden Sie OR-Wälder in meta_query. Verschachtelte OR-Beziehungen über mehrere Meta-Keys zwingen MySQL zu breiten Scans und temporären Tabellen. Nehmen Sie lieber eine gezielte Query, eine denormalisierte Flag-Spalte oder zwei billige Queries, die Sie in PHP zusammenführen, statt einer Monster-meta_query.

4. Aggregate cachen und N+1-Meta-Schleifen abstellen

Fangen Sie mit Object Caching an. Redis oder Memcached halten Query-Ergebnisse im Speicher – wiederholte Queries landen im Cache statt in MySQL. Für die meisten Sites ist das die wirkungsvollste einzelne Änderung.

Cachen Sie als Nächstes teure Aggregate selbst. COUNT() über komplexe WHEREs, „Beiträge diesen Monat"-Widgets, Term-Zählungen – die müssen nicht bei jedem Request frisch sein. Einmal berechnen, in einem Transient oder im Object Cache ablegen, nach Zeitplan oder bei save_post auffrischen. Ein 900-ms-COUNT, der einmal pro Stunde läuft, kostet nichts.

Kürzen Sie dann, was WP_Query überhaupt holt. Brauchen Sie nur IDs, sagen Sie es: 'fields' => 'ids' spart das Laden vollständiger Zeilenobjekte. 'no_found_rows' => true lässt den SQL_CALC_FOUND_ROWS-Durchlauf weg, wenn Sie nicht paginieren. 'update_post_meta_cache' => false und 'update_post_term_cache' => false sparen die Cache-Vorbereitungs-Queries, wenn Sie Meta oder Terms gar nicht anfassen – lassen Sie das Vorwärmen des Meta-Caches aber AN, wenn Sie in einer Schleife Meta lesen, denn genau diese eine Query erspart Ihnen ein get_post_meta() pro Beitrag.

Und schließlich: Kürzen Sie die Query, die WordPress vor allen anderen ausführt. Jeder Request beginnt damit, alle autoloaded Optionen zu laden, bevor Ihre erste Query überhaupt feuert. Ballast in autoloaded Optionen zu beheben beschleunigt jede Seite, nicht nur die langsamen.

5. Wie „schneller" aussieht (Messwerte nach jeder Maßnahme)

Wenden Sie eine Maßnahme nach der anderen an und messen Sie jedes Mal neu: EXPLAIN plus Zeitmessung. Die Ziele sind die Richtwerte oben – unter 50 ms pro Query, unter 200 ms gesamte Datenbankzeit pro Seite. In der EXPLAIN-Analyse senkte ein zusammengesetzter Index die gescannten Zeilen von 812.441 auf 2.874; das ist die Größenordnung, die ein richtiger Index bewirkt. Bewegt eine Maßnahme weder den EXPLAIN-Plan noch die Zeit, nehmen Sie sie zurück und gehen zur nächsten.

Slow-Query-Log vs. Query Monitor vs. automatische Erkennung

Es gibt drei Wege, eine langsame Query zu erwischen. Sie beantworten verschiedene Fragen:

Ansatz Serverzugriff nötig Läuft dauerhaft Erfasst Stack Trace Schlägt Indizes vor Produktionstauglich
Manuell (SAVEQUERIES / MySQL-Slow-Log) Ja – Rechte für my.cnf oder SET GLOBAL Das MySQL-Log kann das, aber niemand liest es, bevor etwas kaputtgeht Nein – Sie bekommen das SQL, nicht das PHP, das es ausgelöst hat Nein – Sie führen EXPLAIN aus und deuten es selbst SAVEQUERIES nein (Speicher-Overhead bei jedem Request); das MySQL-Log ja, mit vernünftigem Schwellenwert
Plugin Query Monitor Nein Nein – zeigt den aktuellen Seitenaufruf, und nur für eingeloggte Administratoren Ja – vollständiger Aufrufstapel, seine größte Stärke Nein Für die Entwicklung gedacht. In der Produktion installierbar, sieht aber nur die Requests, die Sie selbst auslösen, während Sie hinschauen – die Spitze um 3 Uhr nachts erwischt es nicht
WP Multitool Slow Query Analyzer Nein – funktioniert auch auf Shared Hosting Ja – protokolliert rund um die Uhr jede Query über Ihrem Schwellenwert, im echten Traffic Ja – die Plugin- oder Theme-Datei, die die Query ausgelöst hat Ja – führt EXPLAIN lokal aus und zeigt auf den fehlenden Index Ja – dafür gebaut, auf Live-Sites mit minimalem Overhead zu laufen

Query Monitor ist richtig gut in dem, was es tut – ich benutze es selbst. Es beantwortet nur eine andere Frage. Es sagt Ihnen, was dieser Seitenaufruf getan hat, während Sie zusehen; es kann Ihnen nicht sagen, was Ihre Site letzten Dienstag unter Last getan hat. Genau diese Lücke füllt eine dauerhafte Erkennung.

Warum langsame Queries immer wiederkommen

Langsame Queries einmal zu finden reicht nicht. Sie kommen zurück:

  1. Plugin-Updates bringen neue Queries oder ändern bestehende
  2. Ihre Datenbank wächst—eine Query, die bei 10.000 Zeilen schnell ist, ist es bei 500.000 nicht mehr
  3. Neue Inhaltstypen bringen zusätzliche postmeta- und Taxonomie-Zeilen
  4. WooCommerce-Verkäufe häufen Bestelldaten in denselben Tabellen an

Manuelle SAVEQUERIES-Kontrollen sind mühsam und schnell vergessen. Wenn langsame Queries außerdem Ihr WordPress-Backend langsam machen, verschärft sich das Problem. Sie brauchen laufende Überwachung, die Regressionen in dem Moment erwischt, in dem sie entstehen—nicht erst, wenn Nutzer sich beschweren.

Langsame WordPress-Queries: FAQ

Wie finde ich langsame Queries in WordPress?
Drei Wege: das MySQL-Slow-Query-Log mit long_query_time um 0,5 s aktivieren (braucht Serverzugriff), Query Monitor installieren und Seitenaufrufe im eingeloggten Zustand untersuchen, oder ein Monitoring-Plugin einsetzen, das langsame Queries dauerhaft im echten Traffic protokolliert. Die ersten beiden erwischen Queries, die Sie selbst auslösen; nur dauerhafte Überwachung erwischt die langsamen Queries, die Ihre Besucher treffen, wenn Sie nicht hinschauen.
Wie beschleunige ich WordPress-Datenbank-Queries?
Sie brauchen keine Neuentwicklung – die meisten Maßnahmen sind gezielte Änderungen. Führen Sie zuerst EXPLAIN auf der langsamen Query aus: Es sagt Ihnen, ob MySQL die ganze Tabelle scannt (type: ALL, key: NULL). Die meisten langsamen WordPress-Queries lassen sich mit dem richtigen Index beheben, meist einem zusammengesetzten auf wp_postmeta über meta_key und meta_value. Über Indizes hinaus: einen Object Cache (Redis oder Memcached) vor MySQL setzen, unbegrenzte Queries wie posts_per_page => -1 abstellen, LIKE '%term%'-Suchen durch FULLTEXT ersetzen, teure Zählungen cachen und Ballast in autoloaded Optionen beheben, was jede Query schon vor ihrer Ausführung verzögert.
Was ist eine gute Query-Zeit in WordPress?
Einzelne Queries sollten unter 50 ms bleiben – die meisten gut indizierten laufen in unter 5 ms. Eine typische Seite sollte weniger als 30 Queries brauchen und insgesamt unter 200 ms in der Datenbank verbringen. Braucht eine einzelne Query über 500 ms, ist sie ein EXPLAIN wert; über 1 s schadet sie bei jedem ungecachten Aufruf aktiv Ihrer TTFB.
Behebt Page Caching langsame Datenbank-Queries?
Nein – es versteckt sie. Gecachte Besucher bekommen schnelles HTML, aber jeder Cache-Miss, jeder eingeloggte Nutzer, jede Warenkorbseite und jeder Admin-Request zahlt weiterhin die vollen Query-Kosten. Die langsame Query ist noch da, verbrennt weiter CPU und taucht wieder auf, sobald der Traffic ansteigt oder der Cache geleert wird. Caching lohnt sich, aber es ist eine Schicht über einer reparierten Datenbank, kein Ersatz dafür.

Schluss mit Raten. Fangen Sie an zu protokollieren.

Der Slow Query Analyzer von WP Multitool protokolliert jede Query über Ihrem Schwellenwert, erfasst den vollständigen Stack Trace und schlägt konkrete Indizes vor. Kein Serverzugriff nötig.

WP Multitool kaufen Guide zur Backend-Performance