Много от тези, които вече ползват — нашия сервис за визуализация на плановете в PostgreSQL, може би не знаят за една от неговите супер способности — да превръща трудно четим фрагмент от лог файл на сървъра…

… в красиво форматирано запитване с контекстуални подсказки за съответните възли на плана:

В това представяне на втората част от своя ще обясня как успяхме да го направим.
С транскрипцията на първата част, посветена на типичните проблеми с производителността на запитванията и техните решения, можете да се запознаете в статията .

Първо ще започнем с форматирането — и ще форматираме не плана, тъй като вече го направихме красив и разбираем, а запитването.
Намери ни се, че така неформатираната 'постеля', изтеглена от лога, изглежда твърде грозно и затова — неудобно.

Особено, когато разработчиците в кода 'залепват' тялото на запитването (това, разбира се, е антипатерн, но се случва) в един ред. Ужасно!
Нека да го нарисуваме по-приятно.

Ако можем да го нарисуваме красиво, тоест да разберем и отново да съберем тялото на запитването, ще можем и към всеки обект от това запитване да 'прикача' подсказка — какво е ставало в съответната точка на плана.
Синтактичното дърво на запитването
За да го направим, запитването първо трябва да бъде анализирано.

Тъй като , направихме модул за него, можете . Всъщност, това представлява разширени 'биндинги' към вътрешностите на парсера на самия PostgreSQL. Тоест, просто бинарно компилирана граматика и към нея са направени биндинги от страната на NodeJS. Взехме за основа чужди модули — тук няма голяма тайна.
Вкарваме тялото на запитването в нашата функция — на изхода получаваме анализа на синтактичното дърво под формата на JSON обект.

Сега можем да преминем по това дърво в обратна посока и да съберем запитването с точното форматиране, оцветяване и отстъпи, които желаем. Не, това не може да бъде настроено, но ни се стори, че точно така ще е удобно.

Съпоставяне на възлите на запитването и плана
Сега да видим как можем да комбинираме плана, който анализирахме в първата стъпка, и запитването, което анализирахме във втората.
Нека вземем един прост пример — имаме заявка, която формира CTE и два пъти чете от нея. Това генерира такъв план.

CTE
Ако внимателно се погледне, до 12-та версия (или започвайки от нея с ключовото слово MATERIALIZED) формирането .

И следователно, ако видим някъде в заявката генериране на CTE и някъде в плана възел CTE, тези възли определено ще 'удрят' един друг, можем веднага да ги комбинираме.
Задача с 'звездица': CTE могат да бъдат вложени.

Има много лошо вложени, и дори именувани по един и същи начин. Например, можете вътре в CTE A да създадете CTE X, и на същото ниво вътре CTE B да направите отново CTE X:
WITH A AS (
WITH X AS (...)
SELECT ...
)
, B AS (
WITH X AS (...)
SELECT ...
)
...При съпоставянето трябва да разберете това. Да разберете 'с очите си' — дори виждайки плана, дори виждайки тялото на заявката — е много трудно. Ако генерирането на CTE е сложно, вложено, а заявките големи — тогава дори и непознато.
UNION
Ако в нашата заявка има ключово слово UNION [ALL] (оператор за съединяване на две селекции), то му отговаря в плана или възел Добави, или някакво Рекурсивен съюз.

То, което е 'отгоре' над UNION — това е първият потомък на нашия възел, а 'отдолу' — вторият. Ако през UNION имаме 'залепени' няколко блока наведнъж, то Добави-възелът все още ще бъде само един, а децата му ще бъдат не два, а много, в реда, по който идват, съответно:
(...) -- #1
UNION ALL
(...) -- #2
UNION ALL
(...) -- #3Append
-> ... #1
-> ... #2
-> ... #3
Задача с 'звездица': вътре в генерирането на рекурсивна селекция (WITH RECURSIVE) също може да има повече от един UNION. Но винаги рекурсивен е само последният блок след последния UNION. Всичко, което е по-горе — това е един, но друг UNION:
WITH RECURSIVE T AS(
(...) -- #1
UNION ALL
(...) -- #2, тук завършва генерирането на стартовото състояние на рекурсията
UNION ALL
(...) -- #3, само този блок е рекурсивен и може да съдържа обращение към T
)
... Такива примери също трябва да умеем да 'разлепваме'. В този пример виждаме, че UNION-сегменти в нашата заявка е имало 3 броя. Съответно, на един UNION отговаря на Добави-възел, а на другия — Рекурсивен съюз.

Четене-запис на данни
Всичко, разложихме, сега знаем, който отделен фрагмент на заявката съответства на кой фрагмент от плана. И в тези фрагменти можем лесно и удобно да намерим тези обекти, които 'се четат'.
От гледна точка на заявката не знаем — дали е таблица или CTE, но се означават с един и същ възел. Диапазонна променлива. А в плана «читается» — това също е достатъчно ограничен набор от възли:
Последователно сканиране в [tbl]Скенер на битмап хип в [tbl]Индекс [Само] Сканиране [Назад] използвайки [idx] на [tbl]CTE Сканиране на [cte]Вмъкнете/Актуализация/Delete в [tbl]
Структурата на плана и запитването знаем, съвпадението на блоковете знаем, имената на обектите знаем — правим еднозначно съпоставяне.

Отново задачата „с звездица“. Вземаме запитването, изпълняваме, нямаме алиаси — просто два пъти прочетохме от едно CTE.

Гледаме в плана — каква е бедата? Защо алиасът ни се появи? Не сме го поръчвали. Откъде е такъв „номерни“?
PostgreSQL сам го добавя. Трябва просто да разберем, че точно такъв алиас за нас за целите на съпоставянето с плана няма никакъв смисъл, той просто е добавен тук. Да не обръщаме внимание на него.
Втора задачата „с звездица“: ако чете от секционирана таблица, ще получим възел Добави или Обединяване на добавки, който ще се състои от голямо количество „деца“, и всяко от тях ще бъде някакво Сканиране‘ от таблицата-секция: Последователно сканиране, Скенер на битмап хип или Индекс Сканиране. Но, в какъвто и случай, тези „деца“ няма да бъдат сложни запитвания — така тези възли могат да бъдат отличавани от Добави при UNION.

Тези възли също разбираме, събираме „в една купчина“ и казваме: "всичко, което прочетеш от megatable — е тук и надолу по дървото".
„Простите“ възли за получаване на данни

Сканиране на стойности в плана съответстват VALUES в запитването.
Резултат — това е запитване без FROM като SELECT 1. Или когато имате преднамерено фалшиво изразяване в WHERE-блока (тогава възниква атрибутът One-Time Filter):
EXPLAIN ANALYZE
SELECT * FROM pg_class WHERE FALSE; -- или 0 = 1Резултат (разходи=0.00..0.00 редове=0 ширина=230) (реално време=0.000..0.000 редове=0 цикли=1)
One-Time Filter: false
Функция Сканиране „мапят“ на едноименните SRF.
А с вложените запитвания всичко е по-сложно — за съжаление, те не винаги се превръщат в ИнициализирайПлана/Подплан. Понякога те се превръщат в ... Join или ... Anti Join, особено когато пишете нещо като WHERE NOT EXISTS .... И там комбинирането не винаги е възможно — в текста на плана съответстващите възли на плана нямат оператори.
Отново задачата „с звездица“: няколко VALUES в запитването. В този случай и в плана ще получите няколко възли Сканиране на стойности.

Да ги различим един от друг ще помогнат „номерните“ суфикси — те се добавят именно в реда на намиране на съответстващите VALUES-блокове по хода на запитването отгоре надолу.
Обработка на данните
Изглежда всичко в нашето запитване разгледахме — остана само Лимит.

Но тук всичко е просто — такива възли като Лимит, Сортиране, Агрегат, WindowAgg, Unique „мапят“ един-в-едно на съответстващите оператори в запитването, ако ги има. Тук няма „звезди“ и сложности.

JOIN
Сложностите възникват, когато искаме да комбинираме JOIN помежду си. Това не винаги е възможно, но става.

От гледна точка на парсера на запитването, имаме възел Присъединете сеExpr, който има точно два потомка — ляв и десен. Това, съответно, е това, което е "над" вашия JOIN и това, което е "под" него в запитването.
А от гледна точка на плана, това са два потомка на някакъв * Цикъл/* Присъединете се-възел. Наследен цикъл, Хеш антиисо,… — нещо такова.
Нека използваме проста логика: ако имаме таблици A и B, които "джойтват" помежду си в плана, то в запитването те могат да бъдат разположени или A-JOIN-B, или B-JOIN-A. Нека опитаме да ги комбинираме така, да опитаме да комбинираме обратно, и така, докато такова съвпадение не свърши.
Нека вземем нашето синтактично дърво, вземем нашия план, да ги разгледаме… не изглежда добре!

Да го прерисуваме като графове — о, вече започва да прилича на нещо!

Нека обърнем внимание, че имаме възли, които имат деца B и C — не е важно в какъв ред. Нека ги комбинираме и да обърнем картинката на възела.

Да видим отново. Сега имаме възли с деца A и двойки (B + C) — да ги комбинираме и тях.

Отлично! Означава, че сме комбинирали тези два JOIN от запитването с възлите на плана успешно.
За съжаление, тази задача не се решава винаги.

Например, ако в запитването A JOIN B JOIN C, а в плана първо са се комбинирали "крайните" възли A и C. А в запитването няма такъв оператор, нямаме какво да подчертаем, нямаме към какво да привържем подсказката. Същото е и с "запетаята", когато пишете A, B.
Но, в повечето случаи, почти всички възли успяват да бъдат "разплетени" и да получим такова профилиране отляво по време — буквално, както в Google Chrome, когато анализирате код на JavaScript. Виждате колко време е изпълнявал всеки ред и всяка операция.

А за да ви бъде всичко това по-удобно, създадохме хранилище , където можете да запазите и след това да намерите своите планове заедно с асоциираните запитвания или да споделите линк с някого.
Ако просто трябва да приведете нечетимо запитване в адекватен вид, използвайте .

Източник: habr.com
