如何通过子查询或连接来利用分区修剪?

Dea*_*gor 6 postgresql execution-plan partitioning postgresql-performance postgresql-13

我有一个分区表...

CREATE TABLE erco.rtprices
(
    scedtime timestamp with time zone NOT NULL,
    node_id integer NOT NULL,
    lmp numeric(12,6),
    CONSTRAINT rtprices_pkey PRIMARY KEY (scedtime, node_id)
) PARTITION BY LIST (node_id);
Run Code Online (Sandbox Code Playgroud)

每个都有node_id自己的分区。

如果我进行直接查询(第一个版本),例如:

explain select scedtime, lmp 
from erco.rtprices
where node_id = 11111
Run Code Online (Sandbox Code Playgroud)

然后该计划仅对rtprices_11111分区进行顺序扫描。 这就是我要的。

但是,如果我执行(第二个版本)查询,例如

explain select scedtime, lmp 
from erco.rtprices
inner join erco.nodes using (node_id)
where nodename = 'somename'
Run Code Online (Sandbox Code Playgroud)

那么该计划包括对每个分区进行顺序扫描,即使此查询与第一个查询一样有限制。

我尝试了上述查询的另一种形式(第三个版本)。

explain select scedtime, lmp 
from erco.rtprices
where node_id = (select node_id from erco.nodes where nodename='somename')
Run Code Online (Sandbox Code Playgroud)

它仍然对每个分区进行二次扫描。

如果我运行查询(而不是仅仅解释它们),那么毫不奇怪,后两个查询的时间大约是第一个查询的 5 倍(约 1 秒 vs 约 5 秒)。***我在这里犯了一个错误,见下文。

node_id是否有一种语法可以让我在事先不知道是什么的情况下获得第一个版本的计划/性能?

编辑:回答评论:

SHOW enable_partition_pruning;  -- on
Run Code Online (Sandbox Code Playgroud)

这样做的EXPLAIN ANALYZE结果是第一个仍然只扫描确切的分区。第二个继续扫描所有分区。第三个分区确实显示了(never executed)除所讨论的一个分区之外的所有分区。

但是等等,如果它实际上没有执行所有这些扫描,那么它怎么会花费与第二个一样长的时间呢?我心不在焉地将查询更改为包含两个节点,因此wheres 变为node_id in (11111, 11112)nodename in ('somenode', 'someothernode')node_id in (select node_id from erco.nodes where nodename in ('somenode', 'someothernode'))
当我EXPLAIN ANALYZE在包含 2 个节点的第三个版本上执行此操作时,它不再执行操作(never executed),而是扫描所有分区。
如果我成功了node_id = (select node_id from erco.nodes where nodename = 'somenode') or node_id = (select node_id from erco.nodes where nodename = 'someothernode'),分区会在运行时再次修剪。

总结一下

  • 使用直接node_id=xxxxnode_id in (xxxx,yyyy)将避免扫描不相关的分区

  • 使用连接表中的列始终会扫描所有分区

  • 使用node_id=(1 node_id subquery)将避免扫描不相关的分区

  • 使用node_id in (multiple node_id subquery)将扫描所有分区。

如果你喜欢冗长的内容......
https://textbin.net/dwwgy7rwx4

编辑#2: 我删除了现有的(scedtime,node_id)pkey,然后添加了(node_id,scedtime)pkey,我得到了相同的结果。我将 random_page_cost 设置为 1.1 并再次尝试查询,但仍然得到相同的结果。

对我来说,使用 WHERE node_id IN (子查询)仍然是扫描每个分区。即使子查询只有一个结果,它仍然会扫描所有分区。换句话说,做...

select scedtime, lmp 
from erco.rtprices
where node_id = (select node_id from erco.nodes where nodename='somename')
Run Code Online (Sandbox Code Playgroud)

效果很好,但将其更改为

select scedtime, lmp 
from erco.rtprices
where node_id in (select node_id from erco.nodes where nodename='somename')
Run Code Online (Sandbox Code Playgroud)

使其扫描所有分区。

我尝试在我的数据库上执行 Erwin 的 dbfiddle 中的所有玩具示例,它们的行为都与 dbfiddle 一样。

我尝试将 random_page_cost 设置为越来越低的值,甚至低至 0.00001,并且仍然使用 IN(子查询)让它扫描所有分区。

我尝试创建一个新的节点表,其中仅包含有问题的 2 个节点。

我尝试对表格进行完全真空分析,认为可能需要新的统计数据。

我尝试重新创建 rtprices 表,认为创建索引时需要正确。

这些都没有什么区别。

Erw*_*ter 2

查询 3 的 5 倍性能下降被证明是错误的。这是令人费解的一点,已经解决了。

其余的一切都是有道理的,尽管目前还没有按照你的方式进行。PRIMARY KEY罪魁祸首是:

PRIMARY KEY (scedtime, node_id)
Run Code Online (Sandbox Code Playgroud)

手册建议:

在分区表的键列上创建索引,以及您可能需要的任何其他索引。(关键索引并不是绝对必要的,但在大多数情况下它是有帮助的。

大胆强调我的。
你的就是这样一个场景。

您的“关键列”是node_id。您应该在 上有一个 B 树索引(node_id)。或者以node_id索引表达式为前导的多列索引。看:

解决方案

将您的 PK 替换为打开(scedtime, node_id)(node_id, scedtime)您也应该在以下情况下看到执行期间的分区修剪(“运行时分区修剪”)。因此,我们(never executed)在以下位置看到了修剪分区的子计划EXPLAIN ANALYZE

  • 使用连接表中的列
  • 用一个node_id in (multiple node_id subquery)

至少,这是我在 Postgres 13 和 14 的测试中看到的。比较这两个小提琴:

db<>在这里摆弄 ——PK不好

db<>fiddle这里 ——有很好的 PK

另请注意我如何配置SET random_page_cost = 1.1;以鼓励查询规划器实际使用索引。要点是保持索引扫描的成本估算较低,“好”计划使用node_idas索引扫描Index Cond,而不是 as Filter。您可以调整各种其他设置以支持索引。喜欢effective_cache_size甚至cpu_index_tuple_cost

如果你没有得到这样的计划,我会尝试:

SET enable_seqscan = off;

或者类似地强制制定一个有利的计划 - 仅用于调试 - 然后找出为什么 Postgres 不期望它是最便宜的。有关的:

目前(包括第 14 页),只有某些类型的查询计划可以使用分区修剪。以下是分区修剪背后的架构师之一 Amit Langote 在针对 Postgres 11 改进该功能时所写的内容: