Im Standardbetrieb von WordPress reicht die herkömmliche Beitragsabfrage für einfache Blogs vollkommen aus. Sobald Sie jedoch maßgeschneiderte Unternehmensportale, komplexe E-Commerce-Kataloge oder redaktionelle Magazine entwickeln, stoßen einfache Schleifen schnell an ihre Grenzen.
Mehrere Custom Post Types, verschachtelte Taxonomien, numerische Preisfilter und Tausende Datensätze fordern die Datenbank heraus. Ohne fundiertes Verständnis der WP_Query-Interna entstehen träge Abfragen, die den Server belasten und die Time to First Byte (TTFB) in die Höhe treiben.
In diesem technischen Leitfaden analysieren wir die fortgeschrittene Nutzung von tax_query und meta_query, decken versteckte Performance-Schalter auf und zeigen, warum der Hook pre_get_posts sauberen Code garantiert.
Wenn Sie Unterstützung bei der Optimierung datenbankintensiver WordPress-Lösungen benötigen, informieren Sie sich über unsere Dienstleistungen als erfahrene WordPress-Entwickler.
WP_Query tax_query mit AND und OR verschachteln
Das Argument tax_query erlaubt weit mehr als das einfache Filtern nach einer einzelnen Kategorie. Sie können beliebig viele Taxonomien über verschachtelte AND- und OR-Beziehungen logisch miteinander verknüpfen.
Beiträge nach Kategorie und mehreren Tags filtern
Stellen Sie sich vor, Sie möchten Beiträge ausgeben, die:
- In der Kategorie
tutorialliegen UND - Entweder mit dem Schlagwort
wordpressODERphpmarkiert sind:
$args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 12,
'no_found_rows' => true,
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'category',
'field' => 'slug',
'terms' => ['tutorial'],
'operator' => 'IN',
],
[
'relation' => 'OR',
[
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => ['wordpress'],
],
[
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => ['php'],
],
],
],
];
$query = new WP_Query($args);tax_query mit NOT IN und EXISTS
Nutzen Sie den Parameter 'operator' => 'NOT IN', um bestimmte Begriffe gezielt auszuschließen. Mit 'operator' => 'EXISTS' oder 'NOT EXISTS' können Sie prüfen, ob einem Beitrag überhaupt ein Begriff der Taxonomie zugewiesen ist, ohne feste Werte definieren zu müssen.
Da Taxonomien über dedizierte Indizes in den Tabellen wp_term_relationships und wp_term_taxonomy verfügen, sind Taxonomie-Abfragen meist günstiger als Meta-Daten-Abfragen.
Nach benutzerdefinierten Feldern mit meta_query filtern
meta_query filtert über wp_postmeta. Jede Klausel hat einen key und optional value, compare und type. type (NUMERIC, DECIMAL, DATE, CHAR, BINARY und weitere) bestimmt den CAST in SQL. Ohne NUMERIC ist der Vergleich zwischen “10” und “2” lexikografisch: '2' > '10' als CHAR.
Einfache Klausel mit CAST
$args = [
'post_type' => 'product',
'meta_query' => [
[
'key' => '_stock',
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
],
],
];meta_query mit EXISTS und NOT EXISTS
Manchmal interessiert nicht der Wert, sondern nur, ob der Schlüssel existiert. compare => 'EXISTS' (und NOT EXISTS) braucht keinen value. Das ist günstiger als ein LIKE auf einen leeren String und liest sich im Code-Review besser.
$args = [
'post_type' => 'event',
'meta_query' => [
'relation' => 'AND',
[
'key' => 'event_start',
'compare' => 'EXISTS',
],
[
'key' => 'event_cancelled',
'compare' => 'NOT EXISTS',
],
],
];Geben Sie bei EXISTS keinen value mit. Im Quellcode von WP_Meta_Query wird EXISTS mit Wert als = interpretiert; nur bei NOT EXISTS wird der Wert ignoriert.
BETWEEN, IN und LIKE in meta_query
BETWEEN braucht ein Array mit zwei Werten, IN eine Liste. LIKE mit % auf beiden Seiten zwingt MySQL, jeden meta_value des Schlüssels zu lesen, weil der Index auf meta_key für eine Suche mitten im Wert nicht reicht. Wenn Sie nach einem Textfragment in Meta-Daten filtern, ist eine eigene Taxonomie oder eine denormalisierte Spalte in einer eigenen Tabelle meist die bessere Lösung.
AND/OR-Relation und benannte Klauseln in meta_query
Die Syntax entspricht der von tax_query, Gruppen lassen sich verschachteln. Benannte Klauseln (ein String-Schlüssel statt eines numerischen Index) erlauben es, anschließend mit orderby nach dem Klauselnamen zu sortieren. Das ist das offizielle Muster aus der WP_Query-Dokumentation, kein Trick.
$args = [
'post_type' => 'product',
'meta_query' => [
'relation' => 'AND',
'stock_clause' => [
'key' => '_stock_status',
'value' => 'instock',
'compare' => '=',
],
'rating_clause' => [
'key' => '_wc_average_rating',
'value' => 4,
'compare' => '>=',
'type' => 'DECIMAL',
],
],
'orderby' => [
'rating_clause' => 'DESC',
'date' => 'DESC',
],
];tax_query und meta_query in einer Abfrage kombinieren
Beide Argumente funktionieren gleichzeitig. WordPress verbindet sie zu einem einzigen SELECT mit JOINs auf Terme und Meta-Daten. Typisches Katalogszenario: eine Kategorie-Taxonomie plus das Feld _featured oder ein Datumsbereich.
$args = [
'post_type' => 'product',
'posts_per_page' => 24,
'tax_query' => [
[
'taxonomy' => 'product_cat',
'field' => 'slug',
'terms' => 't-shirts',
],
],
'meta_query' => [
'relation' => 'AND',
[
'key' => '_stock',
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
],
[
'key' => '_featured',
'value' => 'yes',
'compare' => '=',
],
],
];Jede zusätzliche Meta-Klausel ist ein weiterer JOIN auf wp_postmeta. Zwei Meta-Filter können die Antwortzeit stärker verlängern als drei Taxonomien, weil wp_term_relationships besser indiziert ist als das EAV-Modell von postmeta. Filtern Sie an vielen Stellen nach demselben Schlüssel (etwa Lagerbestand und Hervorhebung), lohnt sich eine Denormalisierung in Taxonomie-Attribute oder eine Indextabelle. Den Ausschlag gibt EXPLAIN, wenn es auf den Meta-JOINs Using where; Using temporary zeigt.
WP_Query ohne Paginierung mit no_found_rows beschleunigen
Standardmäßig, sobald die Abfrage ein LIMIT hat, weist WordPress die MySQL-Datenbank an, die Gesamtzahl aller passenden Treffer zu ermitteln, selbst wenn Sie nur fünf Beiträge abrufen. Dies geschieht intern über die SQL-Klausel SQL_CALC_FOUND_ROWS und einen anschließenden SELECT FOUND_ROWS()-Befehl, um die Gesamtanzahl für Paginierungslinks (paginate_links()) bereitzustellen.
Bei großen Tabellen ist dieser Zählprozess teuer, weil MySQL alle passenden Zeilen durchgehen muss, auch wenn es nur fünf zurückgibt:
-- Standardverhalten: Zählung aller Treffer
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE ... LIMIT 0, 5;
SELECT FOUND_ROWS();Wenn Sie jedoch eine Liste für ein Sidebar-Widget, verwandte Artikel (Related Posts) oder einen JSON-Feed abrufen, benötigen Sie keine Paginierung.
Durch das Setzen von 'no_found_rows' => true entfällt der Zählvorgang komplett:
$args = [
'post_type' => 'produkt',
'posts_per_page' => 4,
'no_found_rows' => true, // Kein SQL_CALC_FOUND_ROWS
];Ohne SQL_CALC_FOUND_ROWS kann MySQL beim LIMIT aufhören, wenn die Sortierung aus einem Index kommt. MySQL hat SQL_CALC_FOUND_ROWS und FOUND_ROWS() seit Version 8.0.17 als veraltet markiert (MySQL-Handbuch).
update_post_meta_cache und Term-Cache in WP_Query deaktivieren
Wenn WP_Query eine Liste von Beitrags-IDs findet, führt WordPress standardmäßig sogenannte “Cache Priming”-Abfragen aus: Es lädt alle verknüpften Meta-Felder und alle Taxonomie-Begriffe in den Objekt-Cache.
Das ist vorteilhaft, wenn Sie im Loop Funktionen wie the_title(), get_field() und the_tags() aufrufen. Wenn Sie jedoch eine schlanke Abfrage schreiben, die beispielsweise nur Beitrags-IDs für eine Sitemap oder Bild-IDs für einen Slider benötigt, ist dieses Verhalten reine Ressourcenverschwendung:
$args = [
'post_type' => 'portfolio',
'posts_per_page' => 50,
'no_found_rows' => true,
'update_post_meta_cache' => false, // Keine Meta-Daten vorab laden
'update_post_term_cache' => false, // Keine Taxonomien vorab laden
'fields' => 'ids', // Gibt ein flaches Array von IDs zurück
];
$post_ids = (new WP_Query($args))->posts;Mit 'fields' => 'ids' spart PHP Arbeitsspeicher, da keine vollständigen WP_Post-Objekte instanziiert werden müssen.
orderby => rand wirkt verlockend für “ähnliche Beiträge”, erzeugt auf großen Tabellen aber ORDER BY RAND(), das das gesamte Ergebnis im Speicher sortiert. Besser: Holen Sie mit fields => ids und no_found_rows einen ID-Pool, mischen Sie ihn in PHP und cachen Sie die Auswahl einige Minuten in einem Transient oder im Objekt-Cache.
Filter mit suppress_filters in WP_Query abschalten
Wenn Plugins von Drittanbietern sich an posts_where, posts_join oder posts_clauses hängen, erben sekundäre Abfragen Bedingungen, die für die Hauptschleife gedacht waren: Sprachfilter, Zugriffsprüfungen oder zusätzliche JOINs.
In Hintergrundjobs, CLI-Skripten und isolierten Mikro-Abfragen können Sie WP_Query mit 'suppress_filters' => true abschirmen:
$saubere_args = [
'post_type' => 'product',
'posts_per_page' => 10,
'no_found_rows' => true,
'suppress_filters' => true,
'fields' => 'ids',
];
$saubere_query = new WP_Query( $saubere_args );get_posts() setzt 'suppress_filters' => true standardmäßig, new WP_Query() dagegen false. Das erklärt, warum identische Argumente je nach Aufrufer unterschiedliche Ergebnisse liefern können. suppress_filters schaltet die posts_*-Filter ab (posts_where, posts_join, posts_clauses und die übrigen), nicht aber die Action pre_get_posts, die der Core immer ausführt. Begrenzt ein Mehrsprachigkeits-Plugin die Sprache über diese Filter, liefert die Abfrage Beiträge in allen Sprachen. Prüfen Sie vor dem Einschalten, wie Ihr Plugin filtert.
Warum meta_query in WordPress langsam ist
Die WordPress-Tabelle wp_postmeta ist ein universeller Key-Value-Speicher. Die Spalte meta_value hat den Datentyp longtext und ist im WordPress-Schema nicht indiziert; MySQL erlaubt auf longtext ohnehin nur einen Präfix-Index.
meta_query mit LIKE vermeiden
// BITTE VERMEIDEN: liest jede Zeile dieses Schlüssels
'meta_query' => [
[
'key' => 'kunden_adresse',
'value' => 'Berlin',
'compare' => 'LIKE', // Wird zu LIKE '%Berlin%'
],
]Solche Abfragen zwingen MySQL, jede Zeile mit diesem meta_key zu lesen und den Text zu durchsuchen. Bei einem führenden % hilft kein Index.
meta_query schneller machen
- Typisierung definieren: Wenn Sie numerische Werte (z. B. Preise oder Quadratmeter) vergleichen, deklarieren Sie immer den Typ:
'meta_query' => [
[
'key' => 'immobilie_preis',
'value' => [200000, 500000],
'type' => 'NUMERIC',
'compare' => 'BETWEEN',
],
]- Architektur-Alternative: Wenn Sie häufig nach Attributen wie “Farbe”, “Ort”, “Hersteller” oder “Status” filtern müssen, modellieren Sie diese Felder als Custom Taxonomy, nicht als Post-Meta. Taxonomien sind relational aufgebaut und nutzen die Indizes von
wp_term_relationships.
Typische Fallen in wp_postmeta
- Mehrere Schlüssel mit derselben Bedeutung:
_price,price,product_pricenach mehreren Migrationen. Die Abfrage liefert ein leeres oder inkonsistentes Ergebnis. Vereinheitlichen Sie den Schlüssel, bevor Sie das SQL optimieren. - Serialisierte Arrays in Meta-Feldern:
LIKEauf PHP-Serialisierung ist fragil und langsam. Halten Sie Beziehungen in Taxonomien oder in getrennten Meta-Zeilen mit flachen Werten. meta_querymit leeremkey: erlaubte historisch einen reinen Wertvergleich, durchsucht aber die ganze Tabelle. Vermeiden.include_children => trueauf tiefen Kategoriebäumen: WordPress expandiert den Baum in PHP und hängt die Terme anIN (...). Bei Hunderten Unterkategorien wächst die ID-Liste stark; manchmal ist eine flache Taxonomie nur mit Blattkategorien günstiger.
Ist ein Meta-Filter geschäftskritisch (Verfügbarkeit, Region, SKU), lohnt sich eine eigene Tabelle mit passenden Indizes und eine dünne Schicht über posts_clauses / posts_join. Die meta_query des Core bleibt für seltene Filter im Admin-Bereich, nicht für den heißen Pfad des Shops.
Zusammengesetzter Index auf wp_postmeta
Das WordPress-Schema (wp-admin/includes/schema.php) legt auf wp_postmeta drei Schlüssel an: PRIMARY KEY (meta_id), KEY post_id (post_id) und KEY meta_key (meta_key(191)).
Nehmen Sie eine Bestandsprüfung:
SELECT post_id FROM wp_postmeta WHERE meta_key = '_stock_status' AND meta_value = 'instock';MySQL kann nur den Index auf meta_key nutzen. Es findet die Zeilen dieses Schlüssels und muss dann jede Zeile lesen, um zu prüfen, ob meta_value passt. Bei vielen Zeilen pro Schlüssel bedeutet das viele Lesezugriffe auf den Buffer Pool.
Ein zusammengesetzter Index (meta_key(191), meta_value(100)) erlaubt MySQL, Schlüssel und Wert im Index zu prüfen, sodass aus der Tabelle nur noch passende Zeilen gelesen werden. Ein abdeckender Index ist das nicht: Ein InnoDB-Sekundärindex speichert den Primärschlüssel (meta_id), nicht post_id, also wird die Zeile für post_id weiterhin gelesen:
ALTER TABLE wp_postmeta ADD INDEX meta_key_val_comp (meta_key(191), meta_value(100));Testen Sie solche Änderungen auf Staging mit EXPLAIN und Query Monitor. Zusätzliche Indizes beschleunigen gezielte Lesezugriffe, verteuern aber jedes update_post_meta() und add_post_meta().
Direkte SQL-Abfragen mit wpdb prepare statt WP_Query
WP_Query ist das richtige Werkzeug für Templates, Paginierung und die Integration mit dem Objekt-Cache. Für Aggregationen, Joins mit eigenen Tabellen oder das Abrufen vieler IDs ohne die Core-Schicht ist eine direkte SQL-Abfrage über $wpdb oft einfacher und schneller.
Verwenden Sie immer $wpdb->prepare(), um die Abfrage zu parametrisieren und SQL-Injection zu verhindern:
global $wpdb;
$mindestbestand = 5;
$laendercode = 'DE';
$sql = $wpdb->prepare(
"SELECT p.ID, pm_stock.meta_value AS bestand
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_stock
ON p.ID = pm_stock.post_id AND pm_stock.meta_key = %s
INNER JOIN {$wpdb->postmeta} pm_geo
ON p.ID = pm_geo.post_id AND pm_geo.meta_key = %s
WHERE p.post_type = %s
AND p.post_status = %s
AND CAST(pm_stock.meta_value AS UNSIGNED) >= %d
AND pm_geo.meta_value = %s
ORDER BY CAST(pm_stock.meta_value AS UNSIGNED) DESC
LIMIT %d",
'_stock',
'_target_country',
'product',
'publish',
$mindestbestand,
$laendercode,
20
);
$ergebnisse = $wpdb->get_results( $sql );Entscheidungskriterium:
WP_Queryin Theme-Templates, normalen paginierten Archiven und Schleifen, die den WordPress-Objekt-Cache füllen sollen.$wpdb->prepare()für Exporte, Statistik-Dashboards, numerische Aggregationen (SUM(),AVG()) und Endpunkte, bei denen die Filterkette vonWP_Querynur Latenz hinzufügt.
WordPress-Hauptabfrage mit pre_get_posts anpassen
Ein häufiger Architekturfehler in Themes ist das Überschreiben der Hauptschleife im Template:
// SCHLECHTE PRAXIS in archive-produkt.php:
// WordPress hat die Standardbeiträge bereits geladen!
$custom_query = new WP_Query(['post_type' => 'produkt', 'posts_per_page' => 24]);
while ($custom_query->have_posts()) : $custom_query->the_post();Hier führt der Server zwei vollständige Abfragen aus: Zuerst die Standardabfrage von WordPress, danach Ihre benutzerdefinierte Abfrage.
Die saubere, professionelle Methode nutzt den Hook pre_get_posts in der functions.php:
add_action('pre_get_posts', function (WP_Query $query): void {
// Nur die Hauptabfrage im Frontend anpassen, niemals das Admin-Panel
if (is_admin() || !$query->is_main_query()) {
return;
}
if ($query->is_post_type_archive('produkt')) {
$query->set('posts_per_page', 24);
$query->set('no_found_rows', false); // Hier für Paginierung nötig
}
if ($query->is_search()) {
// Schließe statische Seiten aus der Suchergebnisseite aus
$query->set('post_type', ['post', 'produkt']);
}
});Mit diesem Ansatz modifizieren Sie die Abfrage, bevor MySQL sie ausführt. Die Templates bleiben sauber, Paginierungslinks funktionieren fehlerfrei und die Datenbank wird nur ein einziges Mal angesprochen.
WP_Query-Ergebnisse mit Transients cachen
Wenn eine komplexe Auswertung unvermeidbar ist (z. B. Bestseller-Listen mit mehreren Joins), sollten Sie das Ergebnis im persistenten Cache speichern:
function wppoland_hole_top_angebote(): array {
$cache_key = 'wppoland_top_angebote';
$angebote = get_transient($cache_key);
if (false === $angebote) {
$args = [
'post_type' => 'angebot',
'posts_per_page' => 6,
'no_found_rows' => true,
'tax_query' => [
[
'taxonomy' => 'aktion',
'field' => 'slug',
'terms' => 'highlight',
],
],
];
$angebote = (new WP_Query($args))->posts;
// Speichere das Ergebnis für 2 Stunden im Cache
set_transient($cache_key, $angebote, 2 * HOUR_IN_SECONDS);
}
return $angebote;
}WP_Query in Query Monitor prüfen
Query Monitor ist das Standardwerkzeug für die Diagnose auf Staging. Öffnen Sie das Panel Queries und filtern Sie nach dem Aufrufer WP_Query. Achten Sie auf:
- die Zahl der Abfragen pro Request (dieselbe
meta_querymehrfach aus verschiedenen Widgets), - die Zeit eines einzelnen SELECT mit JOIN auf
wp_postmeta, - die Komponente (Theme oder Plugin): Oft steckt die “langsame WP_Query” im Related-Posts-Plugin eines Drittanbieters, nicht in Ihrem Shortcode,
- Duplikate: dasselbe SQL zweimal, weil der Objekt-Cache fehlt oder
wp_cache_flushin einer Schleife läuft.
Aktivieren Sie auf Staging zusätzlich SAVEQUERIES, nur für die Dauer der Messung. In der Produktion gehört Query Monitor hinter die Capability view_query_monitor oder wird ganz deaktiviert, weil das Plugin selbst Last erzeugt.
Praktische Routine vor dem Merge:
- Seite mit dem neuen Filter öffnen.
- Das SQL aus Query Monitor sichern.
- Auf einer Datenbankkopie mit
EXPLAINprüfen. - Mit der Variante
no_found_rows+fields => idsvergleichen. - Erst dann das Ergebnis cachen.
Wie WP_Query den Objekt-Cache nutzt
Ein Objekt-Cache (Redis, Memcached über ein Drop-in) beschleunigt schlechtes SQL nicht. Er beschleunigt wiederholte Lesezugriffe auf dieselben Beitrags-, Meta- und Term-Objekte innerhalb eines Requests und über Requests hinweg, solange die Cache-Gruppen nicht bei jedem Schreibvorgang geleert werden.
So hängt das mit WP_Query zusammen:
- Nach dem SELECT ruft der Core
update_post_caches()auf. Die Schalterupdate_post_*_cacheentscheiden, ob Meta-Daten und Terme sofort in einer gebündelten Abfrage geladen oder dem Lazy Loading überlassen werden. - Mit
fields => idsstehen keine vollständigenWP_Post-Objekte in$query->posts, der Beitrags-Cache füllt sich also nicht nebenbei. Rufen Sie danachget_post()undget_post_meta()pro ID in einer Schleife auf, sind Sie wieder beim N+1-Problem, außer Sie rufen selbstupdate_postmeta_cache( $ids )auf. - Ein Transient mit einer serialisierten ID-Liste für 5 bis 15 Minuten ist meist günstiger als ein “cleveres”
orderby randbei jedem Aufruf. Invalidierung: über die Hookssave_post/edited_termdes jeweiligen Post Types.
Verwechseln Sie Seiten-Cache (komplettes HTML) nicht mit dem Objekt-Cache. Der Seiten-Cache umgeht PHP; der Objekt-Cache hilft, wenn PHP die Antwort ohnehin bauen muss (Warenkorb, eingeloggter Nutzer, ESI).
Wie WP_Query seit WordPress 6.1 Abfrageergebnisse cacht
Seit WordPress 6.1 hat WP_Query einen eigenen Ergebnis-Cache. Nach dem SELECT speichert der Core die Liste der IDs zusammen mit found_posts und max_num_pages in der Objekt-Cache-Gruppe post-queries. Der Schlüssel ist ein Hash aus den Argumenten und dem erzeugten SQL (WP_Query::generate_cache_key(), markiert mit @since 6.1.0 in class-wp-query.php). Das Argument cache_results schaltet den Mechanismus ein und aus; Standard ist true.
Drei Details aus dem Core-Quellcode entscheiden, ob dieser Cache Ihnen hilft:
- Die Invalidierung ist global. Der Schlüssel wird mit dem
last_changed-Wert der Gruppepostsgesalzen, bei einertax_queryzusätzlich mit dem vonterms. Das Speichern eines beliebigen Beitrags oder das Hinzufügen, Ändern oder Löschen eines beliebigen Post-Meta-Werts verschiebtlast_changed(der Core hängtwp_cache_set_posts_last_changed()anupdated_post_metaund die übrigen Meta-Actions indefault-filters.php). In einem Shop, in dem ein Import alle paar Minuten den Lagerbestand aktualisiert, bleibt die Trefferquote niedrig. - Manche Abfragen werden nie gecacht. Enthält
orderbyden AusdruckRAND(, umgeht die Abfrage den Cache; dasselbe gilt, wenn die Filterposts_fieldsoderposts_clausesdie abgefragten Felder verändert haben. - Ohne persistenten Objekt-Cache (Redis, Memcached) hält der Standard-
WP_Object_CacheDaten nur für den aktuellen Request. Der Ergebnis-Cache hilft dann nur, wenn dieselbe Abfrage in einem Seitenaufruf zweimal läuft.
Ein Grund mehr, einen ID-Pool zu holen und in PHP zu mischen, statt orderby => rand zu nutzen: Die Pool-Abfrage kann aus post-queries kommen, die zufällige nicht. Schalten Sie cache_results für einen Export oder ein CLI-Skript ab, wird auch der Beitrags-Cache nicht mit den zurückgegebenen Objekten befüllt.
Verwandte Beiträge in WordPress ohne Plugin
function wppoland_get_related_post_ids( int $post_id, int $limit = 3 ): array {
$terms = get_the_terms( $post_id, 'category' );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return [];
}
$term_ids = wp_list_pluck( $terms, 'term_id' );
$cache_key = 'related_' . $post_id . '_' . $limit;
$cached = wp_cache_get( $cache_key, 'wppoland_related' );
if ( false !== $cached ) {
return $cached;
}
$query = new WP_Query(
[
'post_type' => 'post',
'category__in' => $term_ids,
'post__not_in' => [ $post_id ],
'posts_per_page' => $limit * 4,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'fields' => 'ids',
'ignore_sticky_posts' => true,
]
);
$pool = $query->posts;
if ( empty( $pool ) ) {
wp_cache_set( $cache_key, [], 'wppoland_related', 10 * MINUTE_IN_SECONDS );
return [];
}
shuffle( $pool );
$picked = array_slice( $pool, 0, $limit );
wp_cache_set( $cache_key, $picked, 'wppoland_related', 10 * MINUTE_IN_SECONDS );
return $picked;
}Hier steht kein ORDER BY RAND() im SQL. Sie holen mit einer günstigen Abfrage eine größere ID-Menge, mischen sie in PHP und cachen das Ergebnis.
Checkliste zur WP_Query-Optimierung vor dem Go-live
- Brauchen Sie wirklich Paginierung? Wenn nicht:
no_found_rows. - Zeigen Sie Meta-Daten und Terme in der Schleife an? Wenn nicht: beide
update_post_*_cacheabschalten. - Reichen IDs? Dann
fields => idsund bewusstes Nachladen. - Nutzt
meta_querybei Zahlen und Datumswerten den passendentype? - Vermeiden Sie
LIKE '%...%'in den meistgenutzten Filtern? - Haben Sie die Abfrage in Query Monitor und mit
EXPLAINgemessen? - Lässt sich das Ergebnis im Objekt-Cache oder in einem Transient mit sinnvoller Invalidierung ablegen?







