PostgreSQL Query Profiler: hur man matchar plan och fråga
Många som redan använder explain.tensor.ru - vår PostgreSQL-planvisualiseringstjänst kanske inte är medveten om en av dess superkrafter - vänder en svårläst del av serverloggen...
... till en vackert designad fråga med kontextuella tips för motsvarande plannoder:
Transkriptionen av den första delen, tillägnad typiska frågeprestandaproblem och deras lösningar, finns i artikeln "Recept för krångliga SQL-frågor".
Låt oss först börja färglägga - och vi kommer inte längre att färglägga planen, vi har redan färglagt den, vi har redan den vacker och begriplig, men en begäran.
Det verkade för oss att med ett sådant oformaterat "ark" ser begäran från loggen väldigt ful ut och därför obekväm.
Särskilt när utvecklare "limmar" förfrågans kropp i koden (detta är naturligtvis ett antimönster, men det händer) på en rad. Fruktansvärd!
Låt oss rita det här på något sätt vackrare.
Och om vi kan rita detta vackert, det vill säga demontera och sätta ihop förfrågans kropp, kan vi sedan "bifoga" en ledtråd till varje föremål för denna begäran - vad som hände vid motsvarande punkt i planen.
Frågesyntaxträd
För att göra detta måste begäran först analyseras.
För det har vi kärnan i systemet körs på NodeJS, sedan gjorde vi en modul för det, du kan hitta den på GitHub. I själva verket är dessa utökade "bindningar" till det interna i själva PostgreSQL-parsern. Det vill säga, grammatiken är helt enkelt binär kompilerad och bindningar görs till den från NodeJS. Vi tog andras moduler som grund – här finns ingen stor hemlighet.
Vi matar förfrågans brödtext som input till vår funktion - vid utgången får vi ett parsat syntaxträd i form av ett JSON-objekt.
Nu kan vi gå igenom det här trädet i motsatt riktning och sammanställa en begäran med de indrag, färg och formatering som vi vill ha. Nej, detta är inte anpassningsbart, men det verkade för oss att det skulle vara bekvämt.
Mappning av fråge- och plannoder
Låt oss nu se hur vi kan kombinera planen som vi analyserade i det första steget och frågan som vi analyserade i det andra.
Låt oss ta ett enkelt exempel - vi har en fråga som genererar en CTE och läser från den två gånger. Han skapar en sådan plan.
Det betyder att om vi ser en CTE-generation någonstans i förfrågan och en nod någonstans i planen CTE, då dessa noder definitivt "kämpar" med varandra, vi kan omedelbart kombinera dem.
Problem med en asterisk: CTE kan kapslas.
Det finns mycket dåligt kapslade sådana, och till och med de med samma namn. Till exempel kan du inuti CTE A göra CTE X, och på samma nivå inuti CTE B gör det igen CTE X:
WITH A AS (
WITH X AS (...)
SELECT ...
)
, B AS (
WITH X AS (...)
SELECT ...
)
...
När du jämför måste du förstå detta. Att förstå detta "med dina ögon" - även att se planen, till och med se innehållet i begäran - är mycket svårt. Om din CTE-generering är komplex, kapslad och förfrågningarna är stora, är den helt omedveten.
UNION
Om vi har ett nyckelord i frågan UNION [ALL] (operatör för att sammanfoga två sampel), då motsvarar det i planen antingen en nod Append, eller några Recursive Union.
Det som är "ovanför" ovan UNION - det här är den första ättlingen till vår nod, som är "under" - den andra. Om genom UNION vi har flera block "limmade" på en gång, alltså Append-det kommer fortfarande bara att finnas en nod, men den kommer att ha inte två, utan många barn - i den ordning de går, respektive:
(...) -- #1
UNION ALL
(...) -- #2
UNION ALL
(...) -- #3
Append
-> ... #1
-> ... #2
-> ... #3
Problem med en asterisk: inom rekursiv samplingsgenerering (WITH RECURSIVE) kan också vara mer än en UNION. Men bara det allra sista blocket efter det sista är alltid rekursivt UNION. Allt ovan är ett, men olika UNION:
WITH RECURSIVE T AS(
(...) -- #1
UNION ALL
(...) -- #2, тут кончается генерация стартового состояния рекурсии
UNION ALL
(...) -- #3, только этот блок рекурсивный и может содержать обращение к T
)
...
Du måste också kunna "sticka ut" sådana exempel. I det här exemplet ser vi det UNION-Det fanns 3 segment i vår förfrågan. Följaktligen en UNION motsvarar Append-nod, och till den andra - Recursive Union.
Läs-skriv data
Allt är upplagt, nu vet vi vilken del av förfrågan som motsvarar vilken del av planen. Och i dessa bitar kan vi enkelt och naturligt hitta de föremål som är "läsbara".
Ur frågesynpunkt vet vi inte om det är en tabell eller en CTE, men de är designade av samma nod RangeVar. Och när det gäller "läsbarhet" är detta också en ganska begränsad uppsättning noder:
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]
Vi känner till strukturen för planen och frågan, vi känner till blockens överensstämmelse, vi känner till namnen på objekten - vi gör en en-till-en-jämförelse.
Igen uppgift "med en asterisk". Vi tar förfrågan, utför den, vi har inga alias - vi läser den bara två gånger från samma CTE.
Vi tittar på planen – vad är problemet? Varför hade vi ett alias? Vi beställde det inte. Var får han ett sådant "nummernummer"?
PostgreSQL lägger till det själv. Du behöver bara förstå det bara ett sådant alias för oss, för att jämföra med planen, är det inte meningsfullt, det läggs helt enkelt till här. Låt oss inte uppmärksamma honom.
andra uppgift "med en asterisk": om vi läser från en partitionerad tabell kommer vi att få en nod Append eller Merge Append, som kommer att bestå av ett stort antal "barn", och var och en kommer att vara på något sätt Scan'om från tabellsektionen: Seq Scan, Bitmap Heap Scan eller Index Scan. Men i alla fall kommer dessa "barn" inte att vara komplexa frågor - det är så dessa noder kan särskiljas från Append при UNION.
Vi förstår också sådana knutar, samlar dem "i en hög" och säger: "allt du läser från megatable är här och ner i trädet".
"Enkla" datamottagande noder
Values Scan överensstämmer i plan VALUES i begäran.
Result är en begäran utan FROM tycka om SELECT 1. Eller när du har ett medvetet falskt uttryck i dig WHERE-block (då visas attributet One-Time Filter):
EXPLAIN ANALYZE
SELECT * FROM pg_class WHERE FALSE; -- или 0 = 1
Men med kapslade frågor är allt mer komplicerat - tyvärr blir de inte alltid till InitPlan/SubPlan. Ibland förvandlas de till ... Join eller ... Anti Join, speciellt när du skriver något som WHERE NOT EXISTS .... Och här är det inte alltid möjligt att kombinera dem - i planens text finns det inga operatörer som motsvarar planens noder.
Igen uppgift "med en asterisk": några VALUES i begäran. I det här fallet och i planen får du flera noder Values Scan.
"Numrerade" suffix hjälper till att skilja dem från varandra - de läggs till exakt i den ordning som motsvarande hittas VALUES-block längs begäran från topp till botten.
Databehandling
Det verkar som att allt i vår förfrågan har sorterats ut - allt som återstår är Limit.
Men här är allt enkelt - sådana noder som Limit, Sort, Aggregate, WindowAgg, Unique "map" en-till-en till motsvarande operatörer i begäran, om de finns där. Det finns inga "stjärnor" eller svårigheter här.
JOIN
Svårigheter uppstår när vi vill kombinera JOIN sinsemellan. Detta är inte alltid möjligt, men det är möjligt.
Ur frågeparserns synvinkel har vi en nod JoinExpr, som har exakt två barn - vänster och höger. Detta är följaktligen vad som är "ovanför" din JOIN och vad som står "under" den i begäran.
Och ur planens synvinkel är det här två ättlingar till några * Loop/* Join-nod. Nested Loop, Hash Anti Join,... - något sådant.
Låt oss använda enkel logik: om vi har tabellerna A och B som "förenar sig" med varandra i planen, kan de i förfrågan finnas antingen A-JOIN-BEller B-JOIN-A. Låt oss försöka kombinera på det här sättet, låt oss försöka kombinera tvärtom, och så vidare tills vi får slut på sådana par.
Låt oss ta vårt syntaxträd, ta vår plan, titta på dem... inte lika!
Låt oss rita om det i form av grafer - åh, det ser redan ut som något!
Låt oss notera att vi har noder som samtidigt har barn B och C - vi bryr oss inte om i vilken ordning. Låt oss kombinera dem och vända bilden av noden.
Låt oss titta igen. Nu har vi noder med barn A och par (B + C) - kompatibla med dem också.
Bra! Det visar sig att vi är dessa två JOIN från begäran med plannoderna kombinerades framgångsrikt.
Tyvärr är detta problem inte alltid löst.
Till exempel om i en förfrågan A JOIN B JOIN C, och i planen var först och främst de "yttre" noderna A och C anslutna. Men det finns ingen sådan operatör i förfrågan, vi har inget att markera, inget att bifoga en ledtråd till. Det är samma sak med "komma" när du skriver A, B.
Men i de flesta fall kan nästan alla noder "lösgöras" och du kan få den här typen av profilering till vänster i tid - bokstavligen, som i Google Chrome när du analyserar JavaScript-kod. Du kan se hur lång tid varje rad och varje påstående tog att "exekveras".
Och för att göra det bekvämare för dig att använda allt detta har vi gjort förvaring arkivet, där du kan spara och senare hitta dina planer tillsammans med tillhörande förfrågningar eller dela länken med någon.
Om du bara behöver föra en oläslig fråga i en adekvat form, använd vår "normaliserare".