Wydajność bazy

Wolne zapytania bazy WordPress:
jak je przyspieszyć

Strona ładuje się 4 sekundy. Wszystko w cache. Obrazki zoptymalizowane. Baza nadal jest wąskim gardłem.

Dlaczego wolne zapytania SQL to ukryte wąskie gardło WordPress

WordPress odpala dziesiątki zapytań SQL na załadowanie strony. Większość kończy się poniżej 1 ms. Ale wystarczy jedno wolne zapytanie—jeden JOIN 800 ms na wp_postmeta—żeby cała strona czuła się zepsuta.

Schemat

PageSpeed mówi 95. Użytkownicy mówią „jest wolna”. Frontend szybki. Serwer bierze 2+ sekundy, zanim w ogóle zacznie słać HTML.

Najgorsze: WordPress domyślnie nie loguje wolnych zapytań. Brak ostrzeżenia, brak alertu w kokpicie. Zapytania cicho degradują, gdy baza rośnie—i zauważasz dopiero, gdy użytkownicy zaczynają narzekać.

Co powoduje wolne zapytania bazy WordPress

1. JOIN-y postmeta WooCommerce

WooCommerce trzyma dane produktów w wp_postmeta jako pary klucz-wartość. Filtrowanie po cenie, stanie albo atrybutach wymaga wielu JOIN-ów na nieindeksowanej tabeli:

SELECT p.ID FROM wp_posts p
INNER JOIN wp_postmeta pm1 ON p.ID = pm1.post_id
INNER JOIN wp_postmeta pm2 ON p.ID = pm2.post_id
WHERE pm1.meta_key = '_price' AND pm1.meta_value BETWEEN 10 AND 50
AND pm2.meta_key = '_stock_status' AND pm2.meta_value = 'instock'

Przy 10 000 produktów i 500 000 wierszy postmeta to zapytanie potrafi zająć 2–5 sekund bez porządnego indeksu.

2. Lookupy meta_key bez indeksu

Domyślna tabela wp_postmeta ma indeks tylko na post_id. Każde zapytanie filtrujące po meta_key + meta_value robi pełny skan tabeli:

SELECT post_id FROM wp_postmeta
WHERE meta_key = '_thumbnail_id'
-- Full table scan on 500K+ rows

3. Zapytania LIKE na dużych kolumnach tekstowych

Wtyczki wyszukiwania i wyszukiwanie w adminie często odpalają LIKE '%term%' na post_content. To omija każdy indeks i skanuje całą kolumnę:

SELECT ID FROM wp_posts
WHERE post_content LIKE '%shipping policy%'
-- Scans every row, every time

4. Zapytania COUNT ze złożonym WHERE

Tabele list admina odpalają COUNT do paginacji. W połączeniu z filtrami taksonomii i meta query bywają zaskakująco drogie na dużych stronach.

Jak znaleźć wolne zapytania w WordPress

  1. Włącz SAVEQUERIES — Dodaj define('SAVEQUERIES', true); do wp-config.php. WordPress zaloguje każde zapytanie z timingiem i funkcją wołającą. Sprawdź $wpdb->queries po załadowaniu strony.
  2. Sprawdź log wolnych zapytań MySQL — Jeśli masz dostęp do serwera: SET GLOBAL slow_query_log = 'ON'; i SET GLOBAL long_query_time = 0.5;, żeby łapać zapytania powyżej 500 ms.
  3. Użyj EXPLAIN na podejrzanych zapytaniach — Poprzedź wolne zapytanie EXPLAIN, żeby zobaczyć plan. Szukaj type: ALL (pełny skan tabeli) i rows: w setkach tysięcy.
  4. Profiluj konkretną stronę — Query Monitor albo tymczasowo SAVEQUERIES. Sortuj po czasie. Top 3 zapytania zwykle to 80% TTFB.
  5. Sprawdź przy szczytowym ruchu — Wolne zapytania „w porządku” przy 10 użytkownikach równolegle stają się katastrofą przy 100. Testuj pod obciążeniem, nie tylko na deweloperce.

Czytanie planu EXPLAIN na prawdziwym zapytaniu WordPress

EXPLAIN to najbliższe debuggerowi, co ma MySQL. Poprzedzasz nim SELECT, a MySQL mówi, jak planuje wykonać zapytanie — które tabele skanuje, które indeksy używa, ile wierszy spodziewa się ruszyć. Oto zapytanie filtra WooCommerce z góry, z typowym ORDER BY, tak jak leci na prawdziwej stronie sklepu:

EXPLAIN SELECT p.ID FROM wp_posts p
INNER JOIN wp_postmeta pm1 ON p.ID = pm1.post_id
INNER JOIN wp_postmeta pm2 ON p.ID = pm2.post_id
WHERE pm1.meta_key = '_price' AND pm1.meta_value BETWEEN 10 AND 50
  AND pm2.meta_key = '_stock_status' AND pm2.meta_value = 'instock'
ORDER BY p.post_date DESC LIMIT 20;

Na sklepie z ~800k wierszy postmeta plan wraca tak:

idselect_typetabletypepossible_keyskeyrowsExtra
1SIMPLEpm1ALLpost_id, meta_keyNULL812441Using where; Using temporary; Using filesort
1SIMPLEpeq_refPRIMARYPRIMARY1Using where
1SIMPLEpm2refpost_id, meta_keypost_id4Using where

Pierwszy wiersz to cały problem. Oto jak go czytać:

  • type: ALL — pełny skan tabeli. MySQL czyta każdy wiersz w wp_postmeta i sprawdza WHERE na każdym. Na 812k wierszy, przy każdym załadowaniu strony. To najgorszy typ dostępu, jaki EXPLAIN potrafi pokazać.
  • key: NULL — nie użyto indeksu. possible_keys listuje meta_key, więc indeks istnieje — MySQL go po prostu odrzucił. Dlaczego? Bo '_price' pasuje do ogromnej części tabeli (każdy produkt go ma), więc optymalizator uznał, że skan jest tańszy niż indeks, który prawie nic nie filtruje. Indeks nie brakuje, jest bezużyteczny dla tego zapytania.
  • Using temporary — MySQL musi zbudować wewnętrzną tabelę tymczasową na wyniki pośrednie, zanim posortuje. Na dużych zbiorach ta tabela wylewa się z pamięci na dysk, a czas zapytania idzie z milisekund w sekundy.
  • Using filesort — ORDER BY nie da się zaspokoić żadnym indeksem, więc MySQL sortuje ręcznie. Nazwa myli — nie zawsze idzie na dysk — ale z 812k skanowanych wierszy znaczy sortowanie ogromnej kupy danych przy każdym żądaniu.

Poprawka to indeks, którego optymalizator naprawdę chce: taki, który filtruje meta_key AND meta_value razem, żeby '_price' + zakres wartości zwężał do kilku tysięcy wierszy zamiast połowy tabeli.

ALTER TABLE wp_postmeta
ADD INDEX wpmt_key_value (meta_key(191), meta_value(32));

Odpal EXPLAIN jeszcze raz, a pierwszy wiersz zmienia się kompletnie:

idselect_typetabletypepossible_keyskeyrowsExtra
1SIMPLEpm1rangepost_id, meta_key, wpmt_key_valuewpmt_key_value2874Using where
1SIMPLEpeq_refPRIMARYPRIMARY1Using where
1SIMPLEpm2refpost_id, meta_key, wpmt_key_valuewpmt_key_value2Using where

type: range znaczy, że MySQL chodzi tylko po wpisach indeksu między dwoma granicami ceny — 2 874 wiersze zamiast 812 441. To 280× mniej roboty, zanim zapytanie w ogóle ruszy sort. Lookup równości na pm2 dostaje type: ref z tego samego indeksu, po 2 wiersze na produkt. Filesort może nadal być w Extra, ale sortowanie 20 kandydatów to szum; sortowanie 800k było awarią.

To cała umiejętność: odpal EXPLAIN, patrz na type, key i rows dla każdej tabeli i napraw wiersz, który robi najwięcej roboty. Slow Query Analyzer WP Multitool robi tę analizę za ciebie — lokalnie, na każdym złapanym wolnym zapytaniu — i mówi, który indeks dodać.

Benchmarki wydajności zapytań WordPress

Nie wszystkie wolne zapytania są równe. Oto cele:

<50ms
Na zapytanie (zdrowo)
<30
Zapytania na stronę
<200ms
Łączny czas bazy

Jeśli pojedyncze zapytanie bierze powyżej 100 ms, warto badać. Jeśli łączny czas bazy przekracza 500 ms, użytkownicy to czują—nawet z page cache.

Jak przyspieszyć zapytania bazy WordPress (praktyczne poprawki)

Znalezienie wolnego zapytania to połowa roboty. Jedna zasada przed wszystkim: optymalizuj zapytanie, nie objaw. Nie dokładaj indeksów na ślepo — zrozum dlaczego zapytanie jest wolne. Czasem poprawka to przebudowa danych (własne tabele zamiast postmeta). Czasem unikanie zapytania w ogóle (cache transjentów na drogie agregacje). Oto co naprawdę przyspiesza zapytania, w kolejności, w jakiej bym próbował.

1. Dodaj indeksy, o które prosi plan EXPLAIN

Indeks, na który wpada powyższy walkthrough EXPLAIN, dodaję pierwszy — złożony indeks na wp_postmeta obejmujący obie kolumny, po których filtrują meta query WordPress:

ALTER TABLE wp_postmeta
ADD INDEX wpmt_key_value (meta_key(191), meta_value(32));

Pokrycie meta_key i meta_value razem pozwala MySQL zwęzić oba warunki w jednym przejściu indeksu. W walkthrough zamieniło type: ALL na type: range i obcięło badane wiersze z 812 441 do 2 874. Długości prefiksu (191) i (32) trzymają indeks zwarty i nadal pokrywają prawdziwe lookupy. Nie dokładaj indeksów na wiarę: najpierw EXPLAIN, potem ten, którego brakuje w planie.

Na boku: w sieci polecają indeks jednokolumnowy na meta_value. Nie pomoże zapytaniu filtrującemu meta_key i meta_value razem — złożony indeks powyżej to pokrywa, więc zacznij stamtąd.

2. Przyspiesz zapytania produktów i zamówień WooCommerce

Omówione wcześniej JOIN-y postmeta WooCommerce to klasyka: każdy warunek meta_query dokłada JOIN na 4-kolumnowej tabeli EAV. Przestań filtrować po postmeta, gdy możesz zdenormalizować. Dla gorących pól, po których filtrujesz non stop — cena, stan, ocena — skopiuj wartość do dedykowanej tabeli lookup z porządnymi kolumnami i indeksami. WooCommerce wozi wc_product_meta_lookup właśnie po to; użyj jej albo zbuduj ten sam wzorzec dla własnych pól.

3. Przestań z LIKE '%term%' i nieograniczonymi WP_Query

Wiodący wildcard nigdy nie użyje indeksu B-tree — MySQL skanuje każdy wiersz, zawsze. Dodaj indeks FULLTEXT na post_content i pytaj MATCH() AGAINST(), albo wynieś wyszukiwanie ze tabeli posts do dedykowanej wtyczki albo zewnętrznego silnika. Cokolwiek bije skan longtext.

Nigdy nie pytaj z posts_per_page => -1. Nieograniczone zapytania działają przy 200 wpisach i padają przy 20 000. Ustaw prawdziwy limit i paginuj, albo batchuj offsetami w cronie. Wolne zapytanie, którego nie odtworzysz lokalnie, to zwykle jedno z tych na danych rozmiaru produkcji.

I unikaj lasów OR w meta_query. Zagnieżdżone OR na kilku meta kluczach wpychają MySQL w szerokie skany i tabele tymczasowe. Lepiej jedno celowane zapytanie, zdenormalizowana kolumna flagi albo dwa tanie zapytania złączone w PHP niż jeden potwór meta_query.

4. Cache’uj agregaty i zabij pętle meta N+1

Zacznij od object cache. Redis albo Memcached trzyma wyniki zapytań w pamięci — powtórzone zapytania biją w cache zamiast w MySQL. To pojedyncza zmiana o największym wpływie na większości stron.

Potem sam cache’uj drogie agregaty. COUNT() ze złożonym WHERE, widgety „wpisy w tym miesiącu”, liczby termów — nie muszą być świeże przy każdym żądaniu. Policz raz, trzymuj w transjencie albo object cache, odświeżaj wg harmonogramu albo na save_post. COUNT 900 ms raz na godzinę nic nie kosztuje.

Potem obetnij, co ciągnie WP_Query. Jeśli potrzebujesz tylko ID, powiedz: 'fields' => 'ids' pomija ładowanie pełnych obiektów wierszy. 'no_found_rows' => true odcina przebieg SQL_CALC_FOUND_ROWS, gdy nie paginujesz. 'update_post_meta_cache' => false i 'update_post_term_cache' => false pomijają priming cache, gdy nie ruszasz meta ani termów — ale zostaw priming meta-cache WŁĄCZONY, gdy będziesz czytał meta w pętli, bo to jedno zapytanie primingowe ratuje cię przed get_post_meta() na każdy wpis.

Na koniec obetnij zapytanie, które WordPress odpala przed wszystkimi innymi. Każde żądanie zaczyna od załadowania wszystkich opcji autoload, zanim poleci twoje pierwsze zapytanie. Naprawa rozdęcia opcji autoload przyspiesza każdą stronę, nie tylko wolne.

5. Jak wygląda „szybciej” (benchmarki po każdej poprawce)

Stosuj po jednej poprawce i mierz po każdej: EXPLAIN plus timing. Cele to powyższe benchmarki — poniżej 50 ms na zapytanie, poniżej 200 ms łącznego czasu bazy na stronę. W walkthrough EXPLAIN jeden złożony indeks obciął skanowane wiersze z 812 441 do 2 874; taka jest skala zmiany od właściwego indeksu. Jeśli poprawka nie rusza planu EXPLAIN ani timingu, cofnij i idź do następnej.

Log wolnych zapytań kontra Query Monitor kontra detekcja automatyczna

Są trzy sposoby złapać wolne zapytanie. Odpowiadają na różne pytania:

Podejście Potrzebny dostęp do serwera Działa non stop Łapie stack trace Sugeruje poprawki indeksów Bezpieczne na produkcji
Ręcznie (SAVEQUERIES / log wolnych MySQL) Tak — my.cnf albo prawa SET GLOBAL Log MySQL potrafi, ale nikt go nie czyta, aż coś padnie Nie — dostajesz SQL, nie PHP, które je odpaliło Nie — odpalasz EXPLAIN i interpretujesz sam SAVEQUERIES nie (narzut pamięci na każde żądanie); log MySQL tak, przy rozsądnym progu
Wtyczka Query Monitor Nie Nie — pokazuje bieżące załadowanie strony, tylko dla zalogowanych adminów Tak — pełny stos wołającego, jego najlepsza funkcja Nie Do developmentu. Można trzymać na prodzie, ale widzi tylko żądania, które sam odpalasz, patrząc na nie — nie złapie skoku o 3 w nocy
Slow Query Analyzer WP Multitool Nie — działa na hostingu współdzielonym Tak — loguje każde zapytanie powyżej progu, na prawdziwym ruchu, całą dobę Tak — plik wtyczki albo motywu, który odpalił zapytanie Tak — odpala EXPLAIN lokalnie i wskazuje brakujący indeks Tak — zaprojektowane do działania na żywych stronach z minimalnym narzutem

Query Monitor naprawdę jest dobry w tym, co robi — sam go używam. Po prostu odpowiada na inne pytanie. Mówi, co zrobiło to załadowanie strony, gdy patrzysz; nie powie, co strona robiła w zeszły wtorek pod obciążeniem. Tę lukę wypełnia ciągła detekcja.

Dlaczego wolne zapytania wracają

Jedno znalezienie wolnych zapytań nie wystarczy. Wracają:

  1. Aktualizacje wtyczek wnoszą nowe zapytania albo zmieniają istniejące
  2. Baza rośnie—zapytanie szybkie przy 10K wierszy jest wolne przy 500K
  3. Nowe typy treści dokładają wiersze postmeta i taksonomii
  4. Sprzedaż WooCommerce zbiera dane zamówień w tych samych tabelach

Ręczne sprawdzenia SAVEQUERIES są żmudne i łatwo o nich zapomnieć. Jeśli wolne zapytania też sprawiają, że twój admin WordPress wydaje się wolny, problem się kumuluje. Potrzebujesz ciągłego monitoringu, który łapie regresje w chwili, gdy się dzieją—nie po zgłoszeniach użytkowników.

Wolne zapytania WordPress: FAQ

Jak znaleźć wolne zapytania w WordPress?
Trzy sposoby: włącz log wolnych zapytań MySQL z long_query_time ok. 0,5 s (wymaga dostępu do serwera), zainstaluj Query Monitor i oglądaj ładowania zalogowany, albo odpal wtyczkę monitorującą, która loguje wolne zapytania non stop na prawdziwym ruchu. Pierwsze dwa łapią zapytania, które sam odpalisz; tylko ciągły monitoring łapie wolne zapytania, w które wpadają odwiedzający, gdy nie patrzysz.
Jak przyspieszyć zapytania bazy WordPress?
Nie potrzebujesz przepisania — większość poprawek to celowane zmiany. Najpierw EXPLAIN na wolnym zapytaniu: mówi, czy MySQL skanuje całą tabelę (type: ALL, key: NULL). Większość wolnych zapytań WordPress naprawia właściwy indeks, zwykle złożony na wp_postmeta pokrywający meta_key i meta_value. Poza indeksami: object cache (Redis albo Memcached) przed MySQL, stop nieograniczonym zapytaniom typu posts_per_page => -1, zamień wyszukiwania LIKE '%term%' na FULLTEXT, cache’uj drogie COUNT-y i napraw rozdęcie opcji autoload, które dokłada latencję do każdego zapytania, zanim w ogóle poleci.
Jaki jest dobry czas zapytania bazy w WordPress?
Pojedyncze zapytania powinny zostać poniżej 50 ms — większość dobrze zindeksowanych leci poniżej 5 ms. Typowa strona powinna potrzebować mniej niż 30 zapytań i spędzić poniżej 200 ms łącznie w bazie. Jeśli jedno zapytanie bierze powyżej 500 ms, warto EXPLAIN; powyżej 1 s aktywnie rujnuje TTFB przy każdym niecache’owanym ładowaniu.
Czy page cache naprawia wolne zapytania bazy?
Nie — je chowa. Odwiedzający z cache dostają szybki HTML, ale każdy cache miss, zalogowany, koszyk i żądanie admina i tak płaci pełny koszt zapytania. Wolne zapytanie nadal tam jest, pali CPU i wraca, gdy ruch skoczy albo cache się wyczyści. Cache warto mieć, ale to warstwa na naprawionej bazie, nie zamiennik.

Przestań zgadywać. Zacznij logować.

Slow Query Analyzer WP Multitool loguje każde zapytanie powyżej progu, łapie pełny stack trace i sugeruje konkretne poprawki indeksów. Bez dostępu do serwera.

Weź WP Multitool Poradnik wydajności backendu