LW IT Solutions
« Przegląd bloga /Wtyczki i triki WordPress / meta_query w WP_Query: jak każda klauzula staje...
Ten artykuł w innych językach:

meta_query w WP_Query: jak każda klauzula staje się złączeniem i ile to kosztuje

meta_query w WP_Query: jak każda klauzula staje się złączeniem i ile to kosztuje
Spis treści
  1. Co WP_Query buduje z tablicy
  2. Indeks, którego nie ma
  3. Klauzule kosztujące więcej niż pozostałe
  4. Gdzie ten filtr należy zamiast tego
  5. Mierzyć zamiast zgadywać
  6. Pytania i odpowiedzi
  7. Źródła

Strona filtrująca wpisy po trzech polach własnych to w PHP jedna tablica, a w SQL cztery złączenia. Tablica czyta się jak filtr; SQL czyta się jak tabela odwiedzana ponownie dla każdego warunku – bo dokładnie tym jest.

Godne uwagi nie jest to, że działa wolno; większość witryn nigdy nie urośnie na tyle, by to rzucało się w oczy. Godne uwagi jest to, że koszt pozostaje niewidoczny w kodzie, który go wywołuje, i że rośnie od dwóch rzeczy naraz: od liczby klauzul i od rozmiaru tabeli, w którą nikt nie zagląda.

Schody etapów zapytania: zapytanie podstawowe czyta 4800 wierszy, każde z trzech złączeń meta_query dokłada 192000 wierszy, ten sam filtr jako taksonomia czyta 5120
Ten sam zbiór wyników, cztery drogi do niego. Pasek zapytania podstawowego nie brakuje – ma trzy piksele szerokości.

Co WP_Query buduje z tablicy

Każda klauzula w meta_query dostaje własny alias na wp_postmeta i własny warunek złączenia. Dwie klauzule, dwa aliasy; pięć klauzul, pięć. Tabelę łączy się z samą sobą tyle razy, ile jest warunków, a każde z tych złączeń ciągnie za sobą pełny ciężar tabeli.

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 = 'polnoc' )
  AND ( pm2.meta_key = '_poziom' AND pm2.meta_value = 'b' )
  AND ( pm3.meta_key = '_aktywny' AND pm3.meta_value = '1' )
ORDER BY wp_posts.post_date DESC

Nie ma w tym nic błędnego. To dosłowne tłumaczenie tego, o co poproszono, i baza odpowie poprawnie – po odczytaniu na średniej witrynie rzędu pół miliona wierszy, żeby zwrócić czterdzieści.

Indeks, którego nie ma

wp_postmeta niesie trzy indeksy: klucz główny na meta_id, jeden na post_id i jeden na pierwszych 191 znakach meta_key. Na meta_value nie ma żadnego i sensownie być nie może: kolumna jest typu LONGTEXT, a indeks na kolumnie tekstowej nieograniczonej długości to albo przedrostek, albo nic.

Każdą klauzulę obsługuje się więc w dwóch ruchach. Złączenie znajduje wiersze wpisu po post_id – szybko, z indeksu, dokładnie do tego indeks służy. Potem wartość porównuje się wewnątrz każdego z tych wierszy, jeden po drugim, bez pomocy skądkolwiek. Nakład jest proporcjonalny do liczby wierszy meta na wpis, a ta na witrynie z kreatorem stron i kilkoma rozszerzeniami rzadko spada poniżej trzydziestu.

Klauzule kosztujące więcej niż pozostałe

Klauzula Co musi za to zrobić baza
'compare' => '=' przypadek podstawowy: jedno złączenie, jedno porównanie napisów na wiersz
'compare' => 'LIKE' to samo z dopasowaniem wzorca; wiodący znak wieloznaczny wyklucza każdy skrót
'type' => 'NUMERIC' otacza kolumnę CAST() i zabiera ostatnią szansę na użycie indeksu
'compare' => 'NOT EXISTS' LEFT JOIN wraz z testem IS NULL – każdy niepasujący wiersz musi najpierw powstać
'relation' => 'OR' klauzule przestają się nawzajem zawężać; zbiór pośredni to suma
'orderby' => 'meta_value' tabela tymczasowa i sortowanie po kolumnie tekstowej

Ostatni wiersz zamienia wolną stronę w przekroczenie czasu. Sortowania po wartości meta nie da się obsłużyć z żadnego indeksu; baza buduje więc cały wynik pośredni i sortuje go – a ponieważ meta_value jest typu LONGTEXT, sortowanie to odbywa się na dysku, a nie w pamięci, gdy tylko zbiór urośnie.

NOT EXISTS zasługuje na osobne ostrzeżenie. Czyta się jak tanie przeciwieństwo EXISTS, a jest przeciwieństwem taniości: wykazanie nieobecności oznacza odwiedzenie każdego kandydata – i właśnie tego indeks skrócić nie potrafi.

Gdzie ten filtr należy zamiast tego

Wartość, po której się filtruje, nie jest własnością wpisu w tym sensie, w jakim jest nią podtytuł. Jest przyporządkowaniem, a dla przyporządkowań WordPress ma już strukturę z właściwymi indeksami: wp_term_relationships jest zaindeksowana na obu kolumnach, więc filtr po terminie to jedno złączenie obsłużone w całości z indeksów, całkiem bez porównywania tekstu.

Wynikająca stąd reguła jest krótka. To, po czym się filtruje albo sortuje, należy do taksonomii – albo, gdy wartości są naprawdę liczbowe i ciągłe, do kolumny własnej tabeli. To, co odczytuje się dopiero po znalezieniu wpisu, może zostać w metadanych, gdzie kosztuje dokładnie jeden dostęp po post_id i nic poza tym.

Mierzyć zamiast zgadywać

Zapytanie zbudowane przez WP_Query jest dostępne przed wykonaniem: w $query->request leży gotowy SQL, a przepuszczony przez EXPLAIN odpowiada na jedyne dwa pytania, które się liczą. Kolumna rows to oszacowanie, ile wierszy odczyta każdy krok; kolumna Extra nazywa kosztowne kształty wprost, a Using temporary; Using filesort razem są podpisem sortowania, które nie rośnie razem z witryną.

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

Trzy wiersze z Using where po dostępie przez post_id to dokładnie opisany wyżej obraz: indeks znalazł wiersze, warunek zastosowano potem. Liczba w rows, pomnożona przez złączenia, jest wielkością rosnącą razem z witryną – i warto ją znać tego dnia, w którym powstaje zapytanie, a nie tego, w którym strona przestaje się wczytywać.

Pytania i odpowiedzi

Czy podkreślnik na początku klucza, jak w _region, przyspiesza zapytanie?

Nie. Podkreślnik na początku oznacza tylko, że pole jest chronione, dlatego WordPress ukrywa je w edytorze w sekcji pól własnych. Dla bazy danych to znak jak każdy inny, a indeks na meta_key znajduje _region równie szybko jak region.

Czy trwała pamięć podręczna obiektów pomaga przy kosztownym meta_query?

Ukrywa koszt, zamiast go usuwać. Od WordPressa 6.1 WP_Query zapisuje znalezione numery wpisów w pamięci podręcznej obiektów, a przy trwałej pamięci podręcznej, takiej jak Redis, powtórzone identyczne zapytanie dostaje wtedy odpowiedź bez SQL. Klucz tej pozycji powstaje z zapytania, a pozycja traci ważność, gdy tylko zostanie zapisany jakikolwiek wpis, nie tylko któryś z wyniku.

Wynikają z tego dwie granice. Każda kombinacja filtrów to osobne zapytanie, a więc osobna pozycja: strona z trzema filtrami po pięć wartości ma 125 kombinacji, w których ustawione są wszystkie trzy, i każda z nich płaci pełną cenę przy pierwszym wywołaniu. A tam, gdzie wpisy są zapisywane bez przerwy, na przykład przez regularne importy, pozycje wygasają, zanim zdążą się opłacić.

Dla pomiaru oznacza to, że SQL z $query->request przepuszczony przez EXPLAIN pokazuje przypadek bez pamięci podręcznej, a to właśnie on się liczy, bo zachodzi przy każdej nowej kombinacji i po każdej zmianie. Pamięć podręczna stron przed WordPressem pomaga z tego samego powodu tylko w ograniczonym stopniu: każda kombinacja filtrów to osobny adres.

Źródła

Lukas Wójcik

Lukas Wójcik

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.

Napisanie komentarza

Doświadczenia z innymi wtyczkami lub środowiskami hostingowymi oraz pytania o konfigurację są tu mile widziane.

Adres e-mail nie jest publikowany. Pola obowiązkowe oznaczono gwiazdką.

Artykuły i kategorie

CCTV

Śledź tę kategorię przez RSS

Cloud & AI

Wszystkie artykuły w tej kategorii (17) Śledź tę kategorię przez RSS

Data Privacy

Wszystkie artykuły w tej kategorii (19) Śledź tę kategorię przez RSS

Digital Analytics

Wszystkie artykuły w tej kategorii (58) Śledź tę kategorię przez RSS

Digital Marketing

Wszystkie artykuły w tej kategorii (37) Śledź tę kategorię przez RSS

IT & Networks

Wszystkie artykuły w tej kategorii (18) Śledź tę kategorię przez RSS

Music Production

Wszystkie artykuły w tej kategorii (17) Śledź tę kategorię przez RSS

Raspberry Pi

Śledź tę kategorię przez RSS

SaaS & Internet Earning

Śledź tę kategorię przez RSS

Smart Home

Wszystkie artykuły w tej kategorii (18) Śledź tę kategorię przez RSS

Tworzenie stron internetowych

Wszystkie artykuły w tej kategorii (11) Śledź tę kategorię przez RSS

Wtyczki i triki WordPress

Wszystkie artykuły w tej kategorii (14) Śledź tę kategorię przez RSS