PostgreSQL Query Profiler: как да съпоставим план и заявка

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

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

PostgreSQL Query Profiler: как да съпоставим план и заявка
В това представяне на втората част от своя доклад на PGConf.Russia 2020 ще обясня как успяхме да го направим.

С транскрипцията на първата част, посветена на типичните проблеми с производителността на запитванията и техните решения, можете да се запознаете в статията „Рецепти за 'болни' SQL запитвания“.


Възпроизведи видео

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

Намери ни се, че така неформатираната 'постеля', изтеглена от лога, изглежда твърде грозно и затова — неудобно.
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

Нека да го нарисуваме по-приятно.
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

Синтактичното дърво на запитването

За да го направим, запитването първо трябва да бъде анализирано.
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

Вкарваме тялото на запитването в нашата функция — на изхода получаваме анализа на синтактичното дърво под формата на JSON обект.
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

Съпоставяне на възлите на запитването и плана

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

Нека вземем един прост пример — имаме заявка, която формира CTE и два пъти чете от нея. Това генерира такъв план.
PostgreSQL Query Profiler: как да съпоставим план и заявка

CTE

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

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

Задача с 'звездица': CTE могат да бъдат вложени.
PostgreSQL Query Profiler: как да съпоставим план и заявка
Има много лошо вложени, и дори именувани по един и същи начин. Например, можете вътре в 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] (оператор за съединяване на две селекции), то му отговаря в плана или възел Добави, или някакво Рекурсивен съюз.
PostgreSQL Query Profiler: как да съпоставим план и заявка

То, което е 'отгоре' над UNION — това е първият потомък на нашия възел, а 'отдолу' — вторият. Ако през UNION имаме 'залепени' няколко блока наведнъж, то Добави-възелът все още ще бъде само един, а децата му ще бъдат не два, а много, в реда, по който идват, съответно:

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

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

Задача с 'звездица': вътре в генерирането на рекурсивна селекция (WITH RECURSIVE) също може да има повече от един UNION. Но винаги рекурсивен е само последният блок след последния UNION. Всичко, което е по-горе — това е един, но друг UNION:

WITH RECURSIVE T AS(
  (...) -- #1
UNION ALL
  (...) -- #2, тук завършва генерирането на стартовото състояние на рекурсията
UNION ALL
  (...) -- #3, само този блок е рекурсивен и може да съдържа обращение към T
)
...

Такива примери също трябва да умеем да 'разлепваме'. В този пример виждаме, че UNION-сегменти в нашата заявка е имало 3 броя. Съответно, на един UNION отговаря на Добави-възел, а на другия — Рекурсивен съюз.
PostgreSQL Query Profiler: как да съпоставим план и заявка

Четене-запис на данни

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

От гледна точка на заявката не знаем — дали е таблица или CTE, но се означават с един и същ възел. Диапазонна променлива. А в плана «читается» — това също е достатъчно ограничен набор от възли:

  • Последователно сканиране в [tbl]
  • Скенер на битмап хип в [tbl]
  • Индекс [Само] Сканиране [Назад] използвайки [idx] на [tbl]
  • CTE Сканиране на [cte]
  • Вмъкнете/Актуализация/Delete в [tbl]

Структурата на плана и запитването знаем, съвпадението на блоковете знаем, имената на обектите знаем — правим еднозначно съпоставяне.
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

Гледаме в плана — каква е бедата? Защо алиасът ни се появи? Не сме го поръчвали. Откъде е такъв „номерни“?

PostgreSQL сам го добавя. Трябва просто да разберем, че точно такъв алиас за нас за целите на съпоставянето с плана няма никакъв смисъл, той просто е добавен тук. Да не обръщаме внимание на него.

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

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

„Простите“ възли за получаване на данни

PostgreSQL Query Profiler: как да съпоставим план и заявка

Сканиране на стойности в плана съответстват 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 в запитването. В този случай и в плана ще получите няколко възли Сканиране на стойности.
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

Обработка на данните

Изглежда всичко в нашето запитване разгледахме — остана само Лимит.
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

JOIN

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

От гледна точка на парсера на запитването, имаме възел Присъединете сеExpr, който има точно два потомка — ляв и десен. Това, съответно, е това, което е "над" вашия JOIN и това, което е "под" него в запитването.

А от гледна точка на плана, това са два потомка на някакъв * Цикъл/* Присъединете се-възел. Наследен цикъл, Хеш антиисо,… — нещо такова.

Нека използваме проста логика: ако имаме таблици A и B, които "джойтват" помежду си в плана, то в запитването те могат да бъдат разположени или A-JOIN-B, или B-JOIN-A. Нека опитаме да ги комбинираме така, да опитаме да комбинираме обратно, и така, докато такова съвпадение не свърши.

Нека вземем нашето синтактично дърво, вземем нашия план, да ги разгледаме… не изглежда добре!
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

Нека обърнем внимание, че имаме възли, които имат деца B и C — не е важно в какъв ред. Нека ги комбинираме и да обърнем картинката на възела.
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

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

За съжаление, тази задача не се решава винаги.
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

Но, в повечето случаи, почти всички възли успяват да бъдат "разплетени" и да получим такова профилиране отляво по време — буквално, както в Google Chrome, когато анализирате код на JavaScript. Виждате колко време е изпълнявал всеки ред и всяка операция.
PostgreSQL Query Profiler: как да съпоставим план и заявка

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

Ако просто трябва да приведете нечетимо запитване в адекватен вид, използвайте нашия "нормализатор".

PostgreSQL Query Profiler: как да съпоставим план и заявка

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

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster