WordPress speichert benutzerdefinierte Felder in einer gemeinsamen Tabelle, wp_postmeta. Jeder Wert ist eine Zeile mit drei relevanten Spalten: der Beitrags-ID, einem Schlüssel und einem Wert. Dieses Design ist flexibel, und es ist der Grund, warum sich Ihr Katalog mit wachsender Größe immer schwerer abfragen lässt.
Das ist kein Fehler von WordPress. wp_postmeta ist bewusst generisch gehalten, damit jedes Plugin jeden Wert an jeden Beitrag hängen kann, ohne das Datenbankschema zu ändern. Der Preis dieser Allgemeinheit zeigt sich erst bei echtem Volumen: einem Produktkatalog, einem Verzeichnis von Einträgen oder allem mit mehr als ein paar Beiträgen und mehr als ein paar Feldern pro Beitrag.
Ein Produkt, viele Zeilen
Ein Produkt mit zehn Feldern wird zu zehn Zeilen. Zwanzigtausend Produkte werden zu 200.000 Zeilen, und alle teilen sich die Tabelle mit allen anderen Plugins, die Beitrags-Metadaten speichern – Page Builder, SEO-Plugins, Caching-Plugins, Ihr Theme. wp_postmeta ist regelmäßig die größte Tabelle einer WordPress-Datenbank und wächst auch dann weiter, wenn Sie keine Produkte mehr hinzufügen, weil Plugins ihre eigenen Verwaltungszeilen weiterhin an derselben Stelle ablegen.
Dieses Muster heißt EAV, kurz für Entity-Attribute-Value. Statt einer Tabelle mit einer Spalte pro Attribut erhalten Sie eine Zeile pro Attributwert, wobei der Attributname in jeder Zeile als Text wiederholt wird. Es ist ein bewährter Weg, Daten zu modellieren, deren Form man vorab nicht kennt, und genau deshalb nutzt WordPress ihn für Beitrags-Metadaten: Jedes Plugin kann neue Felder ohne Datenbankmigration definieren.
Der Nachteil: Die Datenbank kann ein „Feld“ nicht mehr als Spalte behandeln. Sie muss es als Wert behandeln, der zwischen Millionen unzusammenhängender Zeilen vergraben ist und nur über den Abgleich eines Textschlüssels gefunden wird.
Jeder Wert ist Text
meta_value ist eine LONGTEXT-Spalte. Ein Preis von 1299.00 ist der String "1299.00". Ein Lagerbestand von 50 ist der String "50". Das ist unproblematisch, solange ein einzelner Wert genau so zurückgelesen wird, wie er geschrieben wurde – und dafür werden Beitrags-Metadaten meistens genutzt. Sobald Sie sortieren oder vergleichen müssen, genügt es nicht mehr.
Um nach Preis zu sortieren, muss die Datenbank jeden passenden Wert von Text in eine Zahl umwandeln, bevor sie vergleichen kann, und sie kann dafür keinen Index nutzen, weil der Index auf dem Text aufgebaut wurde und nicht auf der Zahl, die eine Umwandlung ergibt. Dasselbe Problem betrifft als Strings gespeicherte Datumswerte und jeden numerischen Filter wie „Preis unter 100“.
Das Filtern nach mehreren Feldern verschärft das Problem, weil jedes Feld in einer eigenen Zeile liegt. Um Produkte mit einem Preis unter 100 und der Stadt Istanbul zu finden, muss die Abfrage wp_postmeta pro Feld mit sich selbst verknüpfen und jeden Join über post_id und den passenden meta_key abgleichen:
SELECT p.ID, pm_price.meta_value AS price
FROM wp_posts p
JOIN wp_postmeta pm_price
ON pm_price.post_id = p.ID AND pm_price.meta_key = '_price'
JOIN wp_postmeta pm_city
ON pm_city.post_id = p.ID AND pm_city.meta_key = 'city'
WHERE pm_city.meta_value = 'Istanbul'
ORDER BY CAST(pm_price.meta_value AS DECIMAL(10,2));Jedes weitere Feld, nach dem Sie filtern oder sortieren, bedeutet einen weiteren Self-Join auf dieselbe überfüllte Tabelle und eine weitere CAST-Umwandlung, die die Datenbank für jede Kandidatenzeile berechnen muss, bevor sie die Ergebnisse ordnen kann.
Was eine typisierte Tabelle ändert
Mit einer Tabelle pro Feldgruppe ist ein Produkt eine Zeile statt zehn. Der Preis ist eine DECIMAL-Spalte, der Lagerbestand eine INT-Spalte, ein Erscheinungsdatum eine DATE-Spalte. Jedes Feld hat den Typ, den es von Anfang an hätte haben sollen, und dieselbe Abfrage braucht nur einen Join auf die Beitragstabelle und keine Umwandlungen:
SELECT p.ID, d.price
FROM wp_posts p
JOIN wp_wpmcf_fg_product d ON d.mid = p.ID AND d.mtype = 'posts'
WHERE d.city = 'Istanbul'
ORDER BY d.price;Weil der Preis als Zahl gespeichert ist, ist die Sortierung danach eine normale numerische Sortierung. Weil es eine echte Spalte ist, können Sie einen normalen Index darauf legen – oder auf city – genau wie bei jeder anderen Tabelle in jeder anderen Anwendung. Die Datenbank tut, wofür sie gemacht ist: Sie springt über einen Index direkt zu den passenden Zeilen, statt alles zu durchsuchen und nebenbei eine Umwandlung zu berechnen.
Dieses SQL müssen Sie selten selbst schreiben. Übergeben Sie wpmcf_query an WP_Query, und das Plugin verknüpft die Tabelle der Gruppe für Sie:
$products = new WP_Query( [
'post_type' => 'product',
'wpmcf_query' => [
[ 'field' => 'product.city', 'value' => 'Istanbul' ],
],
'orderby' => [ 'wpmcf:product.price' => 'ASC' ],
] );Wir veröffentlichen hier keine Geschwindigkeitswerte. Sie hängen von Ihrem Hosting, Ihren Daten und Ihren Indizes ab, und wir möchten, dass Sie auf Ihrer eigenen Website messen, statt einer Zahl von uns zu vertrauen. Entscheidend ist der strukturelle Unterschied, unabhängig von jedem Benchmark: weniger Zeilen, echte Typen und Indizes dort, wo Sie sie brauchen. Die Syntax wird unter Beiträge nach Feldwert abfragen behandelt.
Mass Custom Fields legt pro Feldgruppe automatisch eine solche Tabelle an. Sie definieren ein Feld als Zahl, Datum oder kurzen Text, und das Plugin wählt dafür einen passenden Spaltentyp – SQL-Kenntnisse sind für eine typisierte Tabelle nicht nötig. Wenn Sie bereits ACF verwenden, kann das Plugin Ihre Feldgruppen und gespeicherten Werte übernehmen, ohne die ACF-Daten zu verändern; siehe Migration von ACF.
Die Vergleichsseite stellt diese Unterschiede sachlich dar.
Fazit
wp_postmeta eignet sich gut für ein paar Einstellungen pro Beitrag. Dafür wurde es gebaut: beliebige, gelegentliche Werte an einem Beitrag, die einzeln zurückgelesen werden. Für Kataloge, Verzeichnisse und alles, wonach Sie über viele Zeilen filtern oder sortieren, geben Sie den Daten stattdessen eine eigene Tabelle: typisierte Spalten, einen normalen Index und eine Abfrage, die die Datenbank wirklich optimieren kann.



