PostgreSQL Query Profiler: comment faire correspondre le plan et la requĂȘte

Beaucoup de ceux qui utilisent dĂ©jĂ  expliquer.tensor.ru - notre service de visualisation des plans PostgreSQL, peuvent ne pas ĂȘtre au courant de l'une de ses super-capacitĂ©s - pour transformer un morceau difficile Ă  lire d'un journal de serveur ...





... dans une requĂȘte bien conçue avec des conseils contextuels pour les nƓuds de plan correspondants:





Dans cette transcription de la deuxiÚme partie de mon discours à PGConf.Russia 2020, je vais vous dire comment nous avons réussi à faire cela.

La transcription de la premiĂšre partie, qui traite des problĂšmes typiques de performance des requĂȘtes et de leurs solutions, se trouve dans l'article «Recettes pour les requĂȘtes SQL dĂ©faillantes» .




Tout d'abord, nous allons peindre - et nous ne peindrons plus le plan, nous l'avons déjà peint, nous l'avons déjà beau et compréhensible, mais une demande.



Il nous a semblĂ© que la requĂȘte extraite du journal en utilisant une "feuille" non formatĂ©e semble trĂšs moche et donc peu pratique.





Surtout quand les dĂ©veloppeurs dans le code "collent" le corps de la requĂȘte (c'est bien sĂ»r un anti-pattern, mais ça arrive) sur une seule ligne. Horreur!



Dessinons-le en quelque sorte plus magnifiquement.





Et si nous pouvons le dessiner magnifiquement, c'est-Ă -dire dĂ©monter et remonter le corps de la requĂȘte, nous pouvons alors attacher un indice Ă  chaque objet de cette requĂȘte - ce qui s'est passĂ© au point correspondant du plan.



Arborescence de requĂȘtes de syntaxe



Pour ce faire, la demande doit d'abord ĂȘtre analysĂ©e.





Étant donnĂ© que notre noyau systĂšme fonctionne sur NodeJS , nous avons crĂ©Ă© des modules pour cela, vous pouvez le trouver sur GitHub . En fait, ce sont des "liaisons" Ă©tendues aux composants internes de l'analyseur PostgreSQL lui-mĂȘme. Autrement dit, la grammaire est simplement compilĂ©e en binaire et des liaisons y sont effectuĂ©es du cĂŽtĂ© NodeJS. Nous avons pris les modules d'autres personnes comme base - il n'y a pas de grand secret ici.



Nous transmettons le corps de la requĂȘte Ă  l'entrĂ©e de notre fonction - Ă  la sortie, nous obtenons un arbre de syntaxe analysĂ© sous la forme d'un objet JSON.





Vous pouvez maintenant parcourir cet arbre dans la direction opposĂ©e et collecter la requĂȘte avec les retraits, la coloration, la mise en forme que nous voulons. Non, ce n'est pas configurable, mais il nous a semblĂ© que ce serait pratique.





RequĂȘte de mappage et nƓuds de plan



Voyons maintenant comment nous pouvons combiner le plan que nous avons analysĂ© dans la premiĂšre Ă©tape et la requĂȘte que nous avons analysĂ©e dans la seconde.



Prenons un exemple simple - nous avons une requĂȘte qui gĂ©nĂšre un CTE et le lit deux fois. Il gĂ©nĂšre un tel plan.





CTE



Si vous regardez attentivement, avant la 12e version (ou à partir de celle-ci avec le mot-clé MATERIALIZED), la formation de CTE est une barriÚre absolue pour le planificateur .





Cela signifie que si nous voyons la gĂ©nĂ©ration de CTE quelque part dans la requĂȘte et quelque part dans le plan un nƓud CTE, alors ces nƓuds "se battent" dĂ©finitivement les uns avec les autres, nous pouvons immĂ©diatement les combiner.



ProblĂšme d'astĂ©risque : les CTE peuvent ĂȘtre imbriquĂ©s.



Il y a trĂšs mal imbriquĂ©, et mĂȘme les mĂȘmes noms. Par exemple, vous pouvez le CTE Afaire Ă  l' intĂ©rieur CTE X, et CTE Ble refaire au mĂȘme niveau Ă  l'intĂ©rieur CTE X:



WITH A AS (
  WITH X AS (...)
  SELECT ...
)
, B AS (
  WITH X AS (...)
  SELECT ...
)
...


Vous devez comprendre cela lorsque vous comparez. Il est trĂšs difficile de comprendre cela avec des «yeux» - mĂȘme en voyant le plan, voire en voyant le corps de la demande. Si votre gĂ©nĂ©ration CTE est complexe, imbriquĂ©e, les demandes sont volumineuses, alors elle est complĂštement inconsciente.



SYNDICAT



Si nous avons un mot-clĂ© dans la requĂȘte UNION [ALL](l'opĂ©rateur de jonction de deux sĂ©lections), alors soit un nƓud Appendou un autre lui correspond dans le plan Recursive Union.





Ce qui est "au-dessus" est au-dessus UNIONest le premier enfant de notre nƓud, ce qui est "en dessous" est le second. Si UNIONplusieurs blocs sont "collĂ©s" Ă  travers nous Ă  la fois, il Appendn'y aura toujours qu'un seul nƓud, mais il n'aura pas deux enfants, mais plusieurs - dans l'ordre au fur et Ă  mesure, respectivement:



  (...) -- #1
UNION ALL
  (...) -- #2
UNION ALL
  (...) -- #3


Append
  -> ... #1
  -> ... #2
  -> ... #3


ProblÚme "avec un astérisque" : à l'intérieur de la génération d'une sélection récursive ( WITH RECURSIVE), il peut aussi y en avoir plusieurs UNION. Mais seul le tout dernier bloc aprÚs le dernier est toujours récursif UNION. Tout ce qui précÚde est un mais différent UNION:



WITH RECURSIVE T AS(
  (...) -- #1
UNION ALL
  (...) -- #2,      
UNION ALL
  (...) -- #3,          T
)
...


Vous devez Ă©galement ĂȘtre capable de «coller» de tels exemples. Dans cet exemple, nous voyons qu'il UNIONy avait 3 segments dans notre requĂȘte. En consĂ©quence, l'un UNION correspond Ă  un Append-node, et l'autre correspond Ă  Recursive Union.





Données en lecture-écriture



Ça y est, on l'Ă©tale, maintenant on sait quelle partie de la demande correspond Ă  quelle partie du plan. Et dans ces piĂšces, nous pouvons facilement et naturellement trouver ces objets qui sont «lisibles».



Du point de vue de la requĂȘte, on ne sait pas s'il s'agit d'une table ou d'un CTE, mais ils sont dĂ©signĂ©s par le mĂȘme nƓud RangeVar. Et en termes de "lisible" - c'est aussi un ensemble assez limitĂ© de nƓuds:



  • 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]


Nous connaissons la structure du plan et de la requĂȘte, nous connaissons la correspondance des blocs, nous connaissons les noms des objets - nous faisons une comparaison sans ambiguĂŻtĂ©.





Encore une fois, un problĂšme d'astĂ©risque . Nous prenons la requĂȘte, l'exĂ©cutons, nous n'avons pas d'alias - nous la lisons juste deux fois Ă  partir d'un CTE.





Nous regardons le plan - quel est le problÚme? Pourquoi notre pseudonyme est-il sorti? Nous ne l'avons pas commandé. Pourquoi est-il si "numéroté"?



PostgreSQL l'ajoute lui-mĂȘme. Vous avez juste besoin de comprendre qu'un tel alias n'a aucun sens pour nous Ă  des fins de comparaison avec le plan, il est simplement ajoutĂ© ici. Ne faisons pas attention Ă  lui.



La deuxiĂšme tĂąche est "avec un astĂ©risque" : si nous lisons Ă  partir d'une table partitionnĂ©e, alors nous obtiendrons un nƓud AppendouMerge Append, Qui se composera d'un grand nombre de « enfants », et dont chacun est en quelque sorte Scan« e de la section de la table: Seq Scan, Bitmap Heap Scanou Index Scan. Mais, dans tous les cas, ces "enfants" ne seront pas des requĂȘtes complexes - c'est ainsi que ces nƓuds peuvent ĂȘtre distinguĂ©s de Appendquand UNION.





Nous comprenons Ă©galement ces nƓuds, nous les rassemblons «en une seule pile» et disons: « tout ce que vous lisez Ă  partir de mĂ©gatable est ici et en bas de l'arbre ».



NƓuds "simples" pour recevoir des donnĂ©es







Values Scandans le plan correspond VALUESĂ  la demande.



Result- c'est une demande sans FROMpareil SELECT 1. Ou lorsque vous avez une expression sciemment fausse dans le WHERE-bloc (alors l'attribut se produit One-Time Filter):



EXPLAIN ANALYZE
SELECT * FROM pg_class WHERE FALSE; --  0 = 1


Result  (cost=0.00..0.00 rows=0 width=230) (actual time=0.000..0.000 rows=0 loops=1)
  One-Time Filter: false


Function Scan"Mapper" Ă  la SRF du mĂȘme nom.



Mais avec les requĂȘtes imbriquĂ©es, tout est plus compliquĂ© - malheureusement, elles ne se transforment pas toujours en InitPlan/ SubPlan. Parfois, ils se transforment en ... Joinou ... Anti Join, surtout lorsque vous Ă©crivez quelque chose comme WHERE NOT EXISTS .... Et il n'est pas toujours possible de combiner lĂ -bas - il n'y a pas d'opĂ©rateurs correspondant aux nƓuds du plan dans le texte du plan.



Encore une fois, une tĂąche avec un astĂ©risque : plusieurs VALUESdans la requĂȘte. Dans ce cas et dans le plan, vous recevrez plusieurs nƓuds Values Scan.





Les suffixes "numĂ©rotĂ©s" aideront Ă  les distinguer les uns des autres - ils sont ajoutĂ©s exactement dans l'ordre de recherche des VALUESblocs correspondants le long de la requĂȘte de haut en bas.



Traitement de l'information



Il semble que tout dans notre demande a été réglé - il ne reste que Limit.





Mais tout est simple - comme les noeuds Limit, Sort, Aggregate, WindowAgg, Unique« mapyatsya » one-to-one aux déclarations correspondantes dans la demande, si elles sont là. Il n'y a pas de "stars" et pas de difficultés.





JOINDRE



Des difficultés surviennent lorsque nous voulons combiner les JOINuns avec les autres. Ce n'est pas toujours fait, mais vous le pouvez.





Du point de vue de l'analyseur de requĂȘtes, nous avons un nƓud JoinExprqui a exactement deux enfants - gauche et droite. Ceci, respectivement, est ce qui est "au-dessus" de votre JOIN et ce qui est "sous" dans la requĂȘte est Ă©crit.



Et du point de vue du plan, ce sont deux descendants de certains * Loop/ * Join-node. Nested Loop, Hash Anti Join... - c'est quelque chose.



Utilisons une logique simple: si nous avons des plaques A et B qui "se rejoignent" dans le plan, alors dans la requĂȘte elles pourraient ĂȘtre localisĂ©es soit A-JOIN-Bou B-JOIN-A. Essayons de le combiner de cette façon, essayons de le combiner dans l'autre sens, et ainsi de suite jusqu'Ă  ce que ces paires soient Ă©puisĂ©es.



Prenez notre arbre de syntaxe, prenez notre aperçu, regardez-les ... ça ne lui ressemble pas!





Redessinons-le sous forme de graphiques - oh, c'est déjà devenu quelque chose comme quelque chose!





Remarquons que nous avons des nƓuds qui ont des enfants B et C en mĂȘme temps - peu importe dans quel ordre. Combinons-les et retournons l'image du nƓud.





Voyons cela à nouveau. Maintenant, nous avons des nƓuds avec des enfants A et des paires (B + C) - compatibles avec eux aussi.





Excellent! Il s'avĂšre que nous avons JOINrĂ©ussi Ă  combiner ces deux Ă©lĂ©ments de la requĂȘte avec les nƓuds du plan.



Hélas, cette tùche n'est pas toujours résolue.





Par exemple, si dans la requĂȘte A JOIN B JOIN C, mais dans le plan, les nƓuds "extrĂȘmes" A et C Ă©taient connectĂ©s en premier. Et dans la requĂȘte, il n'y a pas d'opĂ©rateur de ce type, nous n'avons rien Ă  mettre en Ă©vidence, il n'y a rien auquel lier l'indication. C'est la mĂȘme chose avec la "virgule" lorsque vous Ă©crivez A, B.



Mais, dans la plupart des cas, presque tous les nƓuds peuvent ĂȘtre «dĂ©liĂ©s» et vous obtenez ce type de profilage Ă  gauche dans le temps - littĂ©ralement, comme dans Google Chrome, lorsque vous analysez le code JavaScript. Vous pouvez voir combien de temps chaque ligne et chaque instruction ont Ă©tĂ© "exĂ©cutĂ©es".





Et pour que vous puissiez utiliser tout cela plus facilement, nous avons créé un stockage d' archives , dans lequel vous pouvez enregistrer puis retrouver vos plans avec les demandes associées ou partager un lien avec quelqu'un.



Si vous avez juste besoin de mettre une requĂȘte illisible sous une forme adĂ©quate, utilisez notre "normalisateur" .






All Articles