PostgreSQL Query Profiler: Kako uskladiti plan i upit
Mnogi koji već koriste objasni.tensor.ru - naša usluga vizualizacije plana PostgreSQL možda nije svjesna jedne od svojih supermoći - da pretvori teško čitljiv dio serverskog dnevnika...
… u lijepo dizajniran upit s kontekstualnim savjetima za relevantne čvorove plana:
Transkript prvog dijela, posvećenog tipičnim problemima performansi upita i njihovim rješenjima, možete pronaći u članku "Recepti za bolesne SQL upite".
Prvo, hajde da uradimo bojenje - i više nećemo bojati plan, već smo ga ofarbali, već je lepo i razumljivo, ali zahtev.
Činilo nam se da zahtjev izvučen iz dnevnika sa neformatiranim „listom“ izgleda vrlo ružno i stoga nezgodno.
Pogotovo kada programeri "zalijepe" tijelo zahtjeva u kod (ovo je, naravno, anti-uzorak, ali se dešava) u jedan red. Užas!
Hajde da to nacrtamo nekako ljepše.
A ako to možemo lijepo nacrtati, odnosno rastaviti i sastaviti tijelo zahtjeva nazad, onda možemo "prikačiti" nagoveštaj svakom objektu ovog zahtjeva - šta se dogodilo u odgovarajućoj tački plana.
Stablo sintakse upita
Da biste to učinili, upit se prvo mora raščlaniti.
Jer, imamo jezgro sistema radi na NodeJS, onda smo napravili modul za to, možete pronađite ga na githubu. U stvari, ovo su proširena "vezivanja" za unutrašnjost samog PostgreSQL parsera. To jest, gramatika je jednostavno binarno kompajlirana i veze za nju pravi NodeJS. Za osnovu smo uzeli tuđe module - tu nema velike tajne.
Tijelo ulaznog zahtjeva hranimo našoj funkciji - na izlazu dobijamo raščlanjeno sintaktičko stablo u obliku JSON objekta.
Sada možemo proći kroz ovo stablo u suprotnom smjeru i sastaviti zahtjev sa uvlakama, bojama, formatiranjem koje želimo. Ne, ovo nije podesivo, ali smo mislili da bi to bilo zgodno.
Mapiranje čvorova upita i plana
Sada da vidimo kako možemo kombinirati plan koji smo analizirali u prvom koraku i upit koji smo analizirali u drugom.
Uzmimo jednostavan primjer - imamo zahtjev koji formira CTE i čita ga dvaput. On pravi takav plan.
CTE
Ako pažljivo pogledate, to prije 12. verzije (ili počevši od nje s ključnom riječi MATERIALIZED) formiranje CTE je bezuslovna prepreka za planer.
A to znači, ako negdje u zahtjevu vidimo generiranje CTE-a, a negdje u planu čvor CTE, onda se ovi čvorovi jedinstveno "bore" među sobom, možemo ih odmah kombinirati.
Zadatak "sa zvjezdicom"Napomena: CTE-ovi mogu biti ugniježđeni.
Ima ih vrlo slabo ugniježđenih, pa čak i istog imena. Na primjer, možete unutra CTE A napraviti CTE X, i na istom nivou iznutra CTE B uradi ponovo CTE X:
WITH A AS (
WITH X AS (...)
SELECT ...
)
, B AS (
WITH X AS (...)
SELECT ...
)
...
Kada upoređujete, ovo morate razumjeti. Vrlo je teško to shvatiti "očima" - čak i kada se vidi plan, čak i kada se vidi tijelo zahtjeva. Ako je vaša CTE generacija složena, ugniježđena, zahtjevi su veliki, onda je potpuno nesvjesno.
UNION
Ako imamo ključnu riječ u zahtjevu UNION [ALL] (operator spajanja dva uzorka), tada na planu odgovara bilo kojem čvoru Append, ili nešto Recursive Union.
ono što je "iznad" UNION - ovo je prvo dijete našeg čvora, koji je "odozdo" - drugi. Ako prođe UNION onda smo "zalijepili" nekoliko blokova odjednom Append-čvor će i dalje imati samo jedno, ali neće imati dvoje djece, već mnogo - redom kako idu, redom:
(...) -- #1
UNION ALL
(...) -- #2
UNION ALL
(...) -- #3
Append
-> ... #1
-> ... #2
-> ... #3
Zadatak "sa zvjezdicom": unutar generacije rekurzivnog preuzimanja (WITH RECURSIVE) također može biti više od jednog UNION. Ali samo poslednji blok posle poslednjeg je uvek rekurzivan UNION. Sve iznad je jedno ali drugačije UNION:
WITH RECURSIVE T AS(
(...) -- #1
UNION ALL
(...) -- #2, тут кончается генерация стартового состояния рекурсии
UNION ALL
(...) -- #3, только этот блок рекурсивный и может содержать обращение к T
)
...
Takve primjere također treba moći "nalijepiti". U ovom primjeru to vidimo UNION-segmenti u našem zahtjevu su bili 3 komada. Shodno tome, jedan UNION odgovara Append-čvor, a drugi - Recursive Union.
Čitanje-pisanje podataka
Sve, postavljeno, sada znamo koji dio zahtjeva odgovara kojem dijelu plana. I u ovim komadima možemo lako i prirodno pronaći one objekte koji su „čitljivi“.
Sa stanovišta upita, ne znamo da li je ovo tabela ili CTE, ali su označeni istim čvorom RangeVar. A u "čitljivom" planu, ovo je također prilično ograničen skup čvorova:
Seq Scan on [tbl]
Bitmap Heap Scan on [tbl]
Index [Only] Scan [Backward] using [idx] on [tbl]
CTE Scan on [cte]
Insert/Update/Delete on [tbl]
Znamo strukturu plana i zahtjeva, znamo korespondenciju blokova, znamo imena objekata - pravimo nedvosmisleno poređenje.
Opet zadatak "sa zvjezdicom". Uzimamo zahtjev, izvršavamo ga, nemamo pseudonime - samo ga dvaput čitamo iz jednog CTE-a.
Pogledajmo plan - u čemu je problem? Zašto smo imali pseudonim? Mi to nismo naručili. Odakle mu toliki "broj"?
PostgreSQL ga sam dodaje. Samo to treba da shvatite upravo takav alias za nas, za potrebe poređenja sa planom, to nema nikakvog smisla, jednostavno se dodaje ovdje. Ne obraćajmo pažnju na njega.
Drugi zadatak "sa zvjezdicom": ako čitamo iz particionirane tablice, onda ćemo dobiti čvor Append ili Merge Append, koji će se sastojati od velikog broja "djece", a od kojih će svako biti po neko Scan'om iz tabele-sekcije: Seq Scan, Bitmap Heap Scan ili Index Scan. Ali, u svakom slučaju, ova „djeca“ neće biti složeni upiti - po tome se ovi čvorovi mogu razlikovati od Append at UNION.
Razumijemo i takve čvorove, skupljamo ih "na jednu hrpu" i kažemo: "sve što čitate iz megatable-a nalazi se ovdje i niz drvo".
"Jednostavni" čvorovi za prikupljanje podataka
Values Scan u planu odgovara VALUES u zahtjevu.
Result je zahtjev bez FROM kao SELECT 1. Ili kada imate lažan izraz WHERE-block (tada se pojavljuje atribut One-Time Filter):
EXPLAIN ANALYZE
SELECT * FROM pg_class WHERE FALSE; -- или 0 = 1
Ali s ugniježđenim upitima sve je složenije - nažalost, oni se ne pretvaraju uvijek u InitPlan/SubPlan. Ponekad se pretvore u ... Join ili ... Anti Join, pogotovo kada napišete nešto slično WHERE NOT EXISTS .... I tu nije uvijek moguće kombinirati - u tekstu plana nema operatora koji odgovaraju čvorovima plana.
Opet zadatak "sa zvjezdicom": neki VALUES u zahtjevu. U ovom slučaju iu planu dobit ćete nekoliko čvorova Values Scan.
Sufiksi "broja" pomoći će da se razlikuju jedni od drugih - dodaju se točno onim redoslijedom kojim odgovaraju VALUES-blokovi u toku zahtjeva od vrha do dna.
Obrada podataka
Čini se da je sve u našem zahtjevu sređeno - samo Limit.
Ali ovdje je sve jednostavno - takvi čvorovi kao Limit, Sort, Aggregate, WindowAgg, Unique Oni „mapiraju“ jedan-na-jedan sa odgovarajućim operaterima u zahtjevu, ako su tamo. Nema "zvezda" i nema poteškoća.
JOIN
Poteškoće nastaju kada želimo da kombinujemo JOIN između sebe. To nije uvijek moguće, ali je moguće.
Sa tačke gledišta analizatora upita, imamo čvor JoinExpr, koja ima tačno dvoje djece - lijevo i desno. To je, odnosno, ono što je "iznad" vašeg JOIN-a i ono što je napisano "ispod" u upitu.
A sa stanovišta plana, ovo su dva potomka nekih * Loop/* Join-čvor. Nested Loop, Hash Anti Join...je tako nešto.
Upotrijebimo jednostavnu logiku: ako imamo tablice A i B koje se međusobno "pridružuju" u planu, onda bi u upitu mogle biti locirane ili A-JOIN-B, ili B-JOIN-A. Pokušajmo ovako kombinirati, pokušati kombinirati na suprotan način, i tako sve dok ne ponestane takvih parova.
Uzmite naše stablo sintakse, uzmite naš plan, pogledajte ih... ne izgleda tako!
Precrtajmo ga u obliku grafikona - o, nešto je već postalo slično nečemu!
Primijetimo da imamo čvorove koji imaju djecu B i C u isto vrijeme – nije nam bitno kojim redoslijedom. Kombinirajmo ih i okrenimo sliku čvora.
Pogledajmo ponovo. Sada imamo čvorove sa djecom A i parove (B + C) - kompatibilne s njima.
Odlično! Ispostavilo se da smo mi ovo dvoje JOIN iz upita sa čvorovima plana su uspješno kombinovani.
Nažalost, ovaj problem nije uvijek riješen.
Na primjer, ako je zahtjev A JOIN B JOIN C, a u planu su prije svega spojeni “ekstremni” čvorovi A i C. Ali tog operatora u upitu nema, nemamo šta da istaknemo, nemamo za šta vezati nagoveštaj. Isto sa "zarezom" kada pišete A, B.
Ali, u većini slučajeva, skoro svi čvorovi uspevaju da se „odvezu“ i na vreme dobiju takvo profilisanje na levoj strani – doslovno, kao u Google Chrome-u, kada analizirate JavaScript kod. Možete vidjeti koliko dugo je svaki red i svaki izraz "izvršen".
A kako bismo vam olakšali korištenje svega ovoga, napravili smo skladište arhiva, gdje možete sačuvati, a zatim pronaći svoje planove zajedno sa povezanim upitima ili podijeliti vezu s nekim.
Ako samo treba da nečitljiv zahtjev dovedete u adekvatan oblik, koristite naš "normalizator".