LW IT Solutions
« Przegląd bloga /Wtyczki i triki WordPress/Tutorials / Tutorial: Jak znaleźć wolne meta_query i przepisać...
Ten artykuł w innych językach:

Tutorial: Jak znaleźć wolne meta_query i przepisać je na filtr taksonomii

Tutorial: Jak znaleźć wolne meta_query i przepisać je na filtr taksonomii
Spis treści
  1. Co WP_Query buduje z tablicy
  2. Znalezienie zapytania zamiast zgadywania
  3. Odczyt wyniku EXPLAIN
  4. Przeniesienie filtra do taksonomii
  5. Jednorazowe przeniesienie istniejących wartości
  6. Kiedy taksonomia jest złą odpowiedzią
  7. Pytania i odpowiedzi
  8. Źródła

Filtr napisany jako meta_query wygląda w PHP niewinnie. Trzy warunki, każdy jako tablica, a między nimi powiązanie. To, co dociera do MySQL, jest czymś zupełnie innym: trzy dodatkowe złączenia na tabeli zawierającej każde pole dodatkowe każdego wpisu w instalacji, której rozstrzygająca kolumna nie ma żadnego indeksu.

Poniższa przebudowa ma cztery kroki – odczytać SQL, zmierzyć go, przenieść filtr do taksonomii, jednorazowo przenieść wartości. Opłaca się dokładnie dla jednego rodzaju pola, a ostatni rozdział mówi, dla którego.

Porównanie dwóch łańcuchów złączeń: u góry trzy złączenia postmeta z około 480 000 przeglądanych wierszy, u dołu dwa złączenia taksonomii z około 2 100 przeglądanych wierszy
Ten sam filtr, dwa modele danych. Zmienia się nie liczba złączeń, lecz to, co MySQL potrafi osiągnąć przez indeks.

Co WP_Query buduje z tablicy

Zapytanie z trzema warunkami na polach dodatkowych pisze się krótko, a wykonuje długo.

$q = new WP_Query( [
    'post_type'  => 'produkt',
    'meta_query' => [
        'relation' => 'AND',
        [ 'key' => 'farbe',     'value' => 'blau' ],
        [ 'key' => 'material',  'value' => 'eiche' ],
        [ 'key' => 'verfuegbar','value' => '1' ],
    ],
] );

Każda klauzula staje się osobnym złączeniem z własnym aliasem, bo pojedynczy wiersz wp_postmeta niesie zawsze tylko jeden klucz. Trzy warunki potrzebują więc trzech kopii tabeli.

SELECT wp_posts.ID FROM wp_posts
  INNER JOIN wp_postmeta ON wp_posts.ID = wp_postmeta.post_id
  INNER JOIN wp_postmeta AS mt1 ON wp_posts.ID = mt1.post_id
  INNER JOIN wp_postmeta AS mt2 ON wp_posts.ID = mt2.post_id
WHERE wp_posts.post_type = 'produkt'
  AND ( wp_postmeta.meta_key = 'farbe'      AND wp_postmeta.meta_value = 'blau' )
  AND ( mt1.meta_key        = 'material'    AND mt1.meta_value        = 'eiche' )
  AND ( mt2.meta_key        = 'verfuegbar'  AND mt2.meta_value        = '1' )
GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC

Rozstrzygające stoi w ostatniej linii każdego warunku. wp_postmeta przynosi indeks na post_id oraz na meta_key, ale żadnego na meta_value – kolumna jest typu LONGTEXT i nie da się jej zaindeksować w całości. MySQL znajduje więc szybko wszystkie wiersze z kluczem farbe, a potem musi obejrzeć każdy z nich, aby sprawdzić wartość blau.

Jak drogie to będzie, zależy od liczby, której nikt nie obserwuje: ile wierszy dzieli ten sam klucz. Pole obecne przy każdym produkcie ma tyle wierszy, ile jest produktów – a trzy takie pola mnożą się w złączeniu.

Znalezienie zapytania zamiast zgadywania

Zanim cokolwiek zostanie przebudowane, zapytanie musi leżeć na stole. Dwie linie w wp-config.php każą WordPressowi zapisywać każdą wysłaną instrukcję.

define( 'SAVEQUERIES', true );

Potem da się wypisać najwolniejsze instrukcje jednego wywołania strony – wraz ze ścieżką wywołań, która je stworzyła, a to zwykle ciekawsza połowa.

add_action( 'shutdown', function () {
    global $wpdb;
    if ( ! defined( 'SAVEQUERIES' ) || ! SAVEQUERIES ) { return; }
    $langsam = array_filter( $wpdb->queries, fn( $q ) => $q[1] > 0.05 );
    usort( $langsam, fn( $a, $b ) => $b[1] <=> $a[1] );
    foreach ( array_slice( $langsam, 0, 5 ) as $q ) {
        error_log( sprintf( "%.3f s  %s\n   %s", $q[1], $q[2], substr( $q[0], 0, 400 ) ) );
    }
} );

Na działającej witrynie tę samą informację daje dziennik wolnych zapytań, całkiem bez ingerencji w kod – i to lepsze źródło, bo obejmuje także wywołania, których nikt nie obserwuje.

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.2;
SET GLOBAL log_queries_not_using_indexes = 'ON';

Odczyt wyniku EXPLAIN

Gdy instrukcja jest już znana, jedno polecenie mówi, co MySQL zamierza z nią zrobić.

EXPLAIN SELECT wp_posts.ID FROM wp_posts
  INNER JOIN wp_postmeta ON wp_posts.ID = wp_postmeta.post_id
  ...

Wynik niosą trzy kolumny. rows szacuje, ile wierszy zostaje obejrzanych w każdej tabeli, a iloczyn po wszystkich tabelach to z grubsza nakład pracy. key podaje użyty indeks – tutaj meta_key, nigdy meta_value. A w Extra stoją dwie uwagi warte reakcji: Using temporary i Using filesort.

Druga z nich ma własną przyczynę. Sortowanie po polu dodatkowym zamienia sortowanie w porównanie tekstu na kolumnie bez indeksu – dlatego dodane 'orderby' => 'meta_value_num' kosztuje często więcej niż wszystkie trzy filtry razem.

Obserwacja Znaczenie
key = meta_key Klucz zostaje znaleziony przez indeks, wartość nie
rows w dziesiątkach tysięcy Pole wisi przy niemal każdym wpisie – filtr niczego nie zawęża
Using filesort Sortowanie następuje po złączeniu, na całym wyniku
Using temporary GROUP BY wymaga tabeli tymczasowej, zwykle z powodu wielu złączeń

Przeniesienie filtra do taksonomii

To, że taksonomia jest szybsza, nie bierze się z jakiejś szczególnej przemyślności WordPressa. Bierze się stąd, że wp_term_relationships przechowuje w wierszu dwie liczby całkowite i ma klucz główny właśnie po tych dwóch kolumnach – złączenie biegnie więc z obu stron przez indeks.

add_action( 'init', function () {
    register_taxonomy( 'farbe', 'produkt', [
        'hierarchical'      => false,
        'public'            => false,
        'show_ui'           => true,
        'show_admin_column' => true,
        'query_var'         => false,
        'rewrite'           => false,
    ] );
} );

Dwa ustawienia zasługują na chwilę uwagi. public => false trzyma strony archiwum i adresy poza witryną, a właśnie tego chce czysta cecha filtrująca. A show_admin_column nic nie kosztuje i pokazuje wartości na liście wpisów – tam nieudane przeniesienie rzuca się w oczy najwcześniej.

Zapytanie po zmianie opisuje ten sam filtr innym słownictwem.

$q = new WP_Query( [
    'post_type' => 'produkt',
    'tax_query' => [
        'relation' => 'AND',
        [ 'taxonomy' => 'farbe',    'field' => 'slug', 'terms' => 'blau' ],
        [ 'taxonomy' => 'material', 'field' => 'slug', 'terms' => 'eiche' ],
    ],
] );

Słowo o trzecim warunku pierwotnego zapytania: pole typu tak-nie, jak verfuegbar, również nie należy do taksonomii. Dwa terminy na dziesięć tysięcy produktów oznaczają, że jeden termin niesie dziewięć tysięcy przypisań, a indeks na kolumnie o dwóch różnych wartościach jest niemal bezwartościowy. Taki znacznik lepiej mieści się w post_status, w osobnej taksonomii stanów obok innych stanów – albo zostaje w postmeta jako jedyna pozostała klauzula: jedno złączenie nie jest problemem, trzy są.

Jednorazowe przeniesienie istniejących wartości

Taksonomia istnieje, wpisy nic o niej jeszcze nie wiedzą. Jednorazowy przebieg przez WP-CLI przenosi wartości i daje się wcześniej sprawdzić.

wp eval '
$ids = get_posts( [ "post_type" => "produkt", "posts_per_page" => -1,
                    "fields" => "ids", "post_status" => "any" ] );
$n = 0;
foreach ( $ids as $id ) {
    $wert = get_post_meta( $id, "farbe", true );
    if ( $wert === "" ) { continue; }
    wp_set_object_terms( $id, sanitize_title( $wert ), "farbe", false );
    $n++;
}
echo "$n wpisow\n";
'

O powtarzalności tego przebiegu rozstrzygają trzy szczegóły. post_status => any obejmuje także szkice i wpisy zaplanowane – właśnie te, których brak ujawnia się później. false w wp_set_object_terms zastępuje przypisanie, zamiast je uzupełniać, dzięki czemu drugi przebieg daje ten sam stan, a nie rosnącą listę. A sanitize_title sprawia, że termin zostaje znaleziony po slugu, więc z Blau i blau powstaje jeden termin, a nie dwa.

Wiersze postmeta najlepiej zostawić jeszcze na jakiś czas. Gdy nowa droga podziała kilka tygodni, znikną jedną instrukcją – i dopiero wtedy, bo ten krok nie ma drogi powrotnej.

wp post meta delete --all --keys=farbe --dry-run

Kiedy taksonomia jest złą odpowiedzią

Przebudowa opłaca się przy polach o niewielu różnych wartościach, służących jako filtr. Dla trzech innych przypadków jest chwytem błędnym, a każdy z nich ma własną lepszą odpowiedź.

Najwyraźniejsze są zakresy liczbowe. Cena, data, waga – wszystko, o co pyta się przez BETWEEN albo porównanie, nie ma godnych uwagi terminów, a taksonomia z ośmioma tysiącami terminów jest gorsza od zastąpionego postmeta. Takie pole należy do własnej kolumny, we własnej tabeli z indeksem po dokładnie tej odpytywanej kombinacji.

Drugim przypadkiem jest wyszukiwanie pełnotekstowe. Terminy trafiają dokładnie albo wcale; szukanie części słowa wymaga indeksu FULLTEXT albo osobnego indeksu wyszukiwania, a żadne z nich nie jest taksonomią.

Trzecim przypadkiem są wartości wiszące tylko przy garstce wpisów. Pole przy dwudziestu z czterdziestu tysięcy produktów jest wystarczająco rozróżniające już przez sam meta_key, a zapytanie działa szybko bez żadnej zmiany. Narzędziem tej decyzji jest jedno zliczenie, wykonane przed każdą przebudową.

SELECT meta_key, COUNT(*) AS zeilen, COUNT(DISTINCT meta_value) AS werte
FROM wp_postmeta GROUP BY meta_key ORDER BY zeilen DESC LIMIT 20;

Wiele wierszy i mało różnych wartości – to przypadek, dla którego taksonomia powstała. Wiele wierszy i prawie tyle samo różnych wartości domaga się kolumny. Mało wierszy oznacza, że nie musi się dziać nic.

Pytania i odpowiedzi

Co się dzieje, gdy w tygodniach przejściowych nadal zapisuje się stare pole?

Wtedy pole i taksonomia po cichu się rozjeżdżają: zapytanie czyta już tylko taksonomię, a formularz, import albo rozszerzenie może nadal zapisywać stare pole. Rozstrzygające jest więc to, by w tym czasie wiodące było dokładnie jedno z dwóch miejsc przechowywania.

Jeśli wiodące pozostaje pole, bo wprowadzanie danych nie zostało jeszcze przestawione, wystarczy tuż przed usunięciem jeszcze raz uruchomić przebieg przenoszący. Jest do tego przygotowany, bo false w wp_set_object_terms zastępuje przypisanie, zamiast je uzupełniać. Zostaje jedna luka: przebieg pomija wpisy z pustą wartością, więc pole wyczyszczone w międzyczasie zostawia stary termin.

Jeśli natomiast wiodąca jest już taksonomia, bo wartości są utrzymywane w edytorze, przebiegu nie wolno powtarzać. Przy każdym wpisie, który nadal ma stare pole, nadpisałby nowsze przypisanie starą wartością. W takim przypadku należy najpierw przestawić wszystkie miejsca, które jeszcze zapisują pole, a dopiero potem usunąć wiersze postmeta.

Dlaczego nowej taksonomii nie widać w edytorze blokowym?

Bo w rejestracji brakuje ustawienia show_in_rest, które domyślnie ma wartość false; edytor blokowy odczytuje taksonomie przez interfejs REST i pokazuje tylko to, co jest tam udostępnione. Z 'show_in_rest' => true wybór pojawia się w edytorze blokowym. Terminy są wtedy jednak dostępne także pod /wp-json/wp/v2/farbe, czemu samo public => false nie zapobiega.

Czy przebudowa opłaca się także dla produktów WooCommerce?

Tylko dla części pól, bo w WooCommerce wiele z tego jest już zbudowane tak, jak zaleca artykuł. Pokazują to trzy miejsca:

  • Atrybuty globalne, zakładane w Produkty → Atrybuty, są taksonomiami z przedrostkiem pa_, na przykład pa_farbe, i da się je filtrować przez tax_query. Atrybuty zakładane tylko przy pojedynczym produkcie są natomiast przechowywane w postaci zserializowanej w jednym polu meta i nie nadają się na filtr.
  • Widoczność w katalogu i oznaczenie braku w magazynie niesie taksonomia product_visibility, czyli dokładnie taka taksonomia stanów, jaką artykuł proponuje dla pól typu tak-nie.
  • Cena, stan magazynowy i ocena są dodatkowo przechowywane we własnych kolumnach tabeli wc_product_meta_lookup, co odpowiada rozwiązaniu dla zakresów liczbowych z ostatniego rozdziału.

Przebudowa dotyczy tam więc głównie pól, które rozszerzenie albo motyw zakłada dodatkowo jako postmeta. Które to pola, pokazuje zliczenie z ostatniego rozdziału.

Ź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 (11) Śledź tę kategorię przez RSS

Data Privacy

Wszystkie artykuły w tej kategorii (18) Ś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 (17) Śledź tę kategorię przez RSS

Music Production

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

Raspberry Pi

Śledź tę kategorię przez RSS

SaaS & Internet Earning

Artykuły w przygotowaniu

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 (13) Śledź tę kategorię przez RSS