LW IT Solutions
« Blog Overview /WordPress-Plugins & Tricks / meta_query in WP_Query: Wie jede Klausel zu...
This post in other languages:

meta_query in WP_Query: Wie jede Klausel zu einem Join wird und was das kostet

meta_query in WP_Query: Wie jede Klausel zu einem Join wird und was das kostet
Inhalt
  1. Was WP_Query aus dem Feld baut
  2. Der Index, den es nicht gibt
  3. Die Klauseln, die mehr kosten als die anderen
  4. Wohin der Filter stattdessen gehört
  5. Messen statt schätzen
  6. Quellen

Eine Seite, die Beiträge nach drei eigenen Feldern filtert, ist in PHP ein Feld und in SQL vier Verbindungen. Das Feld liest sich wie ein Filter; das SQL liest sich wie eine Tabelle, die für jede Bedingung erneut besucht wird – denn genau das ist es.

Das Bemerkenswerte ist nicht, dass es langsam ist; die meisten Seiten werden nie groß genug, dass es auffiele. Bemerkenswert ist, dass die Kosten in dem Code unsichtbar sind, der sie verursacht, und dass sie mit zwei Dingen zugleich wachsen: mit der Zahl der Klauseln und mit der Größe einer Tabelle, die sich niemand ansieht.

Eine Treppe aus Abfragestufen: die Grundabfrage liest 4800 Zeilen, jede der drei meta_query-Verbindungen kommt mit 192000 Zeilen hinzu, derselbe Filter als Taxonomie liest 5120
Dieselbe Ergebnismenge, vier Wege dorthin. Der Balken der Grundabfrage fehlt nicht, er ist drei Pixel breit.

Was WP_Query aus dem Feld baut

Jede Klausel in meta_query bekommt einen eigenen Aliasnamen auf wp_postmeta und eine eigene Verbindungsbedingung. Zwei Klauseln, zwei Aliasnamen; fünf Klauseln, fünf. Die Tabelle wird so oft mit sich selbst verbunden, wie es Bedingungen gibt, und jede dieser Verbindungen trägt das volle Gewicht der Tabelle hinter sich her.

SELECT wp_posts.ID FROM wp_posts
  INNER JOIN wp_postmeta pm1 ON wp_posts.ID = pm1.post_id
  INNER JOIN wp_postmeta pm2 ON wp_posts.ID = pm2.post_id
  INNER JOIN wp_postmeta pm3 ON wp_posts.ID = pm3.post_id
WHERE post_type = 'post' AND post_status = 'publish'
  AND ( pm1.meta_key = '_region' AND pm1.meta_value = 'nord' )
  AND ( pm2.meta_key = '_stufe'  AND pm2.meta_value = 'b' )
  AND ( pm3.meta_key = '_aktiv'  AND pm3.meta_value = '1' )
ORDER BY wp_posts.post_date DESC

Daran ist nichts falsch. Es ist die wörtliche Übersetzung des Gewünschten, und die Datenbank beantwortet sie richtig – nachdem sie auf einer mittelgroßen Seite in der Größenordnung einer halben Million Zeilen gelesen hat, um vierzig zurückzugeben.

Der Index, den es nicht gibt

wp_postmeta trägt drei Indizes: den Primärschlüssel auf meta_id, einen auf post_id und einen auf die ersten 191 Zeichen von meta_key. Auf meta_value gibt es keinen, und es kann sinnvoll auch keinen geben: Die Spalte ist LONGTEXT, und ein Index auf eine Textspalte unbegrenzter Länge ist entweder ein Präfix oder gar nichts.

Jede Klausel wird deshalb in zwei Bewegungen beantwortet. Die Verbindung findet die Zeilen zu einem Beitrag über post_id – schnell, indiziert, genau wofür ein Index da ist. Danach wird der Wert innerhalb jeder dieser Zeilen verglichen, eine nach der anderen, ohne Hilfe von irgendwoher. Der Aufwand ist der Zahl der Metazeilen je Beitrag proportional, und die liegt auf einer Seite mit Seitenbaukasten und ein paar Erweiterungen selten unter dreißig.

Die Klauseln, die mehr kosten als die anderen

Klausel Was die Datenbank dafür tun muss
'compare' => '=' der Grundfall: eine Verbindung, ein Zeichenkettenvergleich je Zeile
'compare' => 'LIKE' dasselbe mit Mustervergleich; ein führender Platzhalter schließt jede Abkürzung aus
'type' => 'NUMERIC' legt CAST() um die Spalte und nimmt damit die letzte Aussicht auf Indexnutzung
'compare' => 'NOT EXISTS' eine LEFT JOIN samt IS-NULL-Prüfung – jede nicht passende Zeile muss erst entstehen
'relation' => 'OR' die Klauseln engen einander nicht mehr ein; die Zwischenmenge ist die Vereinigung
'orderby' => 'meta_value' eine temporäre Tabelle und eine Sortierung über eine Textspalte

Die letzte Zeile macht aus einer langsamen Seite eine Zeitüberschreitung. Nach einem Metawert zu sortieren, lässt sich aus keinem Index bedienen; die Datenbank baut also das gesamte Zwischenergebnis auf und sortiert es – und weil meta_value ein LONGTEXT ist, geschieht diese Sortierung auf der Platte statt im Arbeitsspeicher, sobald die Menge groß wird.

NOT EXISTS verdient eine eigene Warnung. Es liest sich wie das billige Gegenteil von EXISTS und ist das Gegenteil von billig: Abwesenheit nachzuweisen heißt, jeden Kandidaten zu besuchen – und das ist genau das, was ein Index nicht abkürzen kann.

Wohin der Filter stattdessen gehört

Ein Wert, nach dem gefiltert wird, ist keine Eigenschaft eines Beitrags in dem Sinn, in dem ein Untertitel eine ist. Er ist eine Einordnung, und für Einordnungen hat WordPress bereits eine Struktur mit den richtigen Indizes: wp_term_relationships ist auf beiden Spalten indiziert, ein Begriffsfilter ist also eine Verbindung, die vollständig aus Indizes beantwortet wird, ganz ohne Textvergleich.

Die Regel daraus ist kurz. Was gefiltert oder sortiert wird, gehört in eine Taxonomie – oder, wenn die Werte wirklich zahlenmäßig und stetig sind, in eine Spalte einer eigenen Tabelle. Was erst gelesen wird, nachdem der Beitrag gefunden ist, kann in den Metadaten bleiben, wo es genau einen Zugriff über post_id kostet und sonst nichts.

Messen statt schätzen

Die von WP_Query gebaute Abfrage steht vor der Ausführung bereit: In $query->request liegt das fertige SQL, und durch EXPLAIN geschickt beantwortet es die einzigen zwei Fragen, auf die es ankommt. Die Spalte rows ist die Schätzung, wie viele Zeilen jeder Schritt lesen wird; die Spalte Extra benennt die teuren Formen unverblümt, und Using temporary; Using filesort zusammen sind die Unterschrift einer Sortierung, die nicht mitwächst.

wp db query "EXPLAIN SELECT wp_posts.ID FROM wp_posts INNER JOIN ..."

id  select_type  table     type    key       rows    Extra
1   SIMPLE       wp_posts  ref     type_st.  4800    Using temporary; Using filesort
1   SIMPLE       pm1       ref     post_id     40    Using where
1   SIMPLE       pm2       ref     post_id     40    Using where
1   SIMPLE       pm3       ref     post_id     40    Using where

Drei Zeilen mit Using where hinter einem Zugriff über post_id sind genau das oben beschriebene Bild: Der Index hat die Zeilen gefunden, die Bedingung wurde danach angewandt. Die Zahl in rows, über die Verbindungen multipliziert, ist die Größe, die mit der Seite wächst – und es lohnt sich, sie an dem Tag zu kennen, an dem die Abfrage geschrieben wird, statt an dem, an dem die Seite nicht mehr lädt.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in Touch

Briefly describe your project or inquiry for a tailored response. This site is protected by reCAPTCHA.

Kommentar schreiben

Die E-Mail-Adresse wird nicht veröffentlicht. Pflichtfelder sind mit einem Stern versehen.

ALL ARTICLES & CATEGORIES

CCTV

Diese Rubrik per RSS verfolgen

Cloud & AI

Diese Rubrik per RSS verfolgen

Data Privacy

Alle 12 Artikel dieser Rubrik Diese Rubrik per RSS verfolgen

Digital Analytics

Alle 46 Artikel dieser Rubrik Diese Rubrik per RSS verfolgen

Digital Marketing

Alle 25 Artikel dieser Rubrik Diese Rubrik per RSS verfolgen

IT & Networks

Alle 15 Artikel dieser Rubrik Diese Rubrik per RSS verfolgen

Raspberry Pi

Diese Rubrik per RSS verfolgen

Smart Home

Alle 13 Artikel dieser Rubrik Diese Rubrik per RSS verfolgen

Web Entwicklung

Diese Rubrik per RSS verfolgen

WordPress-Plugins & Tricks

Diese Rubrik per RSS verfolgen