许多人已经在使用
...到一个设计精美的查询,其中包含相应计划节点的上下文提示:
在他的第二部分的记录中
第一部分的记录,专门讨论典型的查询性能问题及其解决方案,可以在文章中找到
“解决 SQL 查询问题的秘诀” .
首先,让我们开始着色 - 我们将不再为计划着色,我们已经为它着色,我们已经拥有它美丽且易于理解的,但一个请求。
在我们看来,使用这样一个未格式化的“表”,从日志中提取的请求看起来非常难看,因此不方便。
特别是当开发人员将代码中的请求正文“粘合”在一行中时(当然,这是一种反模式,但它确实发生了)。 可怕!
让我们把它画得更漂亮一些。
如果我们能把它画得很漂亮,也就是说,将请求的主体拆解并重新组合在一起,那么我们就可以给这个请求的每个对象“附加”一个提示——计划中相应点发生了什么。
查询语法树
为此,必须首先解析请求。
因为我们有
我们将请求正文作为函数的输入 - 在输出中,我们得到 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]
(连接两个样本的运算符),那么在计划中它对应于一个节点 Append
,或一些 Recursive Union
.
上面的“上面” 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
.
读写数据
一切都已安排好,现在我们知道请求的哪一部分对应于计划的哪一部分。 而在这些作品中,我们可以轻松、自然地找到那些“可读”的对象。
从查询的角度来看,我们不知道它是表还是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]
我们知道计划和查询的结构,我们知道块的对应关系,我们知道对象的名称 - 我们进行一对一的比较。
再次 任务“带星号”。 我们接受请求,执行它,我们没有任何别名 - 我们只是从同一个 CTE 读取它两次。
我们看看这个计划——问题出在哪里? 为什么我们有别名? 我们没有订购。 他从哪里得到这样的“号码”?
PostgreSQL 自己添加它。 你只需要明白这一点 只是这样一个别名 对于我们来说,为了与计划进行比较,它没有任何意义,只是在这里添加。 我们不要关注他。
第二个 任务“带星号”:如果我们从分区表中读取,那么我们将得到一个节点 Append
или Merge Append
,它将由大量的“孩子”组成,并且每个孩子都会以某种方式 Scan
'om 来自表部分: Seq Scan
, Bitmap Heap Scan
или Index Scan
。 但是,无论如何,这些“子节点”都不会是复杂的查询——这就是这些节点与其他节点的区别 Append
在 UNION
.
我们也了解这样的结,将它们“堆成一堆”并说:“你从 megatable 读到的所有内容都在这里,在树下".
“简单”的数据接收节点
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
.
“编号”后缀将有助于区分它们 - 它们是按照找到相应后缀的顺序精确添加的 VALUES
-从上到下阻止请求。
数据处理
看来我们的请求中的所有内容都已解决 - 剩下的就是 Limit
.
但这里一切都很简单 - 例如节点 Limit
, Sort
, Aggregate
, WindowAgg
, Unique
一对一地“映射”到请求中相应的运算符(如果存在)。 这里没有“明星”或困难。
注册
当我们想要结合时就会遇到困难 JOIN
他们之间。 这并不总是可能的,但它是可能的。
从查询解析器的角度来看,我们有一个节点 JoinExpr
,它正好有两个孩子 - 左和右。 因此,这就是请求中 JOIN“上方”和其“下方”所写的内容。
从计划的角度来看,这是某些人的两个后代 * Loop
/* Join
-节点。 Nested Loop
, Hash Anti 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