Molti di coloro che già utilizzano — il nostro servizio di visualizzazione dei piani PostgreSQL, potrebbero non essere a conoscenza di una delle sue super capacità: trasformare un pezzo di log del server difficilmente leggibile…

… in una query elegantemente formattata con suggerimenti contestuali sui nodi corrispondenti del piano:

In questa spiegazione della seconda parte del mio vi racconterò come siamo riusciti a farlo.
Il trascrizione della prima parte, incentrata sui problemi di prestazioni delle query e le loro soluzioni, può essere consultata nell'articolo .

Iniziamo con l'evidenziazione: questa volta non andremo a colorare il piano, poiché già lo abbiamo reso bello e comprensibile, ma ci concentreremo sulla query.
Ci è sembrato che questo pezzo di codice non formattato estratto dal log appaia molto brutto e quindi — scomodo.

Soprattutto quando gli sviluppatori incollano il corpo della query (è, ovviamente, un antipattern, ma succede) in un'unica riga. Un incubo!
Diamo una forma più bella a questo.

E se riusciremo a rappresentarlo bene, cioè analizzare e ricostruire il corpo della richiesta, potremo poi "agganciare" un suggerimento a ciascun oggetto di questa richiesta, indicando cosa è successo nel punto corrispondente del piano.
Albero sintattico della richiesta
Per fare ciò, è necessario prima analizzare la richiesta.

Dato che , abbiamo creato un modulo per questo, potete . In realtà, si tratta di binding avanzati verso le interiora del parser di PostgreSQL. Cioè, è semplicemente una grammatica compilata in binario con binding lato NodeJS. Abbiamo preso come base moduli esistenti — qui non c'è un grande mistero.
Passiamo il corpo della richiesta alla nostra funzione — in uscita otteniamo un albero sintattico analizzato sotto forma di oggetto JSON.

Adesso, possiamo percorrere questo albero al contrario e ricostruire la richiesta con le spaziature, colorazioni e formattazioni che preferiamo. No, questo non è configurabile, ma ci è sembrato che in questo modo sarebbe comodo.

Mappatura dei nodi della richiesta e del piano
Ora vediamo come possiamo combinare il piano che abbiamo esaminato nel primo passaggio con la query che abbiamo analizzato nel secondo.
Prendiamo un esempio semplice: abbiamo una query che genera un CTE e lo legge due volte. Genera un piano di questo tipo.

CTE
Se lo guardiamo attentamente, fino alla versione 12 (o a partire da essa con la parola chiave MATERIALIZED) la generazione .

Quindi, se vediamo da qualche parte nella query la generazione di un CTE e da qualche parte nel piano un nodo CTE, questi nodi si «scontrano» certamente, possiamo immediatamente combinarli.
Il compito «con il stipite»: i CTE possono essere nidificati.

Possono essere nidificati molto male e persino con lo stesso nome. Ad esempio, puoi all'interno di CTE A creare CTE X, e allo stesso livello all'interno CTE B ri-creare di nuovo CTE X:
WITH A AS (
WITH X AS (...)
SELECT ...
)
, B AS (
WITH X AS (...)
SELECT ...
)
...Quando fai il corrispondenza, devi tenerlo a mente. Capirlo «con gli occhi» — anche vedendo il piano, anche vedendo il corpo della query — è molto difficile. Se hai una generazione di CTE complessa e nidificata, con query grandi — allora diventa del tutto inconscio.
UNION
Se nella nostra query c'è la parola chiave UNIONE [TUTTE] (operatore di connessione di due set di dati), quindi in questo piano corrisponde a un nodo Aggiungi, oppure a qualche altro Unione Ricorsiva.

Ciò che è "sopra" rispetto a UNION è il primo discendente del nostro nodo, mentre "sotto" è il secondo. Se abbiamo UNION incollato più blocchi contemporaneamente, allora Aggiungi-il nodo sarà comunque uno solo, mentre i suoi figli saranno più di due, in base all'ordine in cui si presentano:
(...) -- #1
UNION ALL
(...) -- #2
UNION ALL
(...) -- #3Append
-> ... #1
-> ... #2
-> ... #3
Il compito «con il stipite»: all'interno della generazione di una selezione ricorsiva (WITH RECURSIVE) può esserci più di uno UNION. Ma l'unico ricorsivo sarà sempre solo l'ultimo blocco dopo l'ultimo UNION. Tutto ciò che è sopra è uno, ma un altro UNION:
WITH RECURSIVE T AS(
(...) -- #1
UNION ALL
(...) -- #2, qui termina la generazione dello stato iniziale della ricorsione
UNION ALL
(...) -- #3, solo questo blocco è ricorsivo e può contenere un riferimento a T
)
... Esempi come questi devono anche essere "incollati". In questo esempio possiamo vedere che UNION-i segmenti nella nostra richiesta erano 3. Di conseguenza, uno per UNION corrisponde a Aggiungi-il nodo, e l'altro — Unione Ricorsiva.

Lettura-scrittura dei dati
Tutto è a posto, ora sappiamo a quale parte della richiesta corrisponde ciascun segmento del piano. E in questi segmenti possiamo facilmente e senza sforzo trovare quegli oggetti che vengono "letto".
Dal punto di vista della richiesta, non sappiamo se è una tabella o un CTE, ma vengono designati con lo stesso nodo. Varietà. E nel piano, "leggere" significa anche un insieme piuttosto limitato di nodi:
Scansione sequenziale su [tbl]Scansione di Bitmap Heap su [tbl]Indice [Solo] Scan [Inverso] utilizzando [idx] su [tbl]Scansione CTE su [cte]Inserisci/Update/Elimina su [tbl]
Conosciamo la struttura del piano e della richiesta, conosciamo le corrispondenze dei blocchi e i nomi degli oggetti — facciamo un'associazione univoca.

Ancora una volta, un compito "con la stellina". Prendiamo la richiesta, la eseguiamo, non abbiamo alias — abbiamo semplicemente letto dalla stessa CTE due volte.

Guardiamo nel piano — qual è il problema? Perché ci appare l'alias? Non lo abbiamo richiesto. Da dove viene questo "numerato"?
PostgreSQL lo aggiunge da solo. Bisogna semplicemente comprendere che questo alias specifico non ha alcun significato per noi ai fini dell'associazione con il piano, è stato semplicemente aggiunto qui. Non prestiamo attenzione ad esso.
La seconda un compito "con la stellina": se stiamo leggendo da una tabella sezionata, otteniamo un nodo Aggiungi o Unisci Aggiungi, che consisterà in un gran numero di "figli", ognuno dei quali sarà qualche Scan' scan dalla tabella-sezione: Scansione sequenziale, Scansione di Bitmap Heap o Index Scan. Ma, in ogni caso, questi «bambini» saranno richieste semplici — così possiamo distinguere questi nodi da Aggiungi con UNION.

Anche questi nodi li comprendiamo, li raggruppiamo «in un'unica soluzione» e diciamo: "tutto ciò che hai letto da megatable — è qui e in giù per l'albero".
Nodi di «semplice» acquisizione dei dati

Scansione dei valori rispetta VALUES nella richiesta.
Risultato — è una richiesta senza DA tipo SELECT 1. Oppure quando hai espressioni false per definizione nel DOVE-blocco (dove si genera l'attributo One-Time Filter):
EXPLAIN ANALYZE
SELECT * FROM pg_class WHERE FALSE; -- o 0 = 1Risultato (costo=0.00..0.00 righe=0 larghezza=230) (tempo reale=0.000..0.000 righe=0 loop=1)
One-Time Filter: false
Funzione Scansione «mappano» su SRF omonimi.
Ma con le query annidate è più complesso — sfortunatamente, non sempre si traducono in IniziaPiano/SubPiano. A volte si trasformano in ... Join o ... Anti Join, specialmente quando scrivi qualcosa come WHERE NOT EXISTS .... E lì combinare non sempre è possibile — nel testo del piano non ci sono nodi corrispondenti agli operatori del piano.
Ancora una volta, un compito "con la stellina": diversi VALUES nella richiesta. In questo caso ottieni diversi nodi anche nel piano Scansione dei valori.

Distinguere l'uno dall'altro aiuteranno i suffissi «numerici» — vengono aggiunti proprio nell'ordine in cui si trovano i corrispondenti VALUES-blocchi lungo il percorso della richiesta dall'alto in basso.
Elaborazione dei dati
Sembra che abbiamo analizzato tutto nella nostra richiesta — rimane solo Limite.

Ma qui tutto è semplice: nodi come Limite, Ordina, Aggregato, WindowAgg, Unique si «mappano» uno a uno con gli operatori corrispondenti nella query, se ci sono. Non ci sono «stelle» o complessità qui.

JOIN
Le difficoltà sorgono quando vogliamo combinare JOIN tra loro. Non è sempre facile, ma è possibile.

Dal punto di vista del parser della query, abbiamo un nodo UniscitiExpr, che ha esattamente due figli: sinistro e destro. Questo, rispettivamente, è ciò che si trova «sopra» il vostro JOIN e ciò che si trova «sotto» di esso nella query.
E dal punto di vista del piano sono due figli di un certo * Giro/* Unisciti-nodo. Nested Loop, Hash Anti Join,… — qualcosa del genere.
Usiamo una logica semplice: se abbiamo le tabelle A e B, che si «unite» tra loro nel piano, allora nella query potrebbero essere disposte o A-JOIN-B, oppure B-JOIN-A. Proviamo a combinarle in questo modo, proviamo a combinarle al contrario, e così via finché non arriviamo a fine delle coppie.
Prendiamo il nostro albero sintattico, prendiamo il nostro piano, guardiamo entrambi… non sembrano simili!

Ridisegniamo sotto forma di grafi — oh, ora somigliano a qualcosa!

Facciamo attenzione, abbiamo nodi che hanno allo stesso tempo figli B e C — non ci importa in che ordine. Combiniamoli e giriamo l'immagine del nodo.

Rivediamo. Ora abbiamo nodi con i figli A e coppie (B + C) - sono compatibili tra loro.

Ottimo! Quindi, abbiamo unito questi due JOIN dalla richiesta con i nodi del piano con successo.
Purtroppo, questa operazione non è sempre possibile.

Ad esempio, se nella richiesta A JOIN B JOIN C, ma nel piano i nodi "estremi" A e C si sono uniti per primi. E nella richiesta non c'è un operatore del genere, non abbiamo nulla da evidenziare, nessun suggerimento a cui agganciarsi. Lo stesso vale per la "virgola", quando scrivete A, B.
Ma, nella maggior parte dei casi, quasi tutti i nodi possono essere "disimpegnati" e ottenere un profilo come questo a sinistra nel tempo - proprio come in Google Chrome, quando analizzate il codice in JavaScript. Vedete quanto tempo ha impiegato ogni riga e ogni operatore.

E per rendervi tutto questo più comodo, abbiamo creato uno spazio di archiviazione , dove potete salvare e poi trovare i vostri piani insieme alle richieste associate o condividere un link con qualcuno.
Se invece avete bisogno di ripulire una richiesta illeggibile, utilizzate .

Fonte: habr.com
