PostgreSQL Query Profiler: Com fer coincidir el pla i la consulta
Molts que ja estan utilitzant explicar.tensor.ru - És possible que el nostre servei de visualització del pla PostgreSQL no conegui un dels seus superpoders: convertir una part difícil de llegir del registre del servidor...
... en una consulta ben dissenyada amb consells contextuals per als nodes del pla corresponents:
En aquesta transcripció de la segona part del seu informe a PGConf.Russia 2020 Us explicaré com ho hem aconseguit.
La transcripció de la primera part, dedicada als problemes típics de rendiment de les consultes i les seves solucions, es pot trobar a l'article "Receptes per a consultes SQL amb problemes".
Primer, comencem a pintar, i ja no pintarem el plànol, ja l'hem pintat, ja el tenim bonic i comprensible, però una petició.
Ens va semblar que amb un "full" tan sense format la sol·licitud extreta del registre sembla molt lletja i, per tant, incòmode.
Sobretot quan els desenvolupadors "enganxen" el cos de la sol·licitud al codi (això és, per descomptat, un antipatró, però passa) en una línia. Horrible!
Dibuixem-ho d'alguna manera més bella.
I si ho podem dibuixar molt bé, és a dir, desmuntar i tornar a reunir el cos de la sol·licitud, aleshores podem "adjuntar" una pista a cada objecte d'aquesta sol·licitud: què va passar al punt corresponent del pla.
Consulta l'arbre de sintaxi
Per fer-ho, primer s'ha d'analitzar la sol·licitud.
Perquè tenim el nucli del sistema s'executa a NodeJS, llavors vam fer un mòdul per a això, pots trobar-lo a GitHub. De fet, aquests són "enllaços" estesos a les parts internes del mateix analitzador de PostgreSQL. És a dir, la gramàtica és simplement compilada en binària i s'hi fan enllaços des de NodeJS. Hem agafat com a base els mòduls d'altres persones: aquí no hi ha cap gran secret.
Introduïm el cos de la sol·licitud com a entrada de la nostra funció: a la sortida obtenim un arbre de sintaxi analitzat en forma d'objecte JSON.
Ara podem recórrer aquest arbre en la direcció oposada i muntar una sol·licitud amb els sagnats, el color i el format que volem. No, això no és personalitzable, però ens va semblar que seria convenient.
Consulta de mapes i nodes de planificació
Ara veiem com podem combinar el pla que hem analitzat en el primer pas i la consulta que hem analitzat en el segon.
Prenguem un exemple senzill: tenim una consulta que genera un CTE i el llegeix dues vegades. Ell genera aquest pla.
Això vol dir que si veiem una generació CTE en algun lloc de la sol·licitud i un node en algun lloc del pla CTE, llavors aquests nodes definitivament "barallen" entre ells, els podem combinar immediatament.
Problema amb un asterisc: els CTE es poden imbricar.
N'hi ha de molt mal nidificats, i fins i tot de homònims. Per exemple, pots dins CTE A fer CTE X, i al mateix nivell interior CTE B fer-ho de nou CTE X:
WITH A AS (
WITH X AS (...)
SELECT ...
)
, B AS (
WITH X AS (...)
SELECT ...
)
...
Quan compareu, heu d'entendre això. Entendre això "amb els ulls" - fins i tot veure el pla, fins i tot veure el cos de la petició - és molt difícil. Si la vostra generació CTE és complexa, imbricada i les sol·licituds són grans, aleshores és completament inconscient.
UNIÓ
Si tenim una paraula clau a la consulta UNION [ALL] (operador d'unir dues mostres), llavors en el pla correspon a un node Append, o alguns Recursive Union.
El que està "a dalt" a dalt UNION - aquest és el primer descendent del nostre node, que està "a sota" - el segon. Si passa UNION tenim diversos blocs "enganxats" alhora, doncs Append-encara hi haurà un sol node, però no tindrà dos, sinó molts fills - en l'ordre en què van, respectivament:
(...) -- #1
UNION ALL
(...) -- #2
UNION ALL
(...) -- #3
Append
-> ... #1
-> ... #2
-> ... #3
Problema amb un asterisc: generació de mostreig recursiu dins (WITH RECURSIVE) també pot ser més d'un UNION. Però només l'últim bloc després de l'últim sempre és recursiu UNION. Tot el que hi ha a dalt és un, però diferent UNION:
WITH RECURSIVE T AS(
(...) -- #1
UNION ALL
(...) -- #2, тут кончается генерация стартового состояния рекурсии
UNION ALL
(...) -- #3, только этот блок рекурсивный и может содержать обращение к T
)
...
També heu de ser capaços de "desmarcar" aquests exemples. En aquest exemple ho veiem UNION-Hi havia 3 segments a la nostra sol·licitud. En conseqüència, un UNION correspon Append-node, i a l'altre - Recursive Union.
Dades de lectura-escriptura
Tot està disposat, ara sabem quina peça de la sol·licitud correspon a quina peça del pla. I en aquestes peces podem trobar de manera fàcil i natural aquells objectes que són "llegibles".
Des del punt de vista de la consulta, no sabem si és una taula o un CTE, però estan designats pel mateix node RangeVar. I en termes de "llegibilitat", aquest també és un conjunt força limitat de nodes:
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]
Coneixem l'estructura del plànol i la consulta, coneixem la correspondència dels blocs, coneixem els noms dels objectes, fem una comparació un a un.
De nou tasca "amb un asterisc". Prenem la sol·licitud, l'executem, no tenim cap àlies; només la llegim dues vegades des del mateix CTE.
Mirem el pla: quin és el problema? Per què teníem un àlies? No l'hem encarregat. D'on treu aquest "número número"?
PostgreSQL l'afegeix ell mateix. Només cal entendre això només un àlies així per a nosaltres, a efectes de comparació amb el pla, no té cap sentit, simplement s'afegeix aquí. No li fem cas.
El segon tasca "amb un asterisc": si estem llegint des d'una taula particionada, obtindrem un node Append o Merge Append, que constarà d'un gran nombre de "nens", i cadascun dels quals serà d'alguna manera Scan'om de la secció de la taula: Seq Scan, Bitmap Heap Scan o Index Scan. Però, en qualsevol cas, aquests "fills" no seran consultes complexes; així és com es poden distingir aquests nodes de Append en UNION.
També entenem aquests nusos, els recollim "en una pila" i diem: "tot el que llegiu de megatable és aquí i avall de l'arbre".
Nodes de recepció de dades "simples".
Values Scan correspon al pla VALUES en la petició.
Result és una petició sense FROM tipus de SELECT 1. O quan tens una expressió deliberadament falsa WHERE-block (aleshores apareix l'atribut One-Time Filter):
EXPLAIN ANALYZE
SELECT * FROM pg_class WHERE FALSE; -- или 0 = 1
Però amb les consultes imbricades tot és més complicat; malauradament, no sempre es converteixen en InitPlan/SubPlan. De vegades es converteixen en ... Join o ... Anti Join, sobretot quan escrius alguna cosa així WHERE NOT EXISTS .... I aquí no sempre és possible combinar-los: al text del pla no hi ha operadors corresponents als nodes del pla.
De nou tasca "amb un asterisc": alguns VALUES en la petició. En aquest cas i en el pla obtindreu diversos nodes Values Scan.
Els sufixos "numerats" ajudaran a distingir-los els uns dels altres: s'afegeixen exactament en l'ordre en què es troben els corresponents VALUES-bloques al llarg de la sol·licitud de dalt a baix.
Processament de dades
Sembla que tot el que hi ha a la nostra sol·licitud s'ha resolt; tot el que queda és Limit.
Però aquí tot és senzill: nodes com Limit, Sort, Aggregate, WindowAgg, Unique “mapa” un a un als operadors corresponents a la sol·licitud, si hi són. Aquí no hi ha "estrelles" ni dificultats.
JOIN
Les dificultats sorgeixen quan volem combinar JOIN entre ells mateixos. Això no sempre és possible, però sí.
Des del punt de vista de l'analitzador de consultes, tenim un node JoinExpr, que té exactament dos fills: esquerra i dreta. Això, en conseqüència, és el que hi ha "a sobre" del vostre JOIN i el que hi ha escrit "a sota" a la sol·licitud.
I des del punt de vista del pla, es tracta de dos descendents d'alguns * Loop/* Join-node. Nested Loop, Hash Anti Join,... - Alguna cosa així.
Utilitzem una lògica senzilla: si tenim taules A i B que "s'uneixen" entre si al pla, a la sol·licitud es podrien localitzar o bé A-JOIN-BO B-JOIN-A. Intentem combinar d'aquesta manera, intentem combinar a l'inrevés, i així successivament fins que ens quedem sense aquestes parelles.
Agafem el nostre arbre de sintaxi, prenem el nostre pla, mirem-los... no semblants!
Redibuixem-lo en forma de gràfics: oh, ja sembla alguna cosa!
Tingueu en compte que tenim nodes que tenen simultàniament fills B i C; no ens importa en quin ordre. Combinem-los i donem la volta a la imatge del node.
Tornem a mirar. Ara tenim nodes amb fills A i parells (B + C), també compatibles amb ells.
Genial! Resulta que som aquests dos JOIN de la sol·licitud amb els nodes del pla es van combinar amb èxit.
Per desgràcia, aquest problema no sempre es resol.
Per exemple, si en una sol·licitud A JOIN B JOIN C, i al pla, en primer lloc, es van connectar els nodes "exteriors" A i C. Però no hi ha aquest operador a la sol·licitud, no tenim res a destacar, res a què adjuntar una pista. Passa el mateix amb la "coma" quan escrius A, B.
Però, en la majoria dels casos, gairebé tots els nodes es poden "deslligar" i podeu obtenir aquest tipus de perfils a l'esquerra a temps, literalment, com a Google Chrome quan analitzeu el codi JavaScript. Podeu veure quant de temps va trigar cada línia i cada instrucció a "executar-se".
I per fer-te més còmode l'ús de tot això, hem fet emmagatzematge Arxiu, on podeu desar i després trobar els vostres plans juntament amb les sol·licituds associades o compartir l'enllaç amb algú.
Si només necessiteu portar una consulta il·legible en una forma adequada, feu servir el nostre "normalitzador".