Viele Custom Post Types bremsen WordPress, weil jede Abfrage zusätzlich durch Meta-Daten und Taxonomien muss und dadurch mehr Joins, Scans und Sortierungen ausführt. Ich zeige, warum das passiert und wie ich die Performance mit einfachen, überprüfbaren Maßnahmen stabil halte.
Zentrale Punkte
Die folgenden Eckpunkte fasse ich vorab zusammen.
- Datenmodell: Eine wp_posts-Tabelle für alle Typen führt bei vielen Meta-Feldern zu dicken Joins.
- Queries: Ungezielte meta_query- und tax_query-Muster kosten Zeit und RAM.
- Indizes: Fehlende Keys auf wp_postmeta und Term-Tabellen verlängern Antwortzeiten.
- Caching: Page-, Object- und Query-Cache entschärfen Lastspitzen deutlich.
- Praxis: Weniger Felder, saubere Templates, gezielte WP_Query und gutes Hosting.
Warum viele Custom Post Types bremsen
WordPress speichert alle Inhalte, auch Custom Post Types, in wp_posts und unterscheidet sie nur über das Feld post_type. Das wirkt simpel, erzeugt aber Druck auf die Datenbank, sobald ich viele Meta-Felder und Taxonomien einbeziehe. Jede WP_Query muss dann durch wp_postmeta und die drei Term-Tabellen joinen, was die Zahl der Vergleiche und Sortierungen erhöht. Wachsen bestimmte Typen stark an, etwa ein großer Produkt- oder Kamera-Bestand, kippt die Antwortzeit zuerst in Archiven und Suchen. Ich erkenne das daran, dass dieselbe Seite mit weniger Feldern schneller lädt, während dichte Datensätze mit vielen Filtern die Latenz hochtreiben.
Wie WordPress Daten intern organisiert
Das markierte Feld post_type in wp_posts ist indiziert und macht einfache Abfragen flott, doch die Musik spielt in wp_postmeta. Jedes Custom Field landet als eigener Eintrag in dieser Tabelle und vervielfacht die Zeilen pro Beitrag. Wenn ein Beitrag 100 Felder besitzt, entstehen 100 zusätzliche Datensätze, die jede meta_query sichten muss. Dazu kommen die Taxonomie-Tabellen wp_terms, wp_term_taxonomy und wp_term_relationships, die ich für Archive, Filter und Facetten einbinde. Steigt die Zahl der Joins, steigen auch CPU-Zeit und Speicherverbrauch, was ich in Top, htop und Query-Monitor sofort an der Auslastung sehe.
Teure SQL-Muster erkennen
Ich prüfe erst die teuren Muster, denn dort liegen die dicken Gewinne für Performance. Besonders kritisch werden meta_query mit mehreren Bedingungen und LIKE-Vergleichen auf meta_value, weil sie häufig nicht auf Indizes treffen. Genauso verlängern breite tax_query mit mehreren Relationen die Zeit, bis MySQL einen passenden Ausführungsplan findet. Ich limitiere Felder, normalisiere Werte und halte Vergleiche so exakt wie möglich, damit Indizes greifen. Die folgende Tabelle hilft mir bei der Einordnung häufiger Bottlenecks und ihrer Alternative:
| Pattern | Typische Kosten | Symptom | Bessere Option |
|---|---|---|---|
| meta_query mit LIKE auf meta_value | hoch ohne Index | lange Query-Zeit, hoher CPU | exakte Werte, normalisierte Spalten, INT/DECIMAL nutzen |
| tax_query mit mehreren Relationen (AND) | mittel bis hoch | Archive langsam, Paginierung stockt | Facettierung cachen, Vorfilter in eigenem Index |
| posts_per_page = -1 | sehr hoch bei großen Typen | Speicher läuft voll | Pagination, Cursor, asynchrone Listen |
| ORDER BY meta_value ohne Cast | hoch | Sortierung träge | numerische Felder, separate Spalte, voraggregiert sortieren |
Der Einfluss von Custom Fields auf wp_postmeta
Ich habe Setups gesehen, in denen hunderte Felder pro Beitrag lagen und die Postmeta-Tabelle im Gigabyte-Bereich wuchs. In solchen Fällen explodiert die Zahl der Zeilen, die MySQL scannen muss, und selbst einfache Filter geraten ins Stolpern. Kritisch sind Felder, die eigentlich numerisch sind, aber als Text gespeichert werden, weil Vergleiche und Sortierung dann teurer sind. Ich lagere selten genutzte Daten aus, reduziere Pflichtfelder auf das Nötige und nutze Repeater-Felder sparsam. So bleiben die Tabellen schmaler, und die Query-Planer finden schneller den passenden Zugriffspfad.
Taxonomien, Feeds und Archive gezielt straffen
Taxonomien sind stark, doch ich setze sie gezielt ein, sonst belaste ich jede Archivseite unnötig. Feeds und globale Archive sollten nicht alle Post Types mischen, wenn nur einer relevant ist. Ich steuere das über pre_get_posts und schließe Post Types aus, die dort nichts verloren haben. Suchseiten profitieren ebenfalls, wenn ich unpassende Typen ausschließe oder separate Such-Templates anlege. Zeigt die Datenbank hohe Lese-Last, reduziere ich die Anzahl der joinenden Tabellen und pufferte häufige Archiv-Ansichten im Objekt-Cache.
Caching-Strategien, die wirklich tragen
Ich kombiniere Page-Cache, Objekt-Cache und Transients, damit teure Abfragen gar nicht erst laufen. Page-Cache fängt anonyme Besucher ab und entlastet PHP und MySQL sofort. Der Objekt-Cache (z. B. Redis oder Memcached) hält WP_Query-Ergebnisse, Terms und Optionen vor und spart Roundtrips. Für Filter, Facetten und teure Meta-Abfragen setze ich Transients mit sauberen Invalidation-Regeln ein. So bleiben auch große Archive schnell, selbst wenn einzelne Custom Post Types zehntausende Einträge besitzen.
Indizes setzen und Datenbank pflegen
Ohne passende Indizes wirkt jedes Tuning wie ein Tropfen auf den heißen Stein. Ich ergänze Keys auf wp_postmeta für (post_id, meta_key), oft auch (meta_key, meta_value) je nach Einsatz. Für Term-Beziehungen prüfe ich Keys auf (object_id, term_taxonomy_id) und säubere verwaiste Relationen regelmäßig. Danach kontrolliere ich mit EXPLAIN, ob MySQL die Indizes wirklich nutzt und ob Sortierung per Filesort verschwindet. Einen strukturierten Einstieg in das Thema liefert mir dieser Beitrag zu Datenbank-Indizes, den ich als Checkliste nutze.
Gute Query-Gewohnheiten statt Vollauszüge
Ich setze WP_Query mit klaren Filtern ein und vermeide posts_per_page = -1, weil das Speicher und CPU exponentiell treibt. Stattdessen paginiere ich hart, nutze eine stabile Reihenfolge und liefere nur die Spalten, die ich wirklich brauche. Für Landingpages ziehe ich Teaser mit wenigen Feldern, die ich voraggregiere oder cache. Außerdem prüfe ich Rewrite-Regeln, weil falsches Routing unnötige DB-Treffer auslöst; ein tieferer Blick in Rewrite Rules als Bremse spart mir oft mehrere Millisekunden pro Request. Wer Suche, Archive und Feeds trennt und jeweils passende Queries einsetzt, reduziert Last spürbar.
Tools, Plugins und Feld-Design schlank halten
Plugins für Felder und Post Types bieten viel, doch ich prüfe ihren Overhead mit Query Monitor und New Relic. Wenn ein CPT hunderte Felder nutzt, teile ich das Datenmodell und lagere selten genutzte Gruppen aus. Nicht jedes Feld gehört in wp_postmeta; manche Daten halte ich in eigenen Tabellen mit klaren Indizes. Ich vermeide unnötige Hierarchien bei Post Types, weil sie Baumstrukturen und Queries aufblähen. Saubere Templates (single-xyz.php, archive-xyz.php) und sparsame Loops halten Renderzeiten kurz.
Hosting und WP scaling in der Praxis
Ab einer gewissen Größe wird WP scaling zur Infrastrukturfrage. Ich nutze reichlich RAM, schnelle NVMe-Storage und aktiviere Persistent Object Cache, damit WordPress nicht ständig neu lädt. Ein Caching-Setup auf Serverebene plus PHP-FPM mit passender Prozesszahl hält Antwortzeiten planbar. Wer stark auf Custom Post Types setzt, profitiert von Hosting mit integriertem Redis und OpCache-Warmup. Bei hosting wordpress achte ich darauf, dass die Plattform Lastspitzen über Queueing und Edge-Cache abfedert.
Suche, Feeds und REST API effizient einsetzen
Suche und REST API wirken wie kleine Details, verursachen aber viele Requests pro Session. Ich limitiere Endpunkte, cache Antworten und setze Conditional Requests ein, damit Clients nicht alles erneut ziehen. Für die REST API minimiere ich Felder im Schema, filtere Post Types streng und aktiviere ETags. Wenn Headless-Frontends laufen, lohnt sich eine eigene Cache-Strategie pro CPT und Route; einen praktischen Überblick hole ich mir hier: REST API Performance. RSS/Atom-Feeds halte ich kurz und schließe unnötige Typen aus, sonst rufen Crawler zu viel ab.
WP_Query-Optionen, die sofort helfen
Viele Bremsen löse ich mit wenigen, zielgenauen Parametern in WP_Query. Sie reduzieren Datenmenge, vermeiden teure Zählungen und sparen Cache-Bandbreite.
- no_found_rows = true: Deaktiviert die Gesamtzählung für Pagination. Ideal für Widgets, Teaser und REST-Listen, die keine Gesamtseitenzahl zeigen.
- fields = ‚ids‘: Liefert nur IDs und vermeidet, dass komplette Post-Objekte aufgebaut werden. Danach hole ich gezielte Metadaten in einem Rutsch (Meta-Cache-Priming).
- update_post_meta_cache = false und update_post_term_cache = false: Spart Cache-Aufbau, wenn ich Metas/Terms in diesem Request nicht brauche.
- ignore_sticky_posts = true: Verhindert zusätzliche Sortierlogik in Archiven, die nicht von Sticky Posts profitieren.
- orderby und order deterministisch wählen: Vermeidet teure Sortierungen und instabile Caches, besonders bei großen CPTs.
Diese Schalter bringen oft zweistellige Prozentwerte, ohne die Ausgabe zu verändern. Wichtig ist, sie pro Template und Einsatzzweck zu setzen, nicht global.
Backend und Admin-Listen beschleunigen
Große Post Types bremsen nicht nur Frontend, sondern auch das Backend. Ich mache die Listenansicht schneller, indem ich Spalten und Filter auf das Nötige reduziere. Zähler für Taxonomien und den Papierkorb kosten bei großen Tabellen Zeit; ich deaktiviere unnötige Counters und nutze kompakte Filter. Außerdem begrenze ich die Anzahl sichtbarer Einträge pro Seite, damit die Admin-Query nicht in Speichergrenzen läuft. Über pre_get_posts differenziere ich zwischen Frontend und Admin, setze dort andere Parameter (z. B. no_found_rows) und verhindere breite meta_query in der Übersicht. Das Resultat: schnellere Redakteurs-Workflows und weniger Timeout-Risiko.
Materialisierung: Vorberechnete Werte statt teurer Laufzeit-Filter
Wenn dieselben Filter und Sortierungen immer wieder auftauchen, materialisiere ich Felder in einer eigenen Lookup-Tabelle. Beispiel: Ein Produkt-CPT sortiert häufig nach Preis und filtert nach Verfügbarkeit. Ich halte dafür eine Tabelle mit post_id, preis DECIMAL, verfuegbar TINYINT und passenden Indizes. Beim Speichern aktualisiere ich diese Werte; im Frontend greife ich direkt darauf zu und hole die Post-IDs zurück. Anschließend löst WP_Query nur noch die ID-Menge in Beiträge auf. Das reduziert die Last auf wp_postmeta drastisch und macht ORDER BY auf numerischen Spalten wieder günstig.
Daten-Typisierung und Generated Columns
Viele Meta-Felder stehen als LONGTEXT in meta_value – nicht indexierbar und teuer. Ich nutze zwei Muster: Erstens typisierte Spiegel-Felder (z. B. preis_num als DECIMAL), auf die ich indiziere und vergleiche. Zweitens Generated Columns in MySQL, die einen Ausschnitt oder Cast aus meta_value bereitstellen und indexierbar machen. Beides sorgt dafür, dass LIKE-Fälle verschwinden und Vergleiche wieder auf Indizes landen. Neben der Query-Geschwindigkeit verbessert sich damit auch die Relevanzplanung von Caches, weil Sortierung und Filter deterministisch sind.
Revisions, Autoload und Aufräumen
Neben Queries selbst bremst auch Datenmüll. Ich limitiere Revisionen, lösche alte Auto-Saves und leere den Papierkorb regelmäßig, damit Tabellen nicht unendlich wachsen. In wp_options prüfe ich den Autoload-Bestand: Zu viele autoloaded Optionen verlängern jeden Request, unabhängig von CPTs. Ich räume verwaiste Postmetas und Term-Relationen auf, entferne ungenutzte Taxonomien und verschlanke Cron-Jobs, die große Suchen fahren. Diese Hygiene sorgt für stabile Pläne des Query-Optimizers und verhindert, dass Indizes an Wirksamkeit verlieren.
Monitoring und Messmethodik
Ohne Messen bleibt Optimierung Blindflug. Ich nutze Query Monitor für den PHP-Teil, EXPLAIN und EXPLAIN ANALYZE für MySQL, sowie das Slow-Query-Log mit praxisnahen Schwellen. Ich schaue auf Kennzahlen wie Rows Examined, Handler Read Key/Firts, Sorts per Filesort und Temp Tables on Disk. Unter Last prüfe ich mit realistischen Datenmengen, damit sich Kartenhäuser nicht erst im Live-Betrieb zeigen. Jede Änderung dokumentiere ich zusammen mit einem vorher/nachher-Snapshot; so bauen sich Maßnahmen zur verlässlichen Checkliste aus, die ich auf neue CPT-Projekte übertrage.
Konsequentes Cache-Design: Invalidation und Warmup
Cache hilft nur, wenn Invalidation stimmt. Für Archive und Facetten definiere ich Schlüssel, die nur bei relevanten Änderungen auslaufen – etwa beim Wechsel einer Verfügbarkeit oder Preisänderung. Ich bündele Invalidations in Hooks (save_post, updated_post_meta), damit nicht die ganze Seite kalt wird. Nach Deployments wärme ich häufige Routen, Sitemaps und Archive vor. Auf Edge- oder Server-Cache-Ebene setze ich variable TTLs pro CPT, damit heiße Pfade länger liegen bleiben, während seltene Listen kürzere TTLs bekommen. Zusammen mit einem persistenten Objekt-Cache bleiben Miss-Raten kalkulierbar.
Multisite, Sprache und Relationen
Installationen mit mehreren Sites oder Sprachen verstärken Join-Last, weil pro Kontext zusätzliche Filter einfließen. Ich isoliere daher große CPTs, wenn möglich, auf eigene Sites und verhindere, dass globale Widgets alle Netzwerke abklappern. Bei Übersetzungen halte ich Relationen zwischen Original und Übersetzung schlank und vermeide redundante Metafelder. Konsistente Typisierung und ein einheitliches Facetten-Set pro Sprache reduzieren die Zahl notwendiger Abfragen spürbar.
Ressourcen-Steuerung und Timeouts
Hohe Parallelität führt bei großen CPTs zu Locking und saturiert I/O. Ich plane FPM-Worker so, dass sie zum CPU- und I/O-Profil passen, und begrenze gleichzeitige, große Listen-Queries mit Rate-Limits im Frontend. Batch-Prozesse (Reindexing, Import) laufen entkoppelt in Randzeiten, damit Caches nicht kollabieren. MySQL profitiert von sauber dimensionierten Buffer-Pools und Perioden mit ANALYZE TABLE, damit Statistiken aktuell bleiben und der Optimizer bessere Pläne wählt.
Deployment-Strategien für große CPTs
Strukturelle Änderungen an großen Post Types rolle ich inkrementell aus. Neue Indizes setze ich online, Materialisierungstabellen befülle ich nebenläufig und schalte Queries erst um, wenn genug Daten vorliegen. Während Migrationen sichere ich Caches mit längeren TTLs ab und halbiere so den Live-Druck. Feature-Flags erlauben Probeläufe mit einem Teil des Traffics. Wichtig ist, dass ich Rollback-Pfade definiere: Alte Queries dürfen bei Bedarf kurzfristig übernehmen, bis die neue Route optimiert ist.
Zukunft: Content Models im WordPress-Kern
Ich beobachte die Arbeit an nativen Content Models, weil sie Feld-Definitionen näher an den Core bringen. Weniger Abhängigkeit von großen Feld-Plugins könnte Query-Pfade vereinfachen und Caching stabiler machen. Wenn Feldtypen klar typisiert sind, greifen Indizes besser und Sortierungen fallen günstiger aus. Das hilft gerade bei Archiven, die viele Filter besitzen und heute stark auf wp_postmeta angewiesen sind. Bis dahin lohnt es sich, Felder sauber zu typisieren und numerische Werte als INT/DECIMAL anzulegen.
Praxis-Setup: Schritt für Schritt zur schnellen CPT-Site
Ich starte immer mit Messen: Query Monitor, Debug Bar, EXPLAIN und realistische Datenmengen auf Staging. Danach setze ich Page-Cache, aktiviere Redis und optimiere die drei langsamsten Abfragen mit Indizes oder Materialisierung. Im dritten Schritt reduziere ich Felder, ersetze -1-Listen durch Pagination und streiche unnötige Sortierungen. Viertens schreibe ich dedizierte Archive pro CPT und entferne breit gefasste Vorlagen, die zu viel laden. Zum Schluss härte ich die REST API und Feeds ab, damit Bots die Datenbank nicht permanent aufwecken.
Kurz zusammengefasst
Viele Custom Post Types verlangsamen WordPress, weil Meta- und Taxonomie-Joins die Datenbank belasten. Ich halte Abfragen schlank, setze Indizes, cache teuerste Pfade und reduziere Felder auf das Nötige. Saubere Templates, klare WP_Query-Filter und ein passendes Hosting sorgen für konstante Antwortzeiten. Wer zusätzlich Rewrite-Regeln, REST API und Feeds strafft, spart weitere Millisekunden ein. So bleibt selbst eine große Sammlung an Custom Post Types schnell, wartbar und bereit für künftiges WP scaling.


