[{"data":1,"prerenderedAt":2950},["ShallowReactive",2],{"wpis-zapytania-n-plus-1-filtry-wordpress":3},{"id":4,"title":5,"body":6,"date":2936,"description":2937,"extension":2938,"kategoria":2939,"meta":2940,"navigation":166,"obrazek":2941,"obrazekAlt":2942,"path":2943,"seo":2944,"sitemap":2945,"stem":2946,"szkic":2947,"zaktualizowano":2948,"__hash__":2949},"wpisy/zapytania-n-plus-1-filtry-wordpress.md","Zapytania N+1 w filtrach WordPressa — jak je znaleźć i naprawić",{"type":7,"value":8,"toc":2920},"minimark",[9,18,27,32,35,42,78,84,266,280,288,299,306,310,313,855,866,877,893,896,910,914,921,927,930,934,945,1297,1308,1323,1326,1558,1564,1586,1592,1599,1604,1623,1626,1678,1691,1734,1755,1762,1768,1790,1793,1805,1809,1822,1836,1840,1843,2182,2192,2199,2213,2220,2229,2236,2240,2243,2410,2420,2427,2430,2635,2655,2665,2687,2690,2739,2762,2766,2769,2775,2784,2790,2794,2797,2819,2832,2845,2849,2855,2867,2870,2876,2891,2904,2907,2916],[10,11,12,13,17],"p",{},"Filtr ",[14,15,16],"code",{},"wp_nav_menu_objects",", który dobudowuje menu z kategorii rekurencyjnie, wykonuje osobne zapytanie dla każdego poziomu drzewa i drugie dla każdej pozycji — tylko po to, żeby sprawdzić, czy ma dzieci. Przy stu kategoriach to kilkaset zapytań SQL przy każdym wyświetleniu dowolnej strony witryny. Poprawka: pobrać wszystkie terminy jednym zapytaniem, złożyć drzewo w PHP i wynik odłożyć w transiencie.",[10,19,20,21,26],{},"Ten kod znalazłem w motywie na koncie, które diagnozowałem z powodu ",[22,23,25],"a",{"href":24},"/limit-cpu-hosting-wspoldzielony-wordpress/","przekroczeń limitu CPU",". Z tamtym problemem nie miał nic wspólnego i to jest osobna historia. Warto go było naprawić z własnego powodu: strona generowała się zauważalnie dłużej, niż powinna.",[28,29,31],"h2",{"id":30},"skąd-wiadomo-że-to-n1","Skąd wiadomo, że to N+1",[10,33,34],{},"N+1 to jedno zapytanie po listę i po jednym dodatkowym dla każdego elementu z tej listy. Rośnie liniowo z liczbą danych, więc na instalacji deweloperskiej z pięcioma kategoriami jest niewidoczne, a na produkcji ze stoma zjada sekundę.",[10,36,37,38,41],{},"Najszybszy pomiar bez wtyczek — stała w ",[14,39,40],{},"wp-config.php",":",[43,44,49],"pre",{"className":45,"code":46,"language":47,"meta":48,"style":48},"language-php shiki shiki-themes one-dark-pro","define('POMIAR_ZAPYTAN', true);\n","php","",[14,50,51],{"__ignoreMap":48},[52,53,56,60,64,68,71,75],"span",{"class":54,"line":55},"line",1,[52,57,59],{"class":58},"sjrmR","define",[52,61,63],{"class":62},"sn6KH","(",[52,65,67],{"class":66},"subq3","'POMIAR_ZAPYTAN'",[52,69,70],{"class":62},",",[52,72,74],{"class":73},"sVC51"," true",[52,76,77],{"class":62},");\n",[10,79,80,81,41],{},"i jedna funkcja w ",[14,82,83],{},"functions.php",[43,85,87],{"className":45,"code":86,"language":47,"meta":48,"style":48},"add_action('shutdown', function (): void {\n  if (!defined('POMIAR_ZAPYTAN') || !POMIAR_ZAPYTAN) {\n    return;\n  }\n\n  error_log(sprintf(\n    '%s — %d zapytań, %s s',\n    sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? '-')),\n    get_num_queries(),\n    timer_stop(0, 3)\n  ));\n});\n",[14,88,89,116,146,155,161,168,182,191,226,235,254,260],{"__ignoreMap":48},[52,90,91,95,97,100,102,106,109,113],{"class":54,"line":55},[52,92,94],{"class":93},"sVbv2","add_action",[52,96,63],{"class":62},[52,98,99],{"class":66},"'shutdown'",[52,101,70],{"class":62},[52,103,105],{"class":104},"seHd6"," function",[52,107,108],{"class":62}," ():",[52,110,112],{"class":111},"sU0A5"," void",[52,114,115],{"class":62}," {\n",[52,117,119,122,125,128,130,132,135,138,141,144],{"class":54,"line":118},2,[52,120,121],{"class":104},"  if",[52,123,124],{"class":62}," (",[52,126,127],{"class":58},"!defined",[52,129,63],{"class":62},[52,131,67],{"class":66},[52,133,134],{"class":62},")",[52,136,137],{"class":58}," ||",[52,139,140],{"class":58}," !",[52,142,143],{"class":62},"POMIAR_ZAPYTAN)",[52,145,115],{"class":62},[52,147,149,152],{"class":54,"line":148},3,[52,150,151],{"class":104},"    return",[52,153,154],{"class":62},";\n",[52,156,158],{"class":54,"line":157},4,[52,159,160],{"class":62},"  }\n",[52,162,164],{"class":54,"line":163},5,[52,165,167],{"emptyLinePlaceholder":166},true,"\n",[52,169,171,174,176,179],{"class":54,"line":170},6,[52,172,173],{"class":58},"  error_log",[52,175,63],{"class":62},[52,177,178],{"class":58},"sprintf",[52,180,181],{"class":62},"(\n",[52,183,185,188],{"class":54,"line":184},7,[52,186,187],{"class":66},"    '%s — %d zapytań, %s s'",[52,189,190],{"class":62},",\n",[52,192,194,197,199,202,204,208,211,214,217,220,223],{"class":54,"line":193},8,[52,195,196],{"class":93},"    sanitize_text_field",[52,198,63],{"class":62},[52,200,201],{"class":93},"wp_unslash",[52,203,63],{"class":62},[52,205,207],{"class":206},"sVyAn","$_SERVER",[52,209,210],{"class":62},"[",[52,212,213],{"class":66},"'REQUEST_URI'",[52,215,216],{"class":62},"]",[52,218,219],{"class":62}," ??",[52,221,222],{"class":66}," '-'",[52,224,225],{"class":62},")),\n",[52,227,229,232],{"class":54,"line":228},9,[52,230,231],{"class":93},"    get_num_queries",[52,233,234],{"class":62},"(),\n",[52,236,238,241,243,246,248,251],{"class":54,"line":237},10,[52,239,240],{"class":93},"    timer_stop",[52,242,63],{"class":62},[52,244,245],{"class":73},"0",[52,247,70],{"class":62},[52,249,250],{"class":73}," 3",[52,252,253],{"class":62},")\n",[52,255,257],{"class":54,"line":256},11,[52,258,259],{"class":62},"  ));\n",[52,261,263],{"class":54,"line":262},12,[52,264,265],{"class":62},"});\n",[10,267,268,269,272,273,275,276,279],{},"Wynik ląduje w ",[14,270,271],{},"error_log"," obok ",[14,274,40],{}," albo w logu skonfigurowanym przez ",[14,277,278],{},"WP_DEBUG_LOG",". Odśwież kilka podstron i porównaj liczby. Zdrowa strona na WordPressie z kilkoma wtyczkami mieści się w przedziale 30–80 zapytań. Trzysta na każdej podstronie oznacza, że coś liczy w pętli.",[10,281,282,283,287],{},"Dwie uwagi do tego licznika. Po włączeniu stałej ",[284,285,286],"strong",{},"loguje każde żądanie",", także boty, więc zostawiony na dobę sam urośnie w problem — zapal go na kilka minut i zgaś.",[10,289,290,291,294,295,298],{},"I rzecz ważniejsza: mierz ",[284,292,293],{},"wyjście anonimowe",", nie własną sesję. Odruch podpowiada bramkę na ",[14,296,297],{},"current_user_can('manage_options')",", ale zalogowany administrator ma pasek administracyjny, sprawdzanie uprawnień, powiadomienia o aktualizacjach i omija cache stron — to dwadzieścia parę zapytań ponad to, co widzi zwykły odwiedzający. Do porównania „przed i po\" różnica się skraca, ale liczby bezwzględne przestają się odnosić do przedziału wyżej. Otwórz stronę w oknie prywatnym.",[10,300,301,302,305],{},"Gdy już wiesz, że jest źle, do znalezienia miejsca służy ",[284,303,304],{},"Query Monitor",". Zakładka „Queries\" grupuje zapytania po funkcji wywołującej i po komponencie, więc filtr motywu wychodzi na wierzch bez czytania kodu. Wtyczkę instalujesz na czas diagnozy i zdejmujesz po niej — sama dokłada kilkanaście zapytań.",[28,307,309],{"id":308},"filtr-menu-co-robił-kod","Filtr menu: co robił kod",[10,311,312],{},"Motyw miał w menu jedną pozycję wskazującą na kategorię z własnej taksonomii, a pod nią miało się rozwijać całe drzewo podkategorii. Autor rozwiązał to filtrem, który dla każdego węzła pyta bazę o jego dzieci:",[43,314,316],{"className":45,"code":315,"language":47,"meta":48,"style":48},"add_filter('wp_nav_menu_objects', 'motyw_dobuduj_kategorie');\n\nfunction motyw_dobuduj_kategorie(array $pozycje): array {\n  foreach ($pozycje as $pozycja) {\n    if ($pozycja->object !== 'oferta_kategoria') {\n      continue;\n    }\n\n    $pozycje = array_merge($pozycje, motyw_galezie((int) $pozycja->object_id, $pozycja->ID));\n  }\n\n  return $pozycje;\n}\n\nfunction motyw_galezie(int $rodzic, int $rodzicMenu): array {\n  $dzieci = get_terms([\n    'taxonomy'   => 'oferta_kategoria',\n    'parent'     => $rodzic,\n    'hide_empty' => false,\n  ]);\n\n  $wynik = [];\n\n  foreach ($dzieci as $dziecko) {\n    $wnuki = get_terms([\n      'taxonomy'   => 'oferta_kategoria',\n      'parent'     => $dziecko->term_id,\n      'hide_empty' => false,\n      'fields'     => 'ids',\n    ]);\n\n    $pozycja = motyw_pozycja_menu($dziecko, $rodzicMenu);\n    $pozycja->classes[] = $wnuki ? 'menu-item-has-children' : '';\n    $pozycja->url = get_term_link($dziecko);\n\n    $wynik[] = $pozycja;\n    $wynik = array_merge($wynik, motyw_galezie($dziecko->term_id, $pozycja->ID));\n  }\n\n  return $wynik;\n}\n",[14,317,318,335,339,362,381,404,411,416,420,468,472,476,485,491,496,524,538,551,564,578,584,589,600,605,622,634,646,664,676,689,695,700,721,754,775,780,794,830,835,840,850],{"__ignoreMap":48},[52,319,320,323,325,328,330,333],{"class":54,"line":55},[52,321,322],{"class":93},"add_filter",[52,324,63],{"class":62},[52,326,327],{"class":66},"'wp_nav_menu_objects'",[52,329,70],{"class":62},[52,331,332],{"class":66}," 'motyw_dobuduj_kategorie'",[52,334,77],{"class":62},[52,336,337],{"class":54,"line":118},[52,338,167],{"emptyLinePlaceholder":166},[52,340,341,344,347,349,352,355,358,360],{"class":54,"line":148},[52,342,343],{"class":104},"function",[52,345,346],{"class":93}," motyw_dobuduj_kategorie",[52,348,63],{"class":62},[52,350,351],{"class":111},"array",[52,353,354],{"class":206}," $pozycje",[52,356,357],{"class":62},"): ",[52,359,351],{"class":111},[52,361,115],{"class":62},[52,363,364,367,369,372,375,378],{"class":54,"line":157},[52,365,366],{"class":104},"  foreach",[52,368,124],{"class":62},[52,370,371],{"class":206},"$pozycje",[52,373,374],{"class":62}," as ",[52,376,377],{"class":206},"$pozycja",[52,379,380],{"class":62},") {\n",[52,382,383,386,388,390,393,396,399,402],{"class":54,"line":163},[52,384,385],{"class":104},"    if",[52,387,124],{"class":62},[52,389,377],{"class":206},[52,391,392],{"class":62},"->",[52,394,395],{"class":206},"object",[52,397,398],{"class":58}," !==",[52,400,401],{"class":66}," 'oferta_kategoria'",[52,403,380],{"class":62},[52,405,406,409],{"class":54,"line":170},[52,407,408],{"class":104},"      continue",[52,410,154],{"class":62},[52,412,413],{"class":54,"line":184},[52,414,415],{"class":62},"    }\n",[52,417,418],{"class":54,"line":193},[52,419,167],{"emptyLinePlaceholder":166},[52,421,422,425,428,431,433,435,437,440,443,446,448,451,453,456,458,460,462,465],{"class":54,"line":228},[52,423,424],{"class":206},"    $pozycje",[52,426,427],{"class":58}," =",[52,429,430],{"class":58}," array_merge",[52,432,63],{"class":62},[52,434,371],{"class":206},[52,436,70],{"class":62},[52,438,439],{"class":93}," motyw_galezie",[52,441,442],{"class":62},"((",[52,444,445],{"class":104},"int",[52,447,134],{"class":62},[52,449,450],{"class":206}," $pozycja",[52,452,392],{"class":62},[52,454,455],{"class":206},"object_id",[52,457,70],{"class":62},[52,459,450],{"class":206},[52,461,392],{"class":62},[52,463,464],{"class":206},"ID",[52,466,467],{"class":62},"));\n",[52,469,470],{"class":54,"line":237},[52,471,160],{"class":62},[52,473,474],{"class":54,"line":256},[52,475,167],{"emptyLinePlaceholder":166},[52,477,478,481,483],{"class":54,"line":262},[52,479,480],{"class":104},"  return",[52,482,354],{"class":206},[52,484,154],{"class":62},[52,486,488],{"class":54,"line":487},13,[52,489,490],{"class":62},"}\n",[52,492,494],{"class":54,"line":493},14,[52,495,167],{"emptyLinePlaceholder":166},[52,497,499,501,503,505,507,510,513,515,518,520,522],{"class":54,"line":498},15,[52,500,343],{"class":104},[52,502,439],{"class":93},[52,504,63],{"class":62},[52,506,445],{"class":111},[52,508,509],{"class":206}," $rodzic",[52,511,512],{"class":62},", ",[52,514,445],{"class":111},[52,516,517],{"class":206}," $rodzicMenu",[52,519,357],{"class":62},[52,521,351],{"class":111},[52,523,115],{"class":62},[52,525,527,530,532,535],{"class":54,"line":526},16,[52,528,529],{"class":206},"  $dzieci",[52,531,427],{"class":58},[52,533,534],{"class":93}," get_terms",[52,536,537],{"class":62},"([\n",[52,539,541,544,547,549],{"class":54,"line":540},17,[52,542,543],{"class":66},"    'taxonomy'",[52,545,546],{"class":62},"   =>",[52,548,401],{"class":66},[52,550,190],{"class":62},[52,552,554,557,560,562],{"class":54,"line":553},18,[52,555,556],{"class":66},"    'parent'",[52,558,559],{"class":62},"     =>",[52,561,509],{"class":206},[52,563,190],{"class":62},[52,565,567,570,573,576],{"class":54,"line":566},19,[52,568,569],{"class":66},"    'hide_empty'",[52,571,572],{"class":62}," =>",[52,574,575],{"class":73}," false",[52,577,190],{"class":62},[52,579,581],{"class":54,"line":580},20,[52,582,583],{"class":62},"  ]);\n",[52,585,587],{"class":54,"line":586},21,[52,588,167],{"emptyLinePlaceholder":166},[52,590,592,595,597],{"class":54,"line":591},22,[52,593,594],{"class":206},"  $wynik",[52,596,427],{"class":58},[52,598,599],{"class":62}," [];\n",[52,601,603],{"class":54,"line":602},23,[52,604,167],{"emptyLinePlaceholder":166},[52,606,608,610,612,615,617,620],{"class":54,"line":607},24,[52,609,366],{"class":104},[52,611,124],{"class":62},[52,613,614],{"class":206},"$dzieci",[52,616,374],{"class":62},[52,618,619],{"class":206},"$dziecko",[52,621,380],{"class":62},[52,623,625,628,630,632],{"class":54,"line":624},25,[52,626,627],{"class":206},"    $wnuki",[52,629,427],{"class":58},[52,631,534],{"class":93},[52,633,537],{"class":62},[52,635,637,640,642,644],{"class":54,"line":636},26,[52,638,639],{"class":66},"      'taxonomy'",[52,641,546],{"class":62},[52,643,401],{"class":66},[52,645,190],{"class":62},[52,647,649,652,654,657,659,662],{"class":54,"line":648},27,[52,650,651],{"class":66},"      'parent'",[52,653,559],{"class":62},[52,655,656],{"class":206}," $dziecko",[52,658,392],{"class":62},[52,660,661],{"class":206},"term_id",[52,663,190],{"class":62},[52,665,667,670,672,674],{"class":54,"line":666},28,[52,668,669],{"class":66},"      'hide_empty'",[52,671,572],{"class":62},[52,673,575],{"class":73},[52,675,190],{"class":62},[52,677,679,682,684,687],{"class":54,"line":678},29,[52,680,681],{"class":66},"      'fields'",[52,683,559],{"class":62},[52,685,686],{"class":66}," 'ids'",[52,688,190],{"class":62},[52,690,692],{"class":54,"line":691},30,[52,693,694],{"class":62},"    ]);\n",[52,696,698],{"class":54,"line":697},31,[52,699,167],{"emptyLinePlaceholder":166},[52,701,703,706,708,711,713,715,717,719],{"class":54,"line":702},32,[52,704,705],{"class":206},"    $pozycja",[52,707,427],{"class":58},[52,709,710],{"class":93}," motyw_pozycja_menu",[52,712,63],{"class":62},[52,714,619],{"class":206},[52,716,70],{"class":62},[52,718,517],{"class":206},[52,720,77],{"class":62},[52,722,724,726,728,731,734,737,740,743,746,749,752],{"class":54,"line":723},33,[52,725,705],{"class":206},[52,727,392],{"class":62},[52,729,730],{"class":206},"classes",[52,732,733],{"class":62},"[] ",[52,735,736],{"class":58},"=",[52,738,739],{"class":206}," $wnuki",[52,741,742],{"class":104}," ?",[52,744,745],{"class":66}," 'menu-item-has-children'",[52,747,748],{"class":104}," :",[52,750,751],{"class":66}," ''",[52,753,154],{"class":62},[52,755,757,759,761,764,766,769,771,773],{"class":54,"line":756},34,[52,758,705],{"class":206},[52,760,392],{"class":62},[52,762,763],{"class":206},"url",[52,765,427],{"class":58},[52,767,768],{"class":93}," get_term_link",[52,770,63],{"class":62},[52,772,619],{"class":206},[52,774,77],{"class":62},[52,776,778],{"class":54,"line":777},35,[52,779,167],{"emptyLinePlaceholder":166},[52,781,783,786,788,790,792],{"class":54,"line":782},36,[52,784,785],{"class":206},"    $wynik",[52,787,733],{"class":62},[52,789,736],{"class":58},[52,791,450],{"class":206},[52,793,154],{"class":62},[52,795,797,799,801,803,805,808,810,812,814,816,818,820,822,824,826,828],{"class":54,"line":796},37,[52,798,785],{"class":206},[52,800,427],{"class":58},[52,802,430],{"class":58},[52,804,63],{"class":62},[52,806,807],{"class":206},"$wynik",[52,809,70],{"class":62},[52,811,439],{"class":93},[52,813,63],{"class":62},[52,815,619],{"class":206},[52,817,392],{"class":62},[52,819,661],{"class":206},[52,821,70],{"class":62},[52,823,450],{"class":206},[52,825,392],{"class":62},[52,827,464],{"class":206},[52,829,467],{"class":62},[52,831,833],{"class":54,"line":832},38,[52,834,160],{"class":62},[52,836,838],{"class":54,"line":837},39,[52,839,167],{"emptyLinePlaceholder":166},[52,841,843,845,848],{"class":54,"line":842},40,[52,844,480],{"class":104},[52,846,847],{"class":206}," $wynik",[52,849,154],{"class":62},[52,851,853],{"class":54,"line":852},41,[52,854,490],{"class":62},[10,856,857,858,861,862,865],{},"To wierny przedruk oryginału, razem z ",[14,859,860],{},"$pozycje = array_merge(…)"," w środku ",[14,863,864],{},"foreach"," po tej samej tablicy. Działa, bo PHP iteruje po kopii zrobionej przy wejściu w pętlę, ale wygląda na błąd i czyta się jak błąd. Zostawiam, bo tekst jest o zapytaniach, a nie o tym, co jeszcze dałoby się w tym kodzie posprzątać.",[10,867,868,869,872,873,876],{},"Każde ",[14,870,871],{},"get_terms()"," z parametrem ",[14,874,875],{},"parent"," to osobne zapytanie, bo zawęża wynik w SQL-u. Rachunek przy stu kategoriach w trzech poziomach wygląda tak:",[878,879,880,884,887],"ul",{},[881,882,883],"li",{},"jedno zapytanie na każdy węzeł, żeby pobrać jego dzieci — sto jeden razem z korzeniem",[881,885,886],{},"jedno zapytanie na każdą pozycję, żeby stwierdzić, czy ma dzieci — kolejne sto",[881,888,889,892],{},[14,890,891],{},"get_term_link()"," dla każdej pozycji, które przy taksonomii hierarchicznej dochodzi po przodkach aż do korzenia",[10,894,895],{},"Drugi punkt boli najbardziej, bo odpowiedź na pytanie „czy ten termin ma dzieci\" siedzi już w wyniku pierwszego zapytania o poziom niżej. Pytanie zadaje się dwa razy.",[10,897,898,899,901,902,905,906,909],{},"I rzecz, która decyduje o skali: filtr ",[14,900,16],{}," odpala się przy ",[284,903,904],{},"każdym"," ",[14,907,908],{},"wp_nav_menu()",", czyli na każdej podstronie i przy każdym menu na niej. Nagłówek i stopka to już dwa przebiegi.",[28,911,913],{"id":912},"dlaczego-cache-obiektów-tego-nie-łapie","Dlaczego cache obiektów tego nie łapie",[10,915,916,917,920],{},"Odruchowa odpowiedź brzmi „przecież WordPress cachuje terminy\". Cachuje, tylko ten cache żyje w obrębie jednego żądania PHP. Domyślna implementacja ",[14,918,919],{},"WP_Object_Cache"," trzyma dane w tablicy w pamięci procesu i znika razem z nim.",[10,922,923,924,926],{},"Pomaga to przy powtórzonych odczytach tego samego terminu w jednym żądaniu. Nie pomaga tutaj, bo każde ",[14,925,871],{}," ma inny zestaw argumentów, a więc inny klucz cache. Pierwsze wywołanie każdego z nich i tak idzie do bazy.",[10,928,929],{},"Cache przeżywa żądanie dopiero z persystentnym backendem: Redis albo Memcached z odpowiednią wtyczką drop-in. Na hostingu współdzielonym zwykle nie ma ani jednego, ani drugiego. Zakładaj, że przy każdym wejściu użytkownika kod startuje z pustym cache.",[28,931,933],{"id":932},"poprawka-drzewo-składane-w-php","Poprawka: drzewo składane w PHP",[10,935,936,938,939,941,942,944],{},[14,937,871],{}," bez parametru ",[14,940,875],{}," zwraca całą taksonomię jednym zapytaniem i przy okazji wrzuca wszystkie terminy do cache obiektów. Relacje rodzic–dziecko są w polu ",[14,943,875],{}," każdego z nich, więc drzewo składa się w PHP bez dotykania bazy:",[43,946,948],{"className":45,"code":947,"language":47,"meta":48,"style":48},"function motyw_drzewo_kategorii(): array {\n  $klucz = 'motyw_drzewo_oferta_kategoria';\n  $drzewo = get_transient($klucz);\n\n  if ($drzewo !== false) {\n    return $drzewo;\n  }\n\n  $terminy = get_terms([\n    'taxonomy'   => 'oferta_kategoria',\n    'hide_empty' => false,\n    'pad_counts' => true,\n  ]);\n\n  if (is_wp_error($terminy)) {\n    return [];\n  }\n\n  $drzewo = [];\n\n  foreach ($terminy as $termin) {\n    if ((int) $termin->count === 0) {\n      continue;\n    }\n\n    $drzewo[$termin->parent][] = [\n      'id'    => $termin->term_id,\n      'nazwa' => $termin->name,\n      'url'   => get_term_link($termin),\n    ];\n  }\n\n  set_transient($klucz, $drzewo, DAY_IN_SECONDS);\n\n  return $drzewo;\n}\n",[14,949,950,964,976,993,997,1012,1021,1025,1029,1040,1050,1060,1071,1075,1079,1096,1102,1106,1110,1118,1122,1137,1164,1170,1174,1178,1199,1215,1232,1250,1255,1259,1263,1281,1285,1293],{"__ignoreMap":48},[52,951,952,954,957,960,962],{"class":54,"line":55},[52,953,343],{"class":104},[52,955,956],{"class":93}," motyw_drzewo_kategorii",[52,958,959],{"class":62},"(): ",[52,961,351],{"class":111},[52,963,115],{"class":62},[52,965,966,969,971,974],{"class":54,"line":118},[52,967,968],{"class":206},"  $klucz",[52,970,427],{"class":58},[52,972,973],{"class":66}," 'motyw_drzewo_oferta_kategoria'",[52,975,154],{"class":62},[52,977,978,981,983,986,988,991],{"class":54,"line":148},[52,979,980],{"class":206},"  $drzewo",[52,982,427],{"class":58},[52,984,985],{"class":93}," get_transient",[52,987,63],{"class":62},[52,989,990],{"class":206},"$klucz",[52,992,77],{"class":62},[52,994,995],{"class":54,"line":157},[52,996,167],{"emptyLinePlaceholder":166},[52,998,999,1001,1003,1006,1008,1010],{"class":54,"line":163},[52,1000,121],{"class":104},[52,1002,124],{"class":62},[52,1004,1005],{"class":206},"$drzewo",[52,1007,398],{"class":58},[52,1009,575],{"class":73},[52,1011,380],{"class":62},[52,1013,1014,1016,1019],{"class":54,"line":170},[52,1015,151],{"class":104},[52,1017,1018],{"class":206}," $drzewo",[52,1020,154],{"class":62},[52,1022,1023],{"class":54,"line":184},[52,1024,160],{"class":62},[52,1026,1027],{"class":54,"line":193},[52,1028,167],{"emptyLinePlaceholder":166},[52,1030,1031,1034,1036,1038],{"class":54,"line":228},[52,1032,1033],{"class":206},"  $terminy",[52,1035,427],{"class":58},[52,1037,534],{"class":93},[52,1039,537],{"class":62},[52,1041,1042,1044,1046,1048],{"class":54,"line":237},[52,1043,543],{"class":66},[52,1045,546],{"class":62},[52,1047,401],{"class":66},[52,1049,190],{"class":62},[52,1051,1052,1054,1056,1058],{"class":54,"line":256},[52,1053,569],{"class":66},[52,1055,572],{"class":62},[52,1057,575],{"class":73},[52,1059,190],{"class":62},[52,1061,1062,1065,1067,1069],{"class":54,"line":262},[52,1063,1064],{"class":66},"    'pad_counts'",[52,1066,572],{"class":62},[52,1068,74],{"class":73},[52,1070,190],{"class":62},[52,1072,1073],{"class":54,"line":487},[52,1074,583],{"class":62},[52,1076,1077],{"class":54,"line":493},[52,1078,167],{"emptyLinePlaceholder":166},[52,1080,1081,1083,1085,1088,1090,1093],{"class":54,"line":498},[52,1082,121],{"class":104},[52,1084,124],{"class":62},[52,1086,1087],{"class":93},"is_wp_error",[52,1089,63],{"class":62},[52,1091,1092],{"class":206},"$terminy",[52,1094,1095],{"class":62},")) {\n",[52,1097,1098,1100],{"class":54,"line":526},[52,1099,151],{"class":104},[52,1101,599],{"class":62},[52,1103,1104],{"class":54,"line":540},[52,1105,160],{"class":62},[52,1107,1108],{"class":54,"line":553},[52,1109,167],{"emptyLinePlaceholder":166},[52,1111,1112,1114,1116],{"class":54,"line":566},[52,1113,980],{"class":206},[52,1115,427],{"class":58},[52,1117,599],{"class":62},[52,1119,1120],{"class":54,"line":580},[52,1121,167],{"emptyLinePlaceholder":166},[52,1123,1124,1126,1128,1130,1132,1135],{"class":54,"line":586},[52,1125,366],{"class":104},[52,1127,124],{"class":62},[52,1129,1092],{"class":206},[52,1131,374],{"class":62},[52,1133,1134],{"class":206},"$termin",[52,1136,380],{"class":62},[52,1138,1139,1141,1144,1146,1149,1151,1153,1156,1159,1162],{"class":54,"line":591},[52,1140,385],{"class":104},[52,1142,1143],{"class":62}," ((",[52,1145,445],{"class":104},[52,1147,1148],{"class":62},") ",[52,1150,1134],{"class":206},[52,1152,392],{"class":62},[52,1154,1155],{"class":206},"count",[52,1157,1158],{"class":58}," ===",[52,1160,1161],{"class":73}," 0",[52,1163,380],{"class":62},[52,1165,1166,1168],{"class":54,"line":602},[52,1167,408],{"class":104},[52,1169,154],{"class":62},[52,1171,1172],{"class":54,"line":607},[52,1173,415],{"class":62},[52,1175,1176],{"class":54,"line":624},[52,1177,167],{"emptyLinePlaceholder":166},[52,1179,1180,1183,1185,1187,1189,1191,1194,1196],{"class":54,"line":636},[52,1181,1182],{"class":206},"    $drzewo",[52,1184,210],{"class":62},[52,1186,1134],{"class":206},[52,1188,392],{"class":62},[52,1190,875],{"class":206},[52,1192,1193],{"class":62},"][] ",[52,1195,736],{"class":58},[52,1197,1198],{"class":62}," [\n",[52,1200,1201,1204,1207,1209,1211,1213],{"class":54,"line":648},[52,1202,1203],{"class":66},"      'id'",[52,1205,1206],{"class":62},"    => ",[52,1208,1134],{"class":206},[52,1210,392],{"class":62},[52,1212,661],{"class":206},[52,1214,190],{"class":62},[52,1216,1217,1220,1223,1225,1227,1230],{"class":54,"line":666},[52,1218,1219],{"class":66},"      'nazwa'",[52,1221,1222],{"class":62}," => ",[52,1224,1134],{"class":206},[52,1226,392],{"class":62},[52,1228,1229],{"class":206},"name",[52,1231,190],{"class":62},[52,1233,1234,1237,1240,1243,1245,1247],{"class":54,"line":678},[52,1235,1236],{"class":66},"      'url'",[52,1238,1239],{"class":62},"   => ",[52,1241,1242],{"class":93},"get_term_link",[52,1244,63],{"class":62},[52,1246,1134],{"class":206},[52,1248,1249],{"class":62},"),\n",[52,1251,1252],{"class":54,"line":691},[52,1253,1254],{"class":62},"    ];\n",[52,1256,1257],{"class":54,"line":697},[52,1258,160],{"class":62},[52,1260,1261],{"class":54,"line":702},[52,1262,167],{"emptyLinePlaceholder":166},[52,1264,1265,1268,1270,1272,1274,1276,1278],{"class":54,"line":723},[52,1266,1267],{"class":93},"  set_transient",[52,1269,63],{"class":62},[52,1271,990],{"class":206},[52,1273,70],{"class":62},[52,1275,1018],{"class":206},[52,1277,70],{"class":62},[52,1279,1280],{"class":62}," DAY_IN_SECONDS);\n",[52,1282,1283],{"class":54,"line":756},[52,1284,167],{"emptyLinePlaceholder":166},[52,1286,1287,1289,1291],{"class":54,"line":777},[52,1288,480],{"class":104},[52,1290,1018],{"class":206},[52,1292,154],{"class":62},[52,1294,1295],{"class":54,"line":782},[52,1296,490],{"class":62},[10,1298,1299,1300,1303,1304,1307],{},"Tablica jest indeksowana identyfikatorem rodzica, więc dzieci dowolnego węzła to ",[14,1301,1302],{},"$drzewo[$id] ?? []",", a pytanie „czy ma dzieci\" to ",[14,1305,1306],{},"isset($drzewo[$id])",". Oba bez zapytania.",[10,1309,1310,1312,1313,1315,1316,1319,1320,1322],{},[14,1311,891],{}," w pętli nie wraca po nic do bazy, bo ",[14,1314,871],{}," zdążyło już zapełnić cache terminów w tym żądaniu. Po co tu ",[14,1317,1318],{},"pad_counts"," i dlaczego bez niego filtr po ",[14,1321,1155],{}," jest błędny — dwie sekcje niżej.",[10,1324,1325],{},"Rekurencja zostaje, ale chodzi po tablicy w pamięci:",[43,1327,1329],{"className":45,"code":1328,"language":47,"meta":48,"style":48},"function motyw_galezie(array $drzewo, int $rodzic, int $rodzicMenu): array {\n  $wynik = [];\n\n  foreach ($drzewo[$rodzic] ?? [] as $dziecko) {\n    $pozycja = motyw_pozycja_menu($dziecko, $rodzicMenu);\n\n    if (isset($drzewo[$dziecko['id']])) {\n      $pozycja->classes[] = 'menu-item-has-children';\n    }\n\n    $wynik[] = $pozycja;\n\n    foreach (motyw_galezie($drzewo, $dziecko['id'], $pozycja->ID) as $wnuk) {\n      $wynik[] = $wnuk;\n    }\n  }\n\n  return $wynik;\n}\n",[14,1330,1331,1361,1369,1373,1393,1411,1415,1440,1457,1461,1465,1477,1481,1520,1534,1538,1542,1546,1554],{"__ignoreMap":48},[52,1332,1333,1335,1337,1339,1341,1343,1345,1347,1349,1351,1353,1355,1357,1359],{"class":54,"line":55},[52,1334,343],{"class":104},[52,1336,439],{"class":93},[52,1338,63],{"class":62},[52,1340,351],{"class":111},[52,1342,1018],{"class":206},[52,1344,512],{"class":62},[52,1346,445],{"class":111},[52,1348,509],{"class":206},[52,1350,512],{"class":62},[52,1352,445],{"class":111},[52,1354,517],{"class":206},[52,1356,357],{"class":62},[52,1358,351],{"class":111},[52,1360,115],{"class":62},[52,1362,1363,1365,1367],{"class":54,"line":118},[52,1364,594],{"class":206},[52,1366,427],{"class":58},[52,1368,599],{"class":62},[52,1370,1371],{"class":54,"line":148},[52,1372,167],{"emptyLinePlaceholder":166},[52,1374,1375,1377,1379,1381,1383,1386,1389,1391],{"class":54,"line":157},[52,1376,366],{"class":104},[52,1378,124],{"class":62},[52,1380,1005],{"class":206},[52,1382,210],{"class":62},[52,1384,1385],{"class":206},"$rodzic",[52,1387,1388],{"class":62},"] ?? [] as ",[52,1390,619],{"class":206},[52,1392,380],{"class":62},[52,1394,1395,1397,1399,1401,1403,1405,1407,1409],{"class":54,"line":163},[52,1396,705],{"class":206},[52,1398,427],{"class":58},[52,1400,710],{"class":93},[52,1402,63],{"class":62},[52,1404,619],{"class":206},[52,1406,70],{"class":62},[52,1408,517],{"class":206},[52,1410,77],{"class":62},[52,1412,1413],{"class":54,"line":170},[52,1414,167],{"emptyLinePlaceholder":166},[52,1416,1417,1419,1421,1424,1426,1428,1430,1432,1434,1437],{"class":54,"line":184},[52,1418,385],{"class":104},[52,1420,124],{"class":62},[52,1422,1423],{"class":58},"isset",[52,1425,63],{"class":62},[52,1427,1005],{"class":206},[52,1429,210],{"class":62},[52,1431,619],{"class":206},[52,1433,210],{"class":62},[52,1435,1436],{"class":66},"'id'",[52,1438,1439],{"class":62},"]])) {\n",[52,1441,1442,1445,1447,1449,1451,1453,1455],{"class":54,"line":193},[52,1443,1444],{"class":206},"      $pozycja",[52,1446,392],{"class":62},[52,1448,730],{"class":206},[52,1450,733],{"class":62},[52,1452,736],{"class":58},[52,1454,745],{"class":66},[52,1456,154],{"class":62},[52,1458,1459],{"class":54,"line":228},[52,1460,415],{"class":62},[52,1462,1463],{"class":54,"line":237},[52,1464,167],{"emptyLinePlaceholder":166},[52,1466,1467,1469,1471,1473,1475],{"class":54,"line":256},[52,1468,785],{"class":206},[52,1470,733],{"class":62},[52,1472,736],{"class":58},[52,1474,450],{"class":206},[52,1476,154],{"class":62},[52,1478,1479],{"class":54,"line":262},[52,1480,167],{"emptyLinePlaceholder":166},[52,1482,1483,1486,1488,1491,1493,1495,1497,1499,1501,1503,1506,1508,1510,1512,1515,1518],{"class":54,"line":487},[52,1484,1485],{"class":104},"    foreach",[52,1487,124],{"class":62},[52,1489,1490],{"class":93},"motyw_galezie",[52,1492,63],{"class":62},[52,1494,1005],{"class":206},[52,1496,70],{"class":62},[52,1498,656],{"class":206},[52,1500,210],{"class":62},[52,1502,1436],{"class":66},[52,1504,1505],{"class":62},"],",[52,1507,450],{"class":206},[52,1509,392],{"class":62},[52,1511,464],{"class":206},[52,1513,1514],{"class":62},") as ",[52,1516,1517],{"class":206},"$wnuk",[52,1519,380],{"class":62},[52,1521,1522,1525,1527,1529,1532],{"class":54,"line":493},[52,1523,1524],{"class":206},"      $wynik",[52,1526,733],{"class":62},[52,1528,736],{"class":58},[52,1530,1531],{"class":206}," $wnuk",[52,1533,154],{"class":62},[52,1535,1536],{"class":54,"line":498},[52,1537,415],{"class":62},[52,1539,1540],{"class":54,"line":526},[52,1541,160],{"class":62},[52,1543,1544],{"class":54,"line":540},[52,1545,167],{"emptyLinePlaceholder":166},[52,1547,1548,1550,1552],{"class":54,"line":553},[52,1549,480],{"class":104},[52,1551,847],{"class":206},[52,1553,154],{"class":62},[52,1555,1556],{"class":54,"line":566},[52,1557,490],{"class":62},[10,1559,1560,1561,1563],{},"Dwieście zapytań schodzi do kilku: jedno po terminy, jedno do dwóch od ",[14,1562,1318],{},", plus odczyt i zapis transientu. Każde następne wyświetlenie strony kosztuje już tylko sam odczyt transientu.",[10,1565,1566,1567,905,1570,1573,1574,1577,1578,1581,1582,1585],{},"To jest dobre miejsce, żeby nie zaokrąglić w swoją stronę. ",[284,1568,1569],{},"Transient nie kosztuje zera zapytań.",[14,1571,1572],{},"set_transient()"," z czasem wygaśnięcia zapisuje dwie opcje — wartość i ",[14,1575,1576],{},"_transient_timeout_…"," — obie z ",[14,1579,1580],{},"autoload"," ustawionym na „nie\". Nie jadą więc w zbiorczym zapytaniu o opcje autoładowane, tylko ",[14,1583,1584],{},"get_transient()"," pobiera po kolei timeout i wartość. To dwa zapytania na żądanie zamiast dwustu.",[10,1587,1588,1589,1591],{},"Można zejść do zera, zapisując transient bez czasu wygaśnięcia — wtedy opcja jest autoładowana i przyjeżdża razem z resztą. Cena jest taka, że drzewo ładuje się przy ",[284,1590,904],{}," żądaniu, także przy tych, w których menu nikomu nie jest potrzebne, i znika zabezpieczenie na wypadek nieunieważnionej zmiany. Przy stu kategoriach ta zamiana się nie opłaca.",[10,1593,1594,1595,1598],{},"Wersja sprzed poprawki dokładała do rekurencji ",[14,1596,1597],{},"array_merge($wynik, …)",", co na każdym poziomie kopiuje całą zebraną dotąd tablicę. Przy stu pozycjach to koszt pomijalny obok dwustu zapytań, ale w kodzie, który właśnie przepisujesz pod wydajność, zostawianie tego wygląda niepoważnie. Dopisywanie po jednym elemencie kosztuje tyle samo linii.",[1600,1601,1603],"h3",{"id":1602},"czego-walker-wymaga-od-dokładanej-pozycji","Czego walker wymaga od dokładanej pozycji",[10,1605,1606,1609,1610,1616,1617,1619,1620,1622],{},[14,1607,1608],{},"motyw_pozycja_menu()"," chowa w moich przykładach składanie obiektu pozycji i to jest miejsce, w którym najłatwiej się przejechać. Pozycje wstawiane przez ten filtr ",[284,1611,1612,1613],{},"nie przeszły przez ",[14,1614,1615],{},"_wp_menu_item_classes_by_context()",", bo ta funkcja odpala się w ",[14,1618,908],{}," wcześniej, przed ",[14,1621,16],{},". Wszystko, co ona normalnie dokłada, musisz dołożyć sam.",[10,1624,1625],{},"Trzy pola, o których zapomina się najczęściej:",[43,1627,1629],{"className":45,"code":1628,"language":47,"meta":48,"style":48},"$pozycja->current               = false;\n$pozycja->current_item_ancestor = false;\n$pozycja->current_item_parent   = false;\n",[14,1630,1631,1647,1662],{"__ignoreMap":48},[52,1632,1633,1635,1637,1640,1643,1645],{"class":54,"line":55},[52,1634,377],{"class":206},[52,1636,392],{"class":62},[52,1638,1639],{"class":206},"current",[52,1641,1642],{"class":58},"               =",[52,1644,575],{"class":73},[52,1646,154],{"class":62},[52,1648,1649,1651,1653,1656,1658,1660],{"class":54,"line":118},[52,1650,377],{"class":206},[52,1652,392],{"class":62},[52,1654,1655],{"class":206},"current_item_ancestor",[52,1657,427],{"class":58},[52,1659,575],{"class":73},[52,1661,154],{"class":62},[52,1663,1664,1666,1668,1671,1674,1676],{"class":54,"line":148},[52,1665,377],{"class":206},[52,1667,392],{"class":62},[52,1669,1670],{"class":206},"current_item_parent",[52,1672,1673],{"class":58},"   =",[52,1675,575],{"class":73},[52,1677,154],{"class":62},[10,1679,1680,1683,1684,1687,1688,1690],{},[14,1681,1682],{},"Walker_Nav_Menu::start_el()"," czyta ",[14,1685,1686],{},"$menu_item->current"," bez sprawdzania, czy pole istnieje. Brak tych trzech linii nie psuje menu wizualnie i dlatego przechodzi niezauważony — za to sypie ostrzeżeniem na każdą pozycję przy każdym renderze. W opisywanym motywie dawało to około czterech tysięcy wpisów do ",[14,1689,271],{}," na dobę.",[10,1692,1693,1694,512,1696,512,1699,512,1702,512,1704,512,1706,512,1709,512,1712,512,1714,512,1716,512,1719,512,1722,1725,1726,1729,1730,1733],{},"Poza tym pozycja potrzebuje kompletu pól, po których chodzi walker: ",[14,1695,464],{},[14,1697,1698],{},"db_id",[14,1700,1701],{},"menu_item_parent",[14,1703,395],{},[14,1705,455],{},[14,1707,1708],{},"type",[14,1710,1711],{},"title",[14,1713,763],{},[14,1715,730],{},[14,1717,1718],{},"target",[14,1720,1721],{},"attr_title",[14,1723,1724],{},"xfn"," i ",[14,1727,1728],{},"description",". Najprościej wziąć ",[14,1731,1732],{},"(object) []"," i wypełnić wszystkie, zamiast liczyć na to, że walker sobie poradzi z brakami.",[10,1735,1736,1739,1740,1743,1744,1746,1747,1750,1751,1754],{},[284,1737,1738],{},"Uwaga przy przepisywaniu: ta funkcja zmienia typ argumentu."," W wersji sprzed poprawki dostawała obiekt ",[14,1741,1742],{},"WP_Term"," prosto z ",[14,1745,871],{},", w wersji po — tablicę z sześcioma polami, którą składa ",[14,1748,1749],{},"motyw_drzewo_kategorii()",". Nazwa została ta sama, więc łatwo zostawić starą implementację i dostać błąd przy pierwszym ",[14,1752,1753],{},"$termin->name",". Przepisz ją razem z resztą albo nazwij inaczej.",[10,1756,1757,1758,1761],{},"I tu domyka się historia z ",[22,1759,1760],{"href":24},"wpisu o limicie CPU",": sześć tysięcy warningów na dobę, które wyglądały tam na najgorętszy trop, brało się w większości właśnie z tego. Trop był fałszywy co do przyczyny przeciążenia i prawdziwy co do tego, że w motywie siedzi błąd.",[1600,1763,1765,1767],{"id":1764},"pad_counts-to-jedna-linia-bez-której-logika-się-wywraca",[14,1766,1318],{}," to jedna linia, bez której logika się wywraca",[10,1769,1770,1771,1774,1775,1778,1779,1781,1782,1785,1786,1789],{},"Oryginał miał ",[14,1772,1773],{},"hide_empty => true"," i puste gałęzie odpadały same, w SQL-u. Po przejściu na ",[14,1776,1777],{},"hide_empty => false"," filtrowanie przenosi się do PHP i tu czeka pułapka: pole ",[14,1780,1155],{}," liczy domyślnie ",[284,1783,1784],{},"wyłącznie wpisy przypisane wprost do danego terminu",". Kategoria nadrzędna, która sama nie ma żadnego wpisu, ale jej podkategorie mają komplet, dostaje ",[14,1787,1788],{},"count === 0",".",[10,1791,1792],{},"Odfiltrowanie po takim liczniku wycina ją z menu razem z całą gałęzią pod spodem. Na instalacji deweloperskiej, gdzie wpisy siedzą byle gdzie, tego nie widać — na produkcji z uporządkowaną strukturą znika pół nawigacji.",[10,1794,1795,1798,1799,1801,1802,1804],{},[14,1796,1797],{},"pad_counts => true"," każe WordPressowi zsumować liczniki rekurencyjnie w dół drzewa, więc rodzic dostaje sumę swoich potomków. Dopiero wtedy ",[14,1800,1155],{}," znaczy to samo co przy ",[14,1803,1773],{}," i dopiero wtedy wolno po nim filtrować.",[1600,1806,1808],{"id":1807},"sprawdź-czy-taksonomia-ma-w-ogóle-przyjazne-adresy","Sprawdź, czy taksonomia ma w ogóle przyjazne adresy",[10,1810,1811,1813,1814,1817,1818,1821],{},[14,1812,891],{}," przy taksonomii zarejestrowanej z ",[14,1815,1816],{},"'rewrite' => false"," zwraca adres z parametrem zapytania, w rodzaju ",[14,1819,1820],{},"/?oferta_kategoria=szkolenia",". To nie jest błąd tej funkcji — taksonomia po prostu nie ma własnej struktury adresów.",[10,1823,1824,1825,1827,1828,1831,1832,1835],{},"Kod, który na wyniku ",[14,1826,891],{}," szuka potem fragmentu ścieżki przez ",[14,1829,1830],{},"strpos()",", nigdy się w takim układzie nie dopasuje i po cichu leci gałęzią awaryjną. Zapytań to nie kosztuje, bo cache jest już pełny, ale to sto wywołań funkcji, której wynik ląduje w koszu, i mylący trop dla następnej osoby czytającej ten plik. Zajrzyj do ",[14,1833,1834],{},"register_taxonomy()",", zanim zaczniesz budować ścieżki z tego, co zwraca.",[1600,1837,1839],{"id":1838},"unieważnianie-transientu","Unieważnianie transientu",[10,1841,1842],{},"Transient bez unieważniania to nowy błąd w miejsce starego: redaktor zmienia nazwę kategorii i przez dobę nie widzi efektu. Drzewo zmienia się przy dwóch rodzajach zdarzeń — przy edycji samych terminów i przy zmianie tego, co jest do nich przypisane, bo od przypisań zależą liczniki:",[43,1844,1846],{"className":45,"code":1845,"language":47,"meta":48,"style":48},"add_action('created_oferta_kategoria', 'motyw_wyczysc_drzewo');\nadd_action('edited_oferta_kategoria', 'motyw_wyczysc_drzewo');\nadd_action('delete_oferta_kategoria', 'motyw_wyczysc_drzewo');\n\nadd_action('set_object_terms', function ($id, $terminy, $tt, $taksonomia): void {\n  if ($taksonomia === 'oferta_kategoria') {\n    motyw_wyczysc_drzewo();\n  }\n}, 10, 4);\n\nadd_action('transition_post_status', function ($nowy, $stary, $post): void {\n  if ($nowy !== $stary && $post->post_type === 'oferta') {\n    motyw_wyczysc_drzewo();\n  }\n}, 10, 3);\n\nadd_action('before_delete_post', function ($id, $post): void {\n  if ($post->post_type === 'oferta') {\n    motyw_wyczysc_drzewo();\n  }\n}, 10, 2);\n\nfunction motyw_wyczysc_drzewo(): void {\n  delete_transient('motyw_drzewo_oferta_kategoria');\n}\n",[14,1847,1848,1864,1879,1894,1898,1938,1955,1963,1967,1982,1986,2020,2051,2057,2061,2073,2077,2104,2125,2131,2135,2148,2152,2166,2178],{"__ignoreMap":48},[52,1849,1850,1852,1854,1857,1859,1862],{"class":54,"line":55},[52,1851,94],{"class":93},[52,1853,63],{"class":62},[52,1855,1856],{"class":66},"'created_oferta_kategoria'",[52,1858,70],{"class":62},[52,1860,1861],{"class":66}," 'motyw_wyczysc_drzewo'",[52,1863,77],{"class":62},[52,1865,1866,1868,1870,1873,1875,1877],{"class":54,"line":118},[52,1867,94],{"class":93},[52,1869,63],{"class":62},[52,1871,1872],{"class":66},"'edited_oferta_kategoria'",[52,1874,70],{"class":62},[52,1876,1861],{"class":66},[52,1878,77],{"class":62},[52,1880,1881,1883,1885,1888,1890,1892],{"class":54,"line":148},[52,1882,94],{"class":93},[52,1884,63],{"class":62},[52,1886,1887],{"class":66},"'delete_oferta_kategoria'",[52,1889,70],{"class":62},[52,1891,1861],{"class":66},[52,1893,77],{"class":62},[52,1895,1896],{"class":54,"line":157},[52,1897,167],{"emptyLinePlaceholder":166},[52,1899,1900,1902,1904,1907,1909,1911,1913,1916,1918,1921,1923,1926,1928,1931,1934,1936],{"class":54,"line":163},[52,1901,94],{"class":93},[52,1903,63],{"class":62},[52,1905,1906],{"class":66},"'set_object_terms'",[52,1908,70],{"class":62},[52,1910,105],{"class":104},[52,1912,124],{"class":62},[52,1914,1915],{"class":206},"$id",[52,1917,70],{"class":62},[52,1919,1920],{"class":206}," $terminy",[52,1922,70],{"class":62},[52,1924,1925],{"class":206}," $tt",[52,1927,70],{"class":62},[52,1929,1930],{"class":206}," $taksonomia",[52,1932,1933],{"class":62},"):",[52,1935,112],{"class":111},[52,1937,115],{"class":62},[52,1939,1940,1942,1944,1947,1949,1951,1953],{"class":54,"line":170},[52,1941,121],{"class":104},[52,1943,124],{"class":62},[52,1945,1946],{"class":206},"$taksonomia",[52,1948,1158],{"class":58},[52,1950,401],{"class":66},[52,1952,134],{"class":62},[52,1954,115],{"class":62},[52,1956,1957,1960],{"class":54,"line":184},[52,1958,1959],{"class":93},"    motyw_wyczysc_drzewo",[52,1961,1962],{"class":62},"();\n",[52,1964,1965],{"class":54,"line":193},[52,1966,160],{"class":62},[52,1968,1969,1972,1975,1977,1980],{"class":54,"line":228},[52,1970,1971],{"class":62},"},",[52,1973,1974],{"class":73}," 10",[52,1976,70],{"class":62},[52,1978,1979],{"class":73}," 4",[52,1981,77],{"class":62},[52,1983,1984],{"class":54,"line":237},[52,1985,167],{"emptyLinePlaceholder":166},[52,1987,1988,1990,1992,1995,1997,1999,2001,2004,2006,2009,2011,2014,2016,2018],{"class":54,"line":256},[52,1989,94],{"class":93},[52,1991,63],{"class":62},[52,1993,1994],{"class":66},"'transition_post_status'",[52,1996,70],{"class":62},[52,1998,105],{"class":104},[52,2000,124],{"class":62},[52,2002,2003],{"class":206},"$nowy",[52,2005,70],{"class":62},[52,2007,2008],{"class":206}," $stary",[52,2010,70],{"class":62},[52,2012,2013],{"class":206}," $post",[52,2015,1933],{"class":62},[52,2017,112],{"class":111},[52,2019,115],{"class":62},[52,2021,2022,2024,2026,2028,2030,2032,2035,2037,2039,2042,2044,2047,2049],{"class":54,"line":262},[52,2023,121],{"class":104},[52,2025,124],{"class":62},[52,2027,2003],{"class":206},[52,2029,398],{"class":58},[52,2031,2008],{"class":206},[52,2033,2034],{"class":58}," &&",[52,2036,2013],{"class":206},[52,2038,392],{"class":62},[52,2040,2041],{"class":206},"post_type",[52,2043,1158],{"class":58},[52,2045,2046],{"class":66}," 'oferta'",[52,2048,134],{"class":62},[52,2050,115],{"class":62},[52,2052,2053,2055],{"class":54,"line":487},[52,2054,1959],{"class":93},[52,2056,1962],{"class":62},[52,2058,2059],{"class":54,"line":493},[52,2060,160],{"class":62},[52,2062,2063,2065,2067,2069,2071],{"class":54,"line":498},[52,2064,1971],{"class":62},[52,2066,1974],{"class":73},[52,2068,70],{"class":62},[52,2070,250],{"class":73},[52,2072,77],{"class":62},[52,2074,2075],{"class":54,"line":526},[52,2076,167],{"emptyLinePlaceholder":166},[52,2078,2079,2081,2083,2086,2088,2090,2092,2094,2096,2098,2100,2102],{"class":54,"line":540},[52,2080,94],{"class":93},[52,2082,63],{"class":62},[52,2084,2085],{"class":66},"'before_delete_post'",[52,2087,70],{"class":62},[52,2089,105],{"class":104},[52,2091,124],{"class":62},[52,2093,1915],{"class":206},[52,2095,70],{"class":62},[52,2097,2013],{"class":206},[52,2099,1933],{"class":62},[52,2101,112],{"class":111},[52,2103,115],{"class":62},[52,2105,2106,2108,2110,2113,2115,2117,2119,2121,2123],{"class":54,"line":553},[52,2107,121],{"class":104},[52,2109,124],{"class":62},[52,2111,2112],{"class":206},"$post",[52,2114,392],{"class":62},[52,2116,2041],{"class":206},[52,2118,1158],{"class":58},[52,2120,2046],{"class":66},[52,2122,134],{"class":62},[52,2124,115],{"class":62},[52,2126,2127,2129],{"class":54,"line":566},[52,2128,1959],{"class":93},[52,2130,1962],{"class":62},[52,2132,2133],{"class":54,"line":580},[52,2134,160],{"class":62},[52,2136,2137,2139,2141,2143,2146],{"class":54,"line":586},[52,2138,1971],{"class":62},[52,2140,1974],{"class":73},[52,2142,70],{"class":62},[52,2144,2145],{"class":73}," 2",[52,2147,77],{"class":62},[52,2149,2150],{"class":54,"line":591},[52,2151,167],{"emptyLinePlaceholder":166},[52,2153,2154,2156,2159,2161,2164],{"class":54,"line":602},[52,2155,343],{"class":104},[52,2157,2158],{"class":93}," motyw_wyczysc_drzewo",[52,2160,959],{"class":62},[52,2162,2163],{"class":111},"void",[52,2165,115],{"class":62},[52,2167,2168,2171,2173,2176],{"class":54,"line":607},[52,2169,2170],{"class":93},"  delete_transient",[52,2172,63],{"class":62},[52,2174,2175],{"class":66},"'motyw_drzewo_oferta_kategoria'",[52,2177,77],{"class":62},[52,2179,2180],{"class":54,"line":624},[52,2181,490],{"class":62},[10,2183,2184,2185,2188,2189,1789],{},"Trzy pierwsze to dynamiczne akcje — w nazwie siedzi slug taksonomii, więc dla ",[14,2186,2187],{},"category"," będzie ",[14,2190,2191],{},"edited_category",[10,2193,2194,2195,2198],{},"Kuszące jest zastąpienie trzech ostatnich jednym ",[14,2196,2197],{},"save_post"," bez warunku i sam tak najpierw napisałem. To kasuje transient przy każdej rewizji, przy autozapisie co minutę i przy zapisie dowolnej strony w całym serwisie, także takiej, która z tą taksonomią nie ma nic wspólnego. Na serwisie, w którym ktoś pisze, cache przestaje wtedy istnieć w praktyce, a ty masz złudzenie, że działa.",[10,2200,2201,2204,2205,2208,2209,2212],{},[14,2202,2203],{},"set_object_terms"," łapie dokładnie to zdarzenie, które zmienia drzewo: podpięcie albo odpięcie wpisu od kategorii. ",[14,2206,2207],{},"transition_post_status"," łapie publikację, przeniesienie do kosza i przywrócenie, bo każde z nich rusza licznikiem. ",[14,2210,2211],{},"before_delete_post"," domyka trwałe usunięcie.",[10,2214,2215,2216,2219],{},"Czas życia ustawiony na ",[14,2217,2218],{},"DAY_IN_SECONDS"," jest tu tylko zabezpieczeniem na wypadek zdarzenia, którego nie przewidziałem. Przy sprawnym unieważnianiu transient i tak znika wcześniej.",[10,2221,2222,2223,2225,2226,2228],{},"Jedno takie zdarzenie warto wymienić, bo żaden z powyższych hooków go nie łapie: w transiencie leżą gotowe adresy z ",[14,2224,891],{},". Ani zmiana struktury bezpośrednich odnośników w ustawieniach, ani zmiana sluga w ",[14,2227,1834],{}," nie rusza terminów ani przypisań, więc menu przez dobę prowadzi pod stare adresy.",[10,2230,2231,2232,2235],{},"Pewne wyjście to nie przechowywać adresów: trzymaj w transiencie same slugi i składaj adres przy renderze. Dopięcie się do ",[14,2233,2234],{},"permalink_structure_changed"," zamyka tylko połowę problemu, bo ta akcja odpala się przy zapisie ustawień odnośników, a zmiana sluga taksonomii dzieje się w kodzie i żadnego zdarzenia w bazie nie zostawia.",[28,2237,2239],{"id":2238},"drugi-filtr-permalinki","Drugi filtr: permalinki",[10,2241,2242],{},"Ten sam motyw miał w permalinkach produktów placeholder podmieniany na slug kategorii:",[43,2244,2246],{"className":45,"code":2245,"language":47,"meta":48,"style":48},"add_filter('post_type_link', 'motyw_permalink_produktu', 10, 2);\n\nfunction motyw_permalink_produktu(string $odnosnik, WP_Post $post): string {\n  $terminy = wp_get_object_terms($post->ID, 'oferta_kategoria');\n\n  if (empty($terminy) || is_wp_error($terminy)) {\n    return $odnosnik;\n  }\n\n  return str_replace('%oferta_kategoria%', $terminy[0]->slug, $odnosnik);\n}\n",[14,2247,2248,2272,2276,2304,2327,2331,2358,2366,2370,2374,2406],{"__ignoreMap":48},[52,2249,2250,2252,2254,2257,2259,2262,2264,2266,2268,2270],{"class":54,"line":55},[52,2251,322],{"class":93},[52,2253,63],{"class":62},[52,2255,2256],{"class":66},"'post_type_link'",[52,2258,70],{"class":62},[52,2260,2261],{"class":66}," 'motyw_permalink_produktu'",[52,2263,70],{"class":62},[52,2265,1974],{"class":73},[52,2267,70],{"class":62},[52,2269,2145],{"class":73},[52,2271,77],{"class":62},[52,2273,2274],{"class":54,"line":118},[52,2275,167],{"emptyLinePlaceholder":166},[52,2277,2278,2280,2283,2285,2288,2291,2293,2296,2298,2300,2302],{"class":54,"line":148},[52,2279,343],{"class":104},[52,2281,2282],{"class":93}," motyw_permalink_produktu",[52,2284,63],{"class":62},[52,2286,2287],{"class":111},"string",[52,2289,2290],{"class":206}," $odnosnik",[52,2292,512],{"class":62},[52,2294,2295],{"class":111},"WP_Post",[52,2297,2013],{"class":206},[52,2299,357],{"class":62},[52,2301,2287],{"class":111},[52,2303,115],{"class":62},[52,2305,2306,2308,2310,2313,2315,2317,2319,2321,2323,2325],{"class":54,"line":157},[52,2307,1033],{"class":206},[52,2309,427],{"class":58},[52,2311,2312],{"class":93}," wp_get_object_terms",[52,2314,63],{"class":62},[52,2316,2112],{"class":206},[52,2318,392],{"class":62},[52,2320,464],{"class":206},[52,2322,70],{"class":62},[52,2324,401],{"class":66},[52,2326,77],{"class":62},[52,2328,2329],{"class":54,"line":163},[52,2330,167],{"emptyLinePlaceholder":166},[52,2332,2333,2335,2337,2340,2342,2344,2346,2349,2352,2354,2356],{"class":54,"line":170},[52,2334,121],{"class":104},[52,2336,124],{"class":62},[52,2338,2339],{"class":58},"empty",[52,2341,63],{"class":62},[52,2343,1092],{"class":206},[52,2345,1148],{"class":62},[52,2347,2348],{"class":58},"||",[52,2350,2351],{"class":93}," is_wp_error",[52,2353,63],{"class":62},[52,2355,1092],{"class":206},[52,2357,1095],{"class":62},[52,2359,2360,2362,2364],{"class":54,"line":184},[52,2361,151],{"class":104},[52,2363,2290],{"class":206},[52,2365,154],{"class":62},[52,2367,2368],{"class":54,"line":193},[52,2369,160],{"class":62},[52,2371,2372],{"class":54,"line":228},[52,2373,167],{"emptyLinePlaceholder":166},[52,2375,2376,2378,2381,2383,2386,2388,2390,2392,2394,2397,2400,2402,2404],{"class":54,"line":237},[52,2377,480],{"class":104},[52,2379,2380],{"class":58}," str_replace",[52,2382,63],{"class":62},[52,2384,2385],{"class":66},"'%oferta_kategoria%'",[52,2387,70],{"class":62},[52,2389,1920],{"class":206},[52,2391,210],{"class":62},[52,2393,245],{"class":73},[52,2395,2396],{"class":62},"]->",[52,2398,2399],{"class":206},"slug",[52,2401,70],{"class":62},[52,2403,2290],{"class":206},[52,2405,77],{"class":62},[52,2407,2408],{"class":54,"line":256},[52,2409,490],{"class":62},[10,2411,2412,2415,2416,2419],{},[14,2413,2414],{},"wp_get_object_terms()"," odpytuje bazę przy każdym wywołaniu i nie sprawdza po drodze cache obiektów. Listing dwudziestu produktów to dwadzieścia zapytań na same odnośniki — a ",[14,2417,2418],{},"post_type_link"," odpala się także przy generowaniu mapy witryny i kanałów RSS, gdzie pozycji bywają setki.",[10,2421,2422,2423,2426],{},"Zamiennik to ",[14,2424,2425],{},"get_the_terms()",". Ta funkcja pyta najpierw cache obiektów i dopiero przy pudle schodzi do bazy, a wynik odkłada dla tego wpisu.",[10,2428,2429],{},"Wdrożona poprawka na tym się skończyła: podmiana jednej funkcji na drugą i tyle. To wystarczyło, żeby zapytania zniknęły. Poniżej wersja, którą napisałbym dziś — różni się od wdrożonej trzema rzeczami i żadna z nich nie dotyczy wydajności:",[43,2431,2433],{"className":45,"code":2432,"language":47,"meta":48,"style":48},"add_filter('post_type_link', 'motyw_permalink_produktu', 10, 2);\n\nfunction motyw_permalink_produktu(string $odnosnik, WP_Post $post): string {\n  if (!str_contains($odnosnik, '%oferta_kategoria%')) {\n    return $odnosnik;\n  }\n\n  $terminy = get_the_terms($post, 'oferta_kategoria');\n\n  if (empty($terminy) || is_wp_error($terminy)) {\n    return str_replace('%oferta_kategoria%', 'bez-kategorii', $odnosnik);\n  }\n\n  return str_replace('%oferta_kategoria%', reset($terminy)->slug, $odnosnik);\n}\n",[14,2434,2435,2457,2461,2485,2509,2517,2521,2525,2544,2548,2572,2593,2597,2601,2631],{"__ignoreMap":48},[52,2436,2437,2439,2441,2443,2445,2447,2449,2451,2453,2455],{"class":54,"line":55},[52,2438,322],{"class":93},[52,2440,63],{"class":62},[52,2442,2256],{"class":66},[52,2444,70],{"class":62},[52,2446,2261],{"class":66},[52,2448,70],{"class":62},[52,2450,1974],{"class":73},[52,2452,70],{"class":62},[52,2454,2145],{"class":73},[52,2456,77],{"class":62},[52,2458,2459],{"class":54,"line":118},[52,2460,167],{"emptyLinePlaceholder":166},[52,2462,2463,2465,2467,2469,2471,2473,2475,2477,2479,2481,2483],{"class":54,"line":148},[52,2464,343],{"class":104},[52,2466,2282],{"class":93},[52,2468,63],{"class":62},[52,2470,2287],{"class":111},[52,2472,2290],{"class":206},[52,2474,512],{"class":62},[52,2476,2295],{"class":111},[52,2478,2013],{"class":206},[52,2480,357],{"class":62},[52,2482,2287],{"class":111},[52,2484,115],{"class":62},[52,2486,2487,2489,2491,2494,2497,2499,2502,2504,2507],{"class":54,"line":157},[52,2488,121],{"class":104},[52,2490,124],{"class":62},[52,2492,2493],{"class":58},"!",[52,2495,2496],{"class":58},"str_contains",[52,2498,63],{"class":62},[52,2500,2501],{"class":206},"$odnosnik",[52,2503,70],{"class":62},[52,2505,2506],{"class":66}," '%oferta_kategoria%'",[52,2508,1095],{"class":62},[52,2510,2511,2513,2515],{"class":54,"line":163},[52,2512,151],{"class":104},[52,2514,2290],{"class":206},[52,2516,154],{"class":62},[52,2518,2519],{"class":54,"line":170},[52,2520,160],{"class":62},[52,2522,2523],{"class":54,"line":184},[52,2524,167],{"emptyLinePlaceholder":166},[52,2526,2527,2529,2531,2534,2536,2538,2540,2542],{"class":54,"line":193},[52,2528,1033],{"class":206},[52,2530,427],{"class":58},[52,2532,2533],{"class":93}," get_the_terms",[52,2535,63],{"class":62},[52,2537,2112],{"class":206},[52,2539,70],{"class":62},[52,2541,401],{"class":66},[52,2543,77],{"class":62},[52,2545,2546],{"class":54,"line":228},[52,2547,167],{"emptyLinePlaceholder":166},[52,2549,2550,2552,2554,2556,2558,2560,2562,2564,2566,2568,2570],{"class":54,"line":237},[52,2551,121],{"class":104},[52,2553,124],{"class":62},[52,2555,2339],{"class":58},[52,2557,63],{"class":62},[52,2559,1092],{"class":206},[52,2561,1148],{"class":62},[52,2563,2348],{"class":58},[52,2565,2351],{"class":93},[52,2567,63],{"class":62},[52,2569,1092],{"class":206},[52,2571,1095],{"class":62},[52,2573,2574,2576,2578,2580,2582,2584,2587,2589,2591],{"class":54,"line":256},[52,2575,151],{"class":104},[52,2577,2380],{"class":58},[52,2579,63],{"class":62},[52,2581,2385],{"class":66},[52,2583,70],{"class":62},[52,2585,2586],{"class":66}," 'bez-kategorii'",[52,2588,70],{"class":62},[52,2590,2290],{"class":206},[52,2592,77],{"class":62},[52,2594,2595],{"class":54,"line":262},[52,2596,160],{"class":62},[52,2598,2599],{"class":54,"line":487},[52,2600,167],{"emptyLinePlaceholder":166},[52,2602,2603,2605,2607,2609,2611,2613,2616,2618,2620,2623,2625,2627,2629],{"class":54,"line":493},[52,2604,480],{"class":104},[52,2606,2380],{"class":58},[52,2608,63],{"class":62},[52,2610,2385],{"class":66},[52,2612,70],{"class":62},[52,2614,2615],{"class":58}," reset",[52,2617,63],{"class":62},[52,2619,1092],{"class":206},[52,2621,2622],{"class":62},")->",[52,2624,2399],{"class":206},[52,2626,70],{"class":62},[52,2628,2290],{"class":206},[52,2630,77],{"class":62},[52,2632,2633],{"class":54,"line":498},[52,2634,490],{"class":62},[10,2636,2637,2643,2644,2646,2647,2650,2651,2654],{},[284,2638,2639,2640,1789],{},"Wczesny ",[14,2641,2642],{},"return"," Filtr ",[14,2645,2418],{}," dostaje odnośniki wszystkich typów treści, także tych, które żadnego placeholdera nie mają. Sprawdzenie ",[14,2648,2649],{},"str_contains()"," kosztuje ułamek mikrosekundy i wycina większość wywołań przed dotknięciem taksonomii. W oryginale każdy odnośnik strony i wpisu przechodził przez pełne zapytanie, którego wynik i tak trafiał do ",[14,2652,2653],{},"str_replace()"," bez efektu.",[10,2656,2657,2660,2661,2664],{},[284,2658,2659],{},"Zapasowy slug."," Wersja z oryginału zwracała przy braku terminu adres z niepodmienionym ",[14,2662,2663],{},"%oferta_kategoria%"," w środku. Taki odnośnik trafia do HTML-u i do mapy witryny.",[10,2666,2667,905,2676,2678,2679,2682,2683,2686],{},[284,2668,2669,2672,2673,1789],{},[14,2670,2671],{},"reset()"," zamiast ",[14,2674,2675],{},"[0]",[14,2677,2425],{}," zwraca tablicę, której klucze nie muszą zaczynać się od zera — przy odczycie z cache bywają zachowane identyfikatory terminów. ",[14,2680,2681],{},"$terminy[0]"," daje wtedy ",[14,2684,2685],{},"null"," i ostrzeżenie.",[10,2688,2689],{},"Ten ostatni punkt ma wersję, która wygląda na naprawioną, a nie jest. Kod, w którym potrzebny jest drugi termin, a pierwszy służy za zapas, kusi żeby napisać:",[43,2691,2693],{"className":45,"code":2692,"language":47,"meta":48,"style":48},"$termin = isset($terminy[1]) ? $terminy[1] : $terminy[0];\n",[14,2694,2695],{"__ignoreMap":48},[52,2696,2697,2699,2701,2704,2706,2708,2710,2713,2716,2719,2721,2723,2725,2728,2730,2732,2734,2736],{"class":54,"line":55},[52,2698,1134],{"class":206},[52,2700,427],{"class":58},[52,2702,2703],{"class":58}," isset",[52,2705,63],{"class":62},[52,2707,1092],{"class":206},[52,2709,210],{"class":62},[52,2711,2712],{"class":73},"1",[52,2714,2715],{"class":62},"]) ",[52,2717,2718],{"class":104},"?",[52,2720,1920],{"class":206},[52,2722,210],{"class":62},[52,2724,2712],{"class":73},[52,2726,2727],{"class":62},"] ",[52,2729,41],{"class":104},[52,2731,1920],{"class":206},[52,2733,210],{"class":62},[52,2735,245],{"class":73},[52,2737,2738],{"class":62},"];\n",[10,2740,2741,2744,2745,2748,2749,2751,2752,2754,2755,2758,2759,2761],{},[14,2742,2743],{},"isset()"," na ",[14,2746,2747],{},"[1]"," zabezpiecza jeden indeks i zostawia drugi gołym dostępem. Gdy klucze nie będą listą od zera, ",[14,2750,2747],{}," nie istnieje, warunek grzecznie przechodzi do ",[14,2753,2675],{}," — którego też nie ma — i ostrzeżenie leci mimo wszystko. Założenie o kształcie kluczy zostało, tylko przesunięte o jedną linię. ",[14,2756,2757],{},"array_values()"," na wejściu albo ",[14,2760,2671],{}," rozwiązuje to u źródła.",[28,2763,2765],{"id":2764},"jak-sprawdzić-że-poprawka-zadziałała","Jak sprawdzić, że poprawka zadziałała",[10,2767,2768],{},"Zapal licznik z pierwszej sekcji drugi raz i porównaj wpisy z tymi sprzed zmiany. Pomiar przed i po nie wymaga ciągłego działania, tylko dwóch krótkich okien na tej samej podstronie i przy tej samej liczbie kategorii. Spodziewasz się trzech rzeczy:",[10,2770,2771,2774],{},[284,2772,2773],{},"Pierwsze wyświetlenie po wyczyszczeniu transientu"," ma o tyle zapytań mniej, ile węzłów liczyło drzewo razy dwa. Menu zostaje przy kilku.",[10,2776,2777,2780,2781,1789],{},[284,2778,2779],{},"Kolejne wyświetlenia"," schodzą do dwóch po stronie menu — timeout i wartość transientu z ",[14,2782,2783],{},"wp_options",[10,2785,2786,2789],{},[284,2787,2788],{},"Liczba przestaje zależeć od liczby kategorii."," To jest właściwy test. Dodaj w panelu dziesięć kategorii i odśwież stronę — wcześniej licznik rósł razem z nimi, po poprawce stoi w miejscu. Dopóki rośnie, gdzieś w kodzie została pętla pytająca bazę.",[28,2791,2793],{"id":2792},"gdzie-jeszcze-szukać-tego-wzorca","Gdzie jeszcze szukać tego wzorca",[10,2795,2796],{},"Trzy miejsca, w których ten sam błąd powtarza się w motywach najczęściej:",[10,2798,2799,2806,2807,2810,2811,2814,2815,2818],{},[284,2800,2801,2802,2805],{},"Pętla po wpisach z ",[14,2803,2804],{},"get_post_meta()"," bez przygotowania cache."," Sama funkcja korzysta z cache, ale zapełnia go dopiero przy pierwszym odczycie danego wpisu, a ",[14,2808,2809],{},"WP_Query"," z ",[14,2812,2813],{},"update_post_meta_cache"," ustawionym na ",[14,2816,2817],{},"false"," odbiera jej to przygotowanie. Sprawdź argumenty zapytania, zanim zaczniesz przepisywać pętlę.",[10,2820,2821,2827,2828,2831],{},[284,2822,2823,2826],{},[14,2824,2825],{},"get_field()"," z ACF w pętli po elementach powtarzalnych."," Każde wywołanie to odczyt metadanych, a przy polach typu relacja także pobranie obiektu wpisu. Wariant ",[14,2829,2830],{},"get_fields()"," dla całego wpisu naraz bywa o rząd wielkości tańszy.",[10,2833,2834,905,2837,2840,2841,2844],{},[284,2835,2836],{},"Liczniki w listingach.",[14,2838,2839],{},"wp_count_posts()"," albo własne ",[14,2842,2843],{},"COUNT(*)"," wywoływane dla każdej kategorii na liście. To dokładnie ten sam kształt co sprawdzanie dzieci w menu: pytanie zadawane per element, gdy jedno zapytanie grupujące załatwia całość.",[28,2846,2848],{"id":2847},"pytania-które-padają-przy-takiej-poprawce","Pytania, które padają przy takiej poprawce",[10,2850,2851,2854],{},[284,2852,2853],{},"Czy nie prościej wrzucić wtyczkę cache'ującą stronę?","\nCache stron ukryje objaw dla niezalogowanych i nie zmieni nic dla redaktora, dla koszyka ani dla żądań, które omijają cache. Kod wykonujący trzysta zapytań na żądanie dalej je wykonuje, po prostu rzadziej. Wtyczka jest warstwą na wierzchu poprawnego kodu.",[10,2856,2857,2860,2861,2863,2864,2866],{},[284,2858,2859],{},"Czy transient nie zaśmieci bazy?","\nTransient bez persystentnego cache obiektów ląduje w tabeli ",[14,2862,2783],{},". Jeden wpis o rozmiarze kilku kilobajtów to żaden koszt, ale ten rozmiar zależy wyłącznie od tego, co do niego włożysz. Przykład wyżej odkłada sześć pól na termin i przy stu kategoriach mieści się w kilku kilobajtach. Wrzucenie tam pełnych obiektów ",[14,2865,1742],{}," daje przy tej samej liczbie kategorii kilkadziesiąt kilobajtów, a przy tysiącu terminów sama deserializacja przy każdym żądaniu potrafi kosztować więcej niż zapytanie, przed którym transient miał chronić. Trzymaj w nim to, czego naprawdę używa szablon.",[10,2868,2869],{},"Druga pułapka to transienty z unikalnym kluczem na każdą kombinację parametrów — tych potrafią urosnąć tysiące i po wygaśnięciu nikt ich nie sprząta.",[10,2871,2872,2875],{},[284,2873,2874],{},"Zapytania liczne kontra zapytania kosztowne — jak je odróżnić?","\nZ samej liczby się nie da i to jest ważne rozróżnienie. Query Monitor podaje czas każdego zapytania i sumę dla całego żądania — dwieście zapytań po 0,1 ms to 20 ms i przepisywanie filtra się za to nie zwróci. Sprawdź sumę, zanim zaczniesz. Druga rzecz warta sprawdzenia to koszt po stronie PHP: przy setkach pozycji menu samo składanie obiektów potrafi ważyć więcej niż baza, a wtedy cache na poziomie transientu daje więcej niż optymalizacja zapytań.",[10,2877,2878,2884,2885,2887,2888,2890],{},[284,2879,2880,2881,2883],{},"Czy ",[14,2882,1777],{}," nie spowalnia zapytania?","\nSamo ",[14,2886,1777],{}," je upraszcza, bo ",[14,2889,1773],{}," dokłada złączenie z tabelą relacji i warunek na liczbę wpisów. Przy budowaniu drzewa i tak potrzebujesz pełnej listy, żeby poznać relacje rodzic–dziecko.",[10,2892,2893,2894,2896,2897,2899,2900,2903],{},"Koszt wraca przez ",[14,2895,1797],{},", bez którego filtrowanie po ",[14,2898,1155],{}," jest błędne. WordPress odpala wtedy ",[14,2901,2902],{},"_pad_term_counts()",", czyli dodatkowy odczyt tabeli relacji i statusów wpisów. To jedno czy dwa zapytania na całą taksonomię, nie jedno na termin — czyli dokładnie ta zamiana, o którą w tym tekście chodzi.",[2905,2906],"hr",{},[10,2908,2909,2910,2913,2914,1789],{},"Pomiar przed poprawką jest tu ważniejszy od samej poprawki. Kod, który wygląda na kosztowny, bywa niewidoczny w profilu, a kilkaset zapytań po 0,1 ms nie jest problemem wartym przepisywania filtra. Zacznij od ",[14,2911,2912],{},"get_num_queries()"," i od czasów w Query Monitorze, a dopiero potem otwieraj ",[14,2915,83],{},[2917,2918,2919],"style",{},"html pre.shiki code .sjrmR, html code.shiki .sjrmR{--shiki-default:#56B6C2}html pre.shiki code .sn6KH, html code.shiki .sn6KH{--shiki-default:#ABB2BF}html pre.shiki code .subq3, html code.shiki .subq3{--shiki-default:#98C379}html pre.shiki code .sVC51, html code.shiki .sVC51{--shiki-default:#D19A66}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sVbv2, html code.shiki .sVbv2{--shiki-default:#61AFEF}html pre.shiki code .seHd6, html code.shiki .seHd6{--shiki-default:#C678DD}html pre.shiki code .sU0A5, html code.shiki .sU0A5{--shiki-default:#E5C07B}html pre.shiki code .sVyAn, html code.shiki .sVyAn{--shiki-default:#E06C75}",{"title":48,"searchDepth":118,"depth":118,"links":2921},[2922,2923,2924,2925,2932,2933,2934,2935],{"id":30,"depth":118,"text":31},{"id":308,"depth":118,"text":309},{"id":912,"depth":118,"text":913},{"id":932,"depth":118,"text":933,"children":2926},[2927,2928,2930,2931],{"id":1602,"depth":148,"text":1603},{"id":1764,"depth":148,"text":2929},"pad_counts to jedna linia, bez której logika się wywraca",{"id":1807,"depth":148,"text":1808},{"id":1838,"depth":148,"text":1839},{"id":2238,"depth":118,"text":2239},{"id":2764,"depth":118,"text":2765},{"id":2792,"depth":118,"text":2793},{"id":2847,"depth":118,"text":2848},"2026-09-19","Filtr budujący menu z taksonomii potrafi kosztować kilkaset zapytań SQL na jedno wyświetlenie strony. Jak to zmierzyć, dlaczego cache obiektów nie pomaga i jak przepisać filtr, żeby liczba zapytań przestała rosnąć z liczbą kategorii.","md","WordPress",{},"/img/blog/zapytania-n-plus-1-filtry-wordpress.webp","Laptop na biurku z otwartym plikiem functions.php w edytorze kodu","/zapytania-n-plus-1-filtry-wordpress",{"title":5,"description":2937},{"loc":2943},"zapytania-n-plus-1-filtry-wordpress",false,null,"hgpCaB_S39dnj4RtVd7X8gz3tTqReJsGV6eAxFG9cgc",1789981061228]