PostgreSQL 查询分析器:如何匹配计划和查询

许多人已经在使用 解释.tensor.ru - 我们的 PostgreSQL 计划可视化服务可能不知道它的超能力之一 - 转动服务器日志中难以阅读的部分......

PostgreSQL 查询分析器:如何匹配计划和查询
...到一个设计精美的查询,其中包含相应计划节点的上下文提示:

PostgreSQL 查询分析器:如何匹配计划和查询
在他的第二部分的记录中 PGConf.Russia 2020 报告 我会告诉你我们是如何做到这一点的。

第一部分的记录,专门讨论典型的查询性能问题及其解决方案,可以在文章中找到 “解决 SQL 查询问题的秘诀”.



首先,让我们开始着色 - 我们将不再为计划着色,我们已经为它着色,我们已经拥有它美丽且易于理解的,但一个请求。

在我们看来,使用这样一个未格式化的“表”,从日志中提取的请求看起来非常难看,因此不方便。
PostgreSQL 查询分析器:如何匹配计划和查询

特别是当开发人员将代码中的请求正文“粘合”在一行中时(当然,这是一种反模式,但它确实发生了)。 可怕!

让我们把它画得更漂亮一些。
PostgreSQL 查询分析器:如何匹配计划和查询

如果我们能把它画得很漂亮,也就是说,将请求的主体拆解并重新组合在一起,那么我们就可以给这个请求的每个对象“附加”一个提示——计划中相应点发生了什么。

查询语法树

为此,必须首先解析请求。
PostgreSQL 查询分析器:如何匹配计划和查询

因为我们有 系统核心运行在NodeJS上,然后我们给它做了一个模块,你可以 在 GitHub 上找到它。 事实上,这些是对 PostgreSQL 解析器本身内部的扩展“绑定”。 也就是说,语法只是二进制编译的,并且从 NodeJS 对其进行绑定。 我们以其他人的模块为基础 - 这里没有什么大秘密。

我们将请求正文作为函数的输入 - 在输出中,我们得到 JSON 对象形式的已解析语法树。
PostgreSQL 查询分析器:如何匹配计划和查询

现在我们可以以相反的方向遍历这棵树,并使用我们想要的缩进、颜色和格式来组合请求。 不,这不是可定制的,但在我们看来这会很方便。
PostgreSQL 查询分析器:如何匹配计划和查询

映射查询和计划节点

现在让我们看看如何将第一步分析的计划和第二步分析的查询结合起来。

让我们举一个简单的例子 - 我们有一个生成 CTE 并从中读取两次的查询。 他制定了这样一个计划。
PostgreSQL 查询分析器:如何匹配计划和查询

CTE

如果你仔细看一下,直到版本 12(或者从它开始使用关键字 MATERIALIZED) 形成 CTE对于规划者来说是绝对的障碍.
PostgreSQL 查询分析器:如何匹配计划和查询

这意味着,如果我们在请求中的某处看到 CTE 生成,并且在计划中的某处看到节点 CTE,那么这些节点肯定是互相“打架”的,我们可以立即将它们组合起来。

星号的问题:CTE 可以嵌套。
PostgreSQL 查询分析器:如何匹配计划和查询
其中有嵌套很差的,甚至有同名的。 例如,您可以在里面 CTE ACTE X,并且在同一水平内部 CTE B 再来一遍 CTE X:

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

对比的时候你一定要明白这一点。 “用眼睛”理解这一点——甚至看到计划,甚至看到请求的正文——是非常困难的。 如果你的 CTE 生成是复杂的、嵌套的,并且请求很大,那么它是完全无意识的。

UNION

如果我们在查询中有一个关键字 UNION [ALL] (连接两个样本的运算符),那么在计划中它对应于一个节点 Append,或一些 Recursive Union.
PostgreSQL 查询分析器:如何匹配计划和查询

上面的“上面” UNION - 这是我们节点的第一个后代,它位于“下面” - 第二个。 如果通过 UNION 我们同时“粘”了几个块,然后 Append-仍然只有一个节点,但它不会有两个,而是许多子节点 - 按照它们的顺序分别:

  (...) -- #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 对应于 Append-节点,以及另一个 - Recursive Union.
PostgreSQL 查询分析器:如何匹配计划和查询

读写数据

一切都已安排好,现在我们知道请求的哪一部分对应于计划的哪一部分。 而在这些作品中,我们可以轻松、自然地找到那些“可读”的对象。

从查询的角度来看,我们不知道它是表还是CTE,但它们是由同一个节点指定的 RangeVar。 就“可读性”而言,这也是一组相当有限的节点:

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

我们知道计划和查询的结构,我们知道块的对应关系,我们知道对象的名称 - 我们进行一对一的比较。
PostgreSQL 查询分析器:如何匹配计划和查询

再次 任务“带星号”。 我们接受请求,执行它,我们没有任何别名 - 我们只是从同一个 CTE 读取它两次。
PostgreSQL 查询分析器:如何匹配计划和查询

我们看看这个计划——问题出在哪里? 为什么我们有别名? 我们没有订购。 他从哪里得到这样的“号码”?

PostgreSQL 自己添加它。 你只需要明白这一点 只是这样一个别名 对于我们来说,为了与计划进行比较,它没有任何意义,只是在这里添加。 我们不要关注他。

第二个 任务“带星号”:如果我们从分区表中读取,那么我们将得到一个节点 Append или Merge Append,它将由大量的“孩子”组成,并且每个孩子都会以某种方式 Scan'om 来自表部分: Seq Scan, Bitmap Heap Scan или Index Scan。 但是,无论如何,这些“子节点”都不会是复杂的查询——这就是这些节点与其他节点的区别 AppendUNION.
PostgreSQL 查询分析器:如何匹配计划和查询

我们也了解这样的结,将它们“堆成一堆”并说:“你从 megatable 读到的所有内容都在这里,在树下".

“简单”的数据接收节点

PostgreSQL 查询分析器:如何匹配计划和查询

Values Scan 对应于计划 VALUES 在请求中。

Result 是一个没有的请求 FROM 排序 SELECT 1。 或者当你有故意虚假的表达 WHERE-block(然后属性出现 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 “映射”到同名的 SRF。

但对于嵌套查询,一切都变得更加复杂 - 不幸的是,它们并不总是变成 InitPlan/SubPlan。 有时他们会变成 ... Join или ... Anti Join,尤其是当你写类似的东西时 WHERE NOT EXISTS ...。 在这里,并不总是可以将它们组合起来 - 在计划的文本中,没有与计划的节点相对应的运算符。

再次 任务“带星号”: 一些 VALUES 在请求中。 在这种情况下和计划中,您将获得多个节点 Values Scan.
PostgreSQL 查询分析器:如何匹配计划和查询

“编号”后缀将有助于区分它们 - 它们是按照找到相应后缀的顺序精确添加的 VALUES-从上到下阻止请求。

数据处理

看来我们的请求中的所有内容都已解决 - 剩下的就是 Limit.
PostgreSQL 查询分析器:如何匹配计划和查询

但这里一切都很简单 - 例如节点 Limit, Sort, Aggregate, WindowAgg, Unique 一对一地“映射”到请求中相应的运算符(如果存在)。 这里没有“明星”或困难。
PostgreSQL 查询分析器:如何匹配计划和查询

注册

当我们想要结合时就会遇到困难 JOIN 他们之间。 这并不总是可能的,但它是可能的。
PostgreSQL 查询分析器:如何匹配计划和查询

从查询解析器的角度来看,我们有一个节点 JoinExpr,它正好有两个孩子 - 左和右。 因此,这就是请求中 JOIN“上方”和其“下方”所写的内容。

从计划的角度来看,这是某些人的两个后代 * Loop/* Join-节点。 Nested Loop, Hash Anti Join,... - 类似的东西。

让我们使用简单的逻辑:如果我们有表 A 和 B 在计划中相互“连接”,那么在请求中它们可以位于 A-JOIN-BB-JOIN-A。 让我们尝试以这种方式组合,让我们尝试以相反的方式组合,依此类推,直到我们用完这样的对。

让我们看看我们的语法树,看看我们的计划,看看它们......不相似!
PostgreSQL 查询分析器:如何匹配计划和查询

让我们以图表的形式重新绘制它——哦,它已经看起来像什么了!
PostgreSQL 查询分析器:如何匹配计划和查询

请注意,我们的节点同时具有子节点 B 和 C - 我们不关心按什么顺序。 我们把它们组合起来,把节点的图片翻过来。
PostgreSQL 查询分析器:如何匹配计划和查询

我们再看一下。 现在我们有了带有子 A 和对 (B + C) 的节点 - 也与它们兼容。
PostgreSQL 查询分析器:如何匹配计划和查询

伟大的! 原来我们就是这两个人 JOIN 来自请求与计划节点已成功合并。

唉,这个问题并不总能得到解决。
PostgreSQL 查询分析器:如何匹配计划和查询

例如,如果在请求中 A JOIN B JOIN C,在计划中,首先连接了“外部”节点A和C,但是请求中没有这样的操作符,我们没有什么可以强调的,没有什么可以附加提示的。 和你写的时候的“逗号”是一样的 A, B.

但是,在大多数情况下,几乎所有节点都可以“解开”,并且您可以及时获得左侧的这种分析 - 从字面上看,就像在 Google Chrome 中分析 JavaScript 代码时一样。 您可以看到每行和每条语句“执行”所需的时间。
PostgreSQL 查询分析器:如何匹配计划和查询

为了让您更方便地使用这一切,我们做了存储 档案,您可以在其中保存并稍后查找您的计划以及相关的请求,或者与某人共享链接。

如果您只需要将不可读的查询转换为适当的形式,请使用 我们的“正常化器”.

PostgreSQL 查询分析器:如何匹配计划和查询

来源: habr.com

添加评论