searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享

Postgres 16 查询优化器中的新增功能

2024-12-02 09:40:20
2
0

PostgreSQL 16 对查询计划程序进行了大量改进,并使许多 SQL 查询的运行速度比以前版本的 PostgreSQL 更快。

如果查看 PG16 的Release Note,你将看到其中有一些优化器的增强。但是,由于每个 PostgreSQL 版本中的更新都很大,因此无法提供有关每项更新的足够详细信息。因此,在了解它是否与你相关之前,你可能需要更详细的信息来了解更改的内容。

在这篇博文中,假设你已经掌握了 EXPLAIN 的基础知识,您将深入了解 PostgreSQL 16 查询计划程序中所做的 10 项改进。对于 PG16 规划器(规划器在其他关系数据库中通常称为优化器)的每项改进,您还将获得 PG15 和 PG16 规划器输出之间的比较,以及更改内容的示例,以独立测试的形式,您可以自己尝试。

让我们深入了解一下 PG16 中 PostgreSQL 规划器的这 10 项改进:

1. 在更多场景允许增量排序,包括 DISTINCT

增量排序最初是在 PostgreSQL 13 中添加的。这些增量排序减少了获取排序结果所需的工作量。如何?通过利用某些给定结果集已按 1 个或多个前导列排序的知识,并且仅对其余列执行排序。

例如,如果 column a 上有一个 btree 索引,并且我们需要按 ab 排序的行,那么我们可以使用 btree 索引(它在 column a 上提供预排序结果),并且仅在 的值发生变化时对到目前为止看到的a行进行排序。使用 PostgreSQL 使用的快速排序算法,对多个较小的组进行排序比对一个大组进行排序更有效。

PostgreSQL 16 查询计划程序现在考虑对SELECT DISTINCT查询执行增量排序。在 PG16 之前,当为SELECT DISTINCT查询选择排序方法时,计划者只考虑执行完整排序(这比增量排序更昂贵)。

-- Setup
CREATE TABLE distinct_test (a INT, b INT);
INSERT INTO distinct_test
SELECT x,1 FROM generate_series(1,1000000)x;
CREATE INDEX on distinct_test(a);
VACUUM ANALYZE distinct_test;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT DISTINCT a,b FROM distinct_test;
PG15 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------
 HashAggregate (actual rows=1000000 loops=1)
   Group Key: a, b
   Batches: 81  Memory Usage: 11153kB  Disk Usage: 31288kB
   ->  Seq Scan on distinct_test (actual rows=1000000 loops=1)
 Planning Time: 0.065 ms
 Execution Time: 414.226 ms
(6 rows)

PG16 EXPLAIN 输出

QUERY PLAN
------------------------------------------------------------------
 Unique (actual rows=1000000 loops=1)
   ->  Incremental Sort (actual rows=1000000 loops=1)
         Sort Key: a, b
         Presorted Key: a
         Full-sort Groups: 31250  Sort Method: quicksort  Average Memory: 26kB  Peak Memory: 26kB
         ->  Index Scan using distinct_test_a_idx on distinct_test (actual rows=1000000 loops=1)
 Planning Time: 0.108 ms
 Execution Time: 263.167 ms
(8 rows)

在上面的 PostgreSQL 16 EXPLAIN 输出中,您可以看到 planner 选择在a列上使用distinct_test_a_idx索引,然后执行 an Incremental Sort 以对所有相等值a进行排序 by b 。这Presorted Key: a表示了这一点。因为上面的INSERT语句只为每个值 a of 添加了一个值 b ,所以每批按增量排序的 Tuples 只包含一行。
上面 PostgreSQL 16 EXPLAIN 的输出显示,Peak MemoryIncremental Sort它只有 26 KB,而 PostgreSQL 15 使用的哈希方法需要大量内存,以至于它需要将大约 30 MB 的数据溢出到磁盘。 查询在 PostgreSQL 16 上的执行速度提高了 63%。

2. 为具有 ORDER BY 或 DISTINCT 的聚合添加使用预排序数据的能力

在 PostgreSQL 15 及更早版本中,包含 or DISTINCT 子句的ORDER BY聚合函数将导致执行程序始终在Aggregate计划的节点内执行排序。因为总是执行排序,所以计划者永远不会尝试形成一个计划来提供预排序的 Importing 来按顺序聚合行。
PostgreSQL 16 查询计划程序现在尝试形成一个计划,该计划以正确的顺序将行馈送到计划的Aggregate节点。执行程序现在足够聪明,可以识别这一点,并在行已经按正确的顺序预先排序时放弃自己执行排序。

-- Setup
CREATE TABLE aggtest (a INT, b text);
INSERT INTO aggtest SELECT a,md5((b%100)::text) FROM generate_series(1,10) a, generate_series(1,100000)b;
CREATE INDEX ON aggtest(a,b);
VACUUM FREEZE ANALYZE aggtest;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, BUFFERS)
SELECT a,COUNT(DISTINCT b) FROM aggtest GROUP BY a;

PG15 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------
 GroupAggregate (actual rows=10 loops=1)
   Group Key: a
   Buffers: shared hit=892, temp read=4540 written=4560
   ->  Index Only Scan using aggtest_a_b_idx on aggtest (actual rows=1000000 loops=1)
         Heap Fetches: 0
         Buffers: shared hit=892
 Planning Time: 0.122 ms
 Execution Time: 302.693 ms
(8 rows)

PG16 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------
 GroupAggregate (actual rows=10 loops=1)
   Group Key: a
   Buffers: shared hit=892
   ->  Index Only Scan using aggtest_a_b_idx on aggtest (actual rows=1000000 loops=1)
         Heap Fetches: 0
         Buffers: shared hit=892
 Planning Time: 0.061 ms
 Execution Time: 115.534 ms
(8 rows)

除了 PostgreSQL 16 执行查询的速度是 PG15 的两倍多之外,上述EXPLAIN ANALYZE输出中这种变化的唯一迹象是 PostgreSQL 16 输出中不存在的temp read=4540 written=4560。在 PG15 中,这是由溢出到磁盘的隐式排序引起的。

3. 允许在 UNION ALL 上记忆

Memoize计划节点最初是在 PostgreSQL 14 中引入的。Memoize plan 节点充当 parameterized Nested Loop 和 Nested Loop 内侧之间的缓存层。当需要多次查找相同的值时,Memoize 可以很好地提高性能,因为当所需的行已经被查询并缓存时,它可以跳过执行其子节点。
PostgreSQL 16 查询计划程序现在将考虑在MemoizeUNION ALL查询出现在 parameterized Nested Loop 的内侧时使用。

-- Setup
CREATE TABLE t1 (a INT PRIMARY KEY);
CREATE TABLE t2 (a INT PRIMARY KEY);
CREATE TABLE lookup (a INT);

INSERT INTO t1 SELECT x FROM generate_Series(1,10000) x;
INSERT INTO t2 SELECT x FROM generate_Series(1,10000) x;
INSERT INTO lookup SELECT x%10+1 FROM generate_Series(1,1000000)x;

ANALYZE t1,t2,lookup;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT * FROM (SELECT * FROM t1 UNION ALL SELECT * FROM t2) t
INNER JOIN lookup l ON l.a = t.a;

PG15 EXPLAIN 输出
QUERY PLAN
-------------------------------------------------------------------------------
 Nested Loop (actual rows=2000000 loops=1)
   ->  Seq Scan on lookup l (actual rows=1000000 loops=1)
   ->  Append (actual rows=2 loops=1000000)
         ->  Index Only Scan using t1_pkey on t1 (actual rows=1 loops=1000000)
               Index Cond: (a = l.a)
               Heap Fetches: 1000000
         ->  Index Only Scan using t2_pkey on t2 (actual rows=1 loops=1000000)
               Index Cond: (a = l.a)
               Heap Fetches: 1000000
 Planning Time: 0.223 ms
 Execution Time: 1926.151 ms
(11 rows)

PG16 EXPLAIN 输出
QUERY
---------------------------------------------------------------------------------
 Nested Loop (actual rows=2000000 loops=1)
   ->  Seq Scan on lookup l (actual rows=1000000 loops=1)
   ->  Memoize (actual rows=2 loops=1000000)
         Cache Key: l.a
         Cache Mode: logical
         Hits: 999990  Misses: 10  Evictions: 0  Overflows: 0  Memory Usage: 2kB
         ->  Append (actual rows=2 loops=10)
               ->  Index Only Scan using t1_pkey on t1 (actual rows=1 loops=10)
                     Index Cond: (a = l.a)
                     Heap Fetches: 10
               ->  Index Only Scan using t2_pkey on t2 (actual rows=1 loops=10)
                     Index Cond: (a = l.a)
                     Heap Fetches: 10
 Planning Time: 0.229 ms
 Execution Time: 282.120 ms
(15 rows)

在上面的 PostgreSQL 16 EXPLAIN 输出中,您可以看到Memoize节点位于Append节点的顶部,这导致 的数量loopsAppend从 PG15 中的 100 万减少到 PG16 中的 10 个。每次Memoize节点有缓存命中时,都无需执行 Append to fetch records。这会导致查询在 PostgreSQL 16 上的运行速度提高约 6 倍。

4. 允许以不可为空的输入作为内部关系来执行反连接

执行 for Hash Join an INNER JOIN 时,PostgreSQL 更喜欢在两个表中较小的一个上构建哈希表。较小的哈希表更好,因为构建它们的工作量更少。较小的表也更好,因为它们对 CPU 的缓存更友好,并且 CPU 在等待数据从主内存到达时不太可能停止。
在 PostgreSQL 16 之前的版本中,如果您在查询中使用NOT EXISTS,可能会看到 an Anti Join 始终将 NOT EXISTS part 中提到的表放在联接的内侧。这意味着无法灵活地对两个表中较小的表进行哈希处理,从而导致可能必须在较大的表上构建哈希表。
PostgreSQL 16 查询计划程序现在可以选择对两个表中较小的一个进行哈希处理。现在可以这样做,因为 PostgreSQL 16 支持 Right Anti Join .

-- Setup
CREATE TABLE small(a int);
CREATE TABLE large(a int);
INSERT INTO small
SELECT a FROM generate_series(1,100) a;
INSERT INTO large
SELECT a FROM generate_series(1,1000000) a;
VACUUM ANALYZE small,large;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT * FROM small s
WHERE NOT EXISTS(SELECT 1 FROM large l WHERE s.a = l.a);

PG15 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------
 Hash Anti Join (actual rows=0 loops=1)
   Hash Cond: (s.a = l.a)
   ->  Seq Scan on small s (actual rows=100 loops=1)
   ->  Hash (actual rows=1000000 loops=1)
         Buckets: 262144  Batches: 8  Memory Usage: 6446kB
         ->  Seq Scan on large l (actual rows=1000000 loops=1)
 Planning Time: 0.103 ms
 Execution Time: 139.023 ms
(8 rows)

PG16 EXPLAIN 输出

QUERY PLAN
-----------------------------------------------------------
 Hash Right Anti Join (actual rows=0 loops=1)
   Hash Cond: (l.a = s.a)
   ->  Seq Scan on large l (actual rows=1000000 loops=1)
   ->  Hash (actual rows=100 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 12kB
         ->  Seq Scan on small s (actual rows=100 loops=1)
 Planning Time: 0.094 ms
 Execution Time: 77.076 ms
(8 rows)

从上面的EXPLAIN ANALYZE输出中可以看到,由于 PG16 的规划器选择使用 ,Hash Right Anti JoinPostgreSQL 16 Memory Usage 中的带宽比 PostgreSQL 15 中的少得多,Execution Time几乎减少了一半。

5. 允许并行化 FULL 和内部右 OUTER 哈希连接

PostgreSQL 11 引入了 Parallel Hash Join .这允许并行查询中的多个并行工作程序协助构建单个哈希表。在 11 之前的版本中,每个 worker 都会构建自己的相同哈希表,从而导致额外的内存开销。
在 PostgreSQL 16 中,Parallel Hash Join已得到改进,现在支持 FULL JOIN RIGHT 类型。这允许并行执行具有 的FULL OUTER JOIN查询,还允许Right Joins并行执行计划。

-- Setup
CREATE TABLE odd (a INT);
CREATE TABLE even (a INT);
INSERT INTO odd
SELECT a FROM generate_series(1,1000000,2) a;
INSERT INTO even
SELECT a FROM generate_series(2,1000000,2) a;
VACUUM ANALYZE odd, even;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT COUNT(o.a),COUNT(e.a) FROM odd o FULL JOIN even e ON o.a = e.a;

PG15 EXPLAIN 输出
QUERY PLAN
-------------------------------------------------------------------
 Aggregate (actual rows=1 loops=1)
   ->  Hash Full Join (actual rows=1000000 loops=1)
         Hash Cond: (o.a = e.a)
         ->  Seq Scan on odd o (actual rows=500000 loops=1)
         ->  Hash (actual rows=500000 loops=1)
               Buckets: 262144  Batches: 4  Memory Usage: 6439kB
               ->  Seq Scan on even e (actual rows=500000 loops=1)
 Planning Time: 0.079 ms
 Execution Time: 220.677 ms
(9 rows)

PG16 EXPLAIN 输出
QUERY PLAN
--------------------------------------------------------------------------------
 Finalize Aggregate (actual rows=1 loops=1)
   ->  Gather (actual rows=2 loops=1)
         Workers Planned: 1
         Workers Launched: 1
         ->  Partial Aggregate (actual rows=1 loops=2)
               ->  Parallel Hash Full Join (actual rows=500000 loops=2)
                     Hash Cond: (o.a = e.a)
                     ->  Parallel Seq Scan on odd o (actual rows=250000 loops=2)
                     ->  Parallel Hash (actual rows=250000 loops=2)
                           Buckets: 262144  Batches: 4  Memory Usage: 6976kB
                           ->  Parallel Seq Scan on even e (actual rows=250000 loops=2)
 Planning Time: 0.161 ms
 Execution Time: 129.769 ms
(13 rows)

输出EXPLAIN显示 PostgreSQL 16 能够并行执行连接,这导致查询的 Execution Time .

6. 当 RANGE 模式处于活动状态但不必要时,允许窗口函数使用更快的 ROWS 模式

当查询包含窗口函数(如 row_number() 、 、 rank()dense_rank()percent_rank()cume_dist()ntile() )时,如果 window 子句未指定ROWS选项,则 PostgreSQL 将始终使用 default RANGE 选项。该RANGE选项使 executor 向前看,直到找到第一个 “non-peer” 行。对等行是窗口框架中的一行,根据 window 子句的ORDER BY子句进行同等比较。如果没有ORDER BY子句,则窗口框架中的所有行都是对等的。当处理具有许多行的记录时,这些行根据 window 子句的ORDER BY子句进行相等排序,用于识别这些对等行的额外处理可能成本很高。
无论 ROWS 在 query 的 window 子句中指定 或 ,RANGE上面提到的 window 函数的行为都没有任何不同。但是,16 之前的 PostgreSQL 版本中的执行程序并不知道这一点,并且由于某些窗口函数确实关心 ROWS /RANGE 选项,因此执行程序在所有情况下都必须对对等行执行检查。
PostgreSQL 16 查询计划程序知道哪些窗口函数关心 /RANGE 选项,ROWS并将此信息传递给执行程序,以便它可以跳过不必要的额外处理。
当用于限制查询中的结果数量时row_number(),此优化效果特别好,如以下示例所示。

-- Setup
CREATE TABLE scores (id INT PRIMARY KEY, score INT);
INSERT INTO scores SELECT s,random()*10 FROM generate_series(1,1000000)s;
CREATE INDEX ON scores(score);
VACUUM ANALYZE scores;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT * FROM (
    SELECT id,ROW_NUMBER() OVER (ORDER BY score) rn,score
    FROM scores
) m WHERE rn <= 10;

PG15 EXPLAIN 输出
QUERY PLAN
-------------------------------------------------------------------------------
 WindowAgg (actual rows=10 loops=1)
   Run Condition: (row_number() OVER (?) <= 10)
   ->  Index Scan using scores_score_idx on scores (actual rows=50410 loops=1)
 Planning Time: 0.096 ms
 Execution Time: 29.775 ms
(5 rows)

PG16 EXPLAIN 输出
QUERY PLAN
----------------------------------------------------------------------------
 WindowAgg (actual rows=10 loops=1)
   Run Condition: (row_number() OVER (?) <= 10)
   ->  Index Scan using scores_score_idx on scores (actual rows=11 loops=1)
 Planning Time: 0.191 ms
 Execution Time: 0.058 ms
(5 rows)

上面 PG15 EXPLAIN 输出中的Index Scan节点显示,在执行停止之前,必须从scores_score_idx索引中读取 50410 行。而在 PostgreSQL 16 中,由于执行程序意识到一旦row_number达到 11 行,就没有更多符合<= 10条件的行了。这和使用 ROWS window 子句选项的执行程序都导致此查询在 PostgreSQL 16 上的运行速度提高了 500 倍以上。

7. 优化不断增加的窗口函数 ntile() 、 cume_dist() 和 percent_rank()

此更改扩展了 PostgreSQL 15 中完成的工作。在 PG15 中,查询计划程序被修改为允许执行程序提前停止处理WindowAgg执行程序节点。当WHERE子句中的项以某种方式筛选窗口函数时,可以执行此操作,一旦条件变为 false,它就永远不会再次为 true。

row_number()是一个函数示例,它可以提供这样的保证,因为它是一个单调递增的函数,即同一分区中的后续行永远不会有低于前一行的row_number。

PostgreSQL 16 查询计划程序扩展了此优化的覆盖范围,还涵盖了 ntile()cume_dist()percent_rank() 。在 PostgreSQL 15 中,这仅适用于 row_number() 、 、 rank()dense_rank()count()count(*)

-- Setup
CREATE TABLE marathon (id INT PRIMARY KEY, time INTERVAL NOT NULL);
INSERT INTO marathon
SELECT id,'03:00:00'::interval + (CAST(RANDOM() * 3600 AS INT) || 'secs')::INTERVAL - (CAST(RANDOM() * 3600 AS INT) || ' secs')::INTERVAL
FROM generate_series(1,50000) id;
CREATE INDEX ON marathon (time);
VACUUM ANALYZE marathon;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT * FROM (SELECT *,percent_rank() OVER (ORDER BY time) pr
FROM marathon) m WHERE pr <= 0.01;

PG15 EXPLAIN 输出
QUERY PLAN
-----------------------------------------------------------------------
 Subquery Scan on m (actual rows=500 loops=1)
   Filter: (m.pr <= '0.01'::double precision)
   Rows Removed by Filter: 49500
   ->  WindowAgg (actual rows=50000 loops=1)
         ->  Index Scan using marathon_time_idx on marathon (actual rows=50000 loops=1)
 Planning Time: 0.108 ms
 Execution Time: 84.358 ms
(7 rows)

PG16 EXPLAIN 输出
QUERY PLAN
-----------------------------------------------------------------------
 WindowAgg (actual rows=500 loops=1)
   Run Condition: (percent_rank() OVER (?) <= '0.01'::double precision)
   ->  Index Scan using marathon_time_idx on marathon (actual rows=50000 loops=1)
 Planning Time: 0.180 ms
 Execution Time: 19.454 ms
(5 rows)

从上面的 PostgreSQL 16 EXPLAIN 输出中,您可以看到 planner 能够将pr <= 0.01条件用作 ,Run Condition而在 PostgreSQL 15 中,此子句在子查询上显示为 aFilter。在 PG16 中,run 条件用于提前中止WindowAgg节点的执行。这导致 Execution Time PG16 比 PG15 快 4 倍多。

8. 允许在分区表上删除左连接和唯一连接

很长一段时间以来,PostgreSQL 一直能够删除查询中不需要左侧联接表中的 LEFT JOIN where 列,并且联接不可能复制任何行。
但是,在 PostgreSQL 16 之前的版本中,不支持对分区表进行左联接删除。为什么?因为 Planner 用于确定任何 inner-side row 是否有可能复制任何 outer-side 行的证明对于分区表不存在。
PostgreSQL 16 查询计划程序现在允许对分区表进行LEFT JOIN删除优化。
这种联接消除优化更有可能对视图有所帮助,因为通常并非始终查询视图中存在的所有列。

-- Setup
CREATE TABLE part_tab (id BIGINT PRIMARY KEY, payload TEXT) PARTITION BY HASH(id);
CREATE TABLE part_tab_p0 PARTITION OF part_tab FOR VALUES WITH (modulus 2, remainder 0);
CREATE TABLE part_tab_p1 PARTITION OF part_tab FOR VALUES WITH (modulus 2, remainder 1);
CREATE TABLE normal_table (id INT, part_tab_id BIGINT);

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT nt.* FROM normal_table nt LEFT JOIN part_tab pt ON nt.part_tab_id = pt.id;

PG15 EXPLAIN 输出
QUERY PLAN
-------------------------------------------------------------------
 Merge Right Join (actual rows=0 loops=1)
   Merge Cond: (pt.id = nt.part_tab_id)
   ->  Merge Append (actual rows=0 loops=1)
         Sort Key: pt.id
         ->  Index Only Scan using part_tab_p0_pkey on part_tab_p0 pt_1 (actual rows=0 loops=1)
               Heap Fetches: 0
         ->  Index Only Scan using part_tab_p1_pkey on part_tab_p1 pt_2 (actual rows=0 loops=1)
               Heap Fetches: 0
   ->  Sort (actual rows=0 loops=1)
         Sort Key: nt.part_tab_id
         Sort Method: quicksort  Memory: 25kB
         ->  Seq Scan on normal_table nt (actual rows=0 loops=1)
 Planning Time: 0.325 ms
 Execution Time: 0.037 ms
(14 rows)

PG16 EXPLAIN 输出
QUERY PLAN
-----------------------------------------------------
 Seq Scan on normal_table nt (actual rows=0 loops=1)
 Planning Time: 0.244 ms
 Execution Time: 0.015 ms
(3 rows)

这里需要注意的重要一点是,PostgreSQL 16 计划不包括 join to part_tab 意味着所要做的就是 scan normal_table .

9. 尽可能使用 Limit 而不是 Unique 来实现 DISTINCT

PostgreSQL 查询计划程序在检测到所有行都包含相同的值时,能够避免包含计划节点以删除重复的结果。检测这一点是微不足道的,当可以应用优化时,它可以带来巨大的性能提升。

-- Setup
CREATE TABLE abc (a int, b int, c int);
INSERT INTO abc SELECT a%10,a%10,a%10 FROM generate_series(1,1000000)a;
VACUUM ANALYZE abc;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT DISTINCT a,b,c FROM abc WHERE a = 5 AND b = 5 AND c = 5;

PG15 EXPLAIN 输出
QUERY PLAN
------------------------------------------------------------------------
 Unique (actual rows=1 loops=1)
   ->  Gather (actual rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Unique (actual rows=1 loops=3)
               ->  Parallel Seq Scan on abc (actual rows=33333 loops=3)
                     Filter: ((a = 5) AND (b = 5) AND (c = 5))
                     Rows Removed by Filter: 300000
 Planning Time: 0.114 ms
 Execution Time: 30.381 ms
(10 rows)

PG16 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------
 Limit (actual rows=1 loops=1)
   ->  Seq Scan on abc (actual rows=1 loops=1)
         Filter: ((a = 5) AND (b = 5) AND (c = 5))
         Rows Removed by Filter: 4
 Planning Time: 0.109 ms
 Execution Time: 0.025 ms
(6 rows)

如果您仔细查看 SQL 查询,您会注意到DISTINCT子句中的每一列在WHERE子句中也有一个相等条件。这意味着查询中的所有输出行在每列中都将具有相同的值。PostgreSQL 16 查询计划器能够利用这些知识,并将查询结果简单地LIMIT转换为 1 行。PostgreSQL 15 通过读取整个结果并使用Unique运算符将所有行减少到一行来生成相同的查询结果。Execution Time PostgreSQL 16 的速度比 PostgreSQL 15 快 1200 倍以上。

10. 放宽 select_ outer_ pathkeys_ for_ merge() 中过于严格的规则

在 PostgreSQL 16 之前,当查询计划程序考虑执行 a Merge Join 时,它会检查合并的排序顺序是否适合任何上层计划操作(例如 DISTINCTGROUP BYORDER BY ),并且仅在它与上层的要求完全匹配时使用该顺序。这个选择有点过时了,因为Incremental Sorts可以用于这些上层操作,并且增量排序可以利用仅由结果需要作为排序依据的一些前导列预排序的结果。
PostgreSQL 16 查询计划程序将考虑Merge Join顺序时使用的规则从“行的顺序必须完全匹配”调整为“必须至少有 1 个前导列正确排序”。这允许 Planner 使用 Incremental Sorts 将行按正确的顺序进行上级操作。我们从本博客的前面部分了解到,在可能的情况下,增量排序比完全排序需要更少的工作,因为增量排序能够利用部分排序的输入并以较小的批次执行排序,从而减少内存消耗和整体排序比较。

-- Setup

CREATE TABLE a (a INT, b INT);
CREATE TABLE b (x INT, y INT);
INSERT INTO a SELECT a,a FROM generate_series(1,1000000) a;
INSERT INTO b SELECT a,a FROM generate_series(1,1000000) a;
VACUUM ANALYZE a, b;

SET enable_hashjoin=0;
SET max_parallel_workers_per_gather=0;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT a,b,count(*) FROM a INNER JOIN b ON a.a = b.x GROUP BY a,b ORDER BY a DESC, b;

PG15 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------------------
 GroupAggregate (actual rows=1000000 loops=1)
   Group Key: a.a, a.b
   ->  Sort (actual rows=1000000 loops=1)
         Sort Key: a.a DESC, a.b
         Sort Method: external merge  Disk: 17664kB
         ->  Merge Join (actual rows=1000000 loops=1)
               Merge Cond: (a.a = b.x)
               ->  Sort (actual rows=1000000 loops=1)
                     Sort Key: a.a
                     Sort Method: external merge  Disk: 17664kB
                     ->  Seq Scan on a (actual rows=1000000 loops=1)
               ->  Materialize (actual rows=1000000 loops=1)
                     ->  Sort (actual rows=1000000 loops=1)
                           Sort Key: b.x
                           Sort Method: external merge  Disk: 11768kB
                           ->  Seq Scan on b (actual rows=1000000 loops=1)
 Planning Time: 0.175 ms
 Execution Time: 1010.738 ms
(18 rows)

PG16 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------------------
 GroupAggregate (actual rows=1000000 loops=1)
   Group Key: a.a, a.b
   ->  Incremental Sort (actual rows=1000000 loops=1)
         Sort Key: a.a DESC, a.b
         Presorted Key: a.a
         Full-sort Groups: 31250  Sort Method: quicksort  Average Memory: 26kB  Peak Memory: 26kB
         ->  Merge Join (actual rows=1000000 loops=1)
               Merge Cond: (a.a = b.x)
               ->  Sort (actual rows=1000000 loops=1)
                     Sort Key: a.a DESC
                     Sort Method: external merge  Disk: 17672kB
                     ->  Seq Scan on a (actual rows=1000000 loops=1)
               ->  Materialize (actual rows=1000000 loops=1)
                     ->  Sort (actual rows=1000000 loops=1)
                           Sort Key: b.x DESC
                           Sort Method: external merge  Disk: 11768kB
                           ->  Seq Scan on b (actual rows=1000000 loops=1)
 Planning Time: 0.140 ms
 Execution Time: 915.589 ms
(19 rows)

在上面的 PG16 EXPLAIN 输出中,您可以看到使用了 an Incremental Sort (与 PG15 相比,PG15 改用 a Sort ),这导致 PG16 Execution Time 中的查询略有减少,而用于执行排序的内存大幅减少。

Conclusion结论

来自世界各地的许多工程师在 PostgreSQL 16 中完成了大量工程工作来改进查询计划器。我要感谢所有通过审查我参与的作品提供帮助的人,以及所有对更改提供反馈的人。
上述 PostgreSQL 16 计划程序的 10 项改进中的每一项都是默认启用的,并且要么应用于可以优化的所有情况,要么由查询计划程序在认为优化会有所帮助时选择性地应用。
如果您运行的是旧版本的 PostgreSQL,我建议您在 PostgreSQL 16 上尝试工作负载,看看哪些查询更快。与往常一样,欢迎在 pgsql-general@postgresql.org 邮件列表中提供有关 PostgreSQL 实际使用情况的反馈 — 您不必只提交问题,也可以随时分享积极的体验。因此,请告诉我们您使用 PostgreSQL 16 规划器的体验。


归属:David Rowley 撰写的这篇关于 Postgres 16 查询计划程序改进的博客文章最初发布在 Citus Open Source Blog 上。

0条评论
0 / 1000
许****清
3文章数
0粉丝数
许****清
3 文章 | 0 粉丝

Postgres 16 查询优化器中的新增功能

2024-12-02 09:40:20
2
0

PostgreSQL 16 对查询计划程序进行了大量改进,并使许多 SQL 查询的运行速度比以前版本的 PostgreSQL 更快。

如果查看 PG16 的Release Note,你将看到其中有一些优化器的增强。但是,由于每个 PostgreSQL 版本中的更新都很大,因此无法提供有关每项更新的足够详细信息。因此,在了解它是否与你相关之前,你可能需要更详细的信息来了解更改的内容。

在这篇博文中,假设你已经掌握了 EXPLAIN 的基础知识,您将深入了解 PostgreSQL 16 查询计划程序中所做的 10 项改进。对于 PG16 规划器(规划器在其他关系数据库中通常称为优化器)的每项改进,您还将获得 PG15 和 PG16 规划器输出之间的比较,以及更改内容的示例,以独立测试的形式,您可以自己尝试。

让我们深入了解一下 PG16 中 PostgreSQL 规划器的这 10 项改进:

1. 在更多场景允许增量排序,包括 DISTINCT

增量排序最初是在 PostgreSQL 13 中添加的。这些增量排序减少了获取排序结果所需的工作量。如何?通过利用某些给定结果集已按 1 个或多个前导列排序的知识,并且仅对其余列执行排序。

例如,如果 column a 上有一个 btree 索引,并且我们需要按 ab 排序的行,那么我们可以使用 btree 索引(它在 column a 上提供预排序结果),并且仅在 的值发生变化时对到目前为止看到的a行进行排序。使用 PostgreSQL 使用的快速排序算法,对多个较小的组进行排序比对一个大组进行排序更有效。

PostgreSQL 16 查询计划程序现在考虑对SELECT DISTINCT查询执行增量排序。在 PG16 之前,当为SELECT DISTINCT查询选择排序方法时,计划者只考虑执行完整排序(这比增量排序更昂贵)。

-- Setup
CREATE TABLE distinct_test (a INT, b INT);
INSERT INTO distinct_test
SELECT x,1 FROM generate_series(1,1000000)x;
CREATE INDEX on distinct_test(a);
VACUUM ANALYZE distinct_test;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT DISTINCT a,b FROM distinct_test;
PG15 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------
 HashAggregate (actual rows=1000000 loops=1)
   Group Key: a, b
   Batches: 81  Memory Usage: 11153kB  Disk Usage: 31288kB
   ->  Seq Scan on distinct_test (actual rows=1000000 loops=1)
 Planning Time: 0.065 ms
 Execution Time: 414.226 ms
(6 rows)

PG16 EXPLAIN 输出

QUERY PLAN
------------------------------------------------------------------
 Unique (actual rows=1000000 loops=1)
   ->  Incremental Sort (actual rows=1000000 loops=1)
         Sort Key: a, b
         Presorted Key: a
         Full-sort Groups: 31250  Sort Method: quicksort  Average Memory: 26kB  Peak Memory: 26kB
         ->  Index Scan using distinct_test_a_idx on distinct_test (actual rows=1000000 loops=1)
 Planning Time: 0.108 ms
 Execution Time: 263.167 ms
(8 rows)

在上面的 PostgreSQL 16 EXPLAIN 输出中,您可以看到 planner 选择在a列上使用distinct_test_a_idx索引,然后执行 an Incremental Sort 以对所有相等值a进行排序 by b 。这Presorted Key: a表示了这一点。因为上面的INSERT语句只为每个值 a of 添加了一个值 b ,所以每批按增量排序的 Tuples 只包含一行。
上面 PostgreSQL 16 EXPLAIN 的输出显示,Peak MemoryIncremental Sort它只有 26 KB,而 PostgreSQL 15 使用的哈希方法需要大量内存,以至于它需要将大约 30 MB 的数据溢出到磁盘。 查询在 PostgreSQL 16 上的执行速度提高了 63%。

2. 为具有 ORDER BY 或 DISTINCT 的聚合添加使用预排序数据的能力

在 PostgreSQL 15 及更早版本中,包含 or DISTINCT 子句的ORDER BY聚合函数将导致执行程序始终在Aggregate计划的节点内执行排序。因为总是执行排序,所以计划者永远不会尝试形成一个计划来提供预排序的 Importing 来按顺序聚合行。
PostgreSQL 16 查询计划程序现在尝试形成一个计划,该计划以正确的顺序将行馈送到计划的Aggregate节点。执行程序现在足够聪明,可以识别这一点,并在行已经按正确的顺序预先排序时放弃自己执行排序。

-- Setup
CREATE TABLE aggtest (a INT, b text);
INSERT INTO aggtest SELECT a,md5((b%100)::text) FROM generate_series(1,10) a, generate_series(1,100000)b;
CREATE INDEX ON aggtest(a,b);
VACUUM FREEZE ANALYZE aggtest;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, BUFFERS)
SELECT a,COUNT(DISTINCT b) FROM aggtest GROUP BY a;

PG15 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------
 GroupAggregate (actual rows=10 loops=1)
   Group Key: a
   Buffers: shared hit=892, temp read=4540 written=4560
   ->  Index Only Scan using aggtest_a_b_idx on aggtest (actual rows=1000000 loops=1)
         Heap Fetches: 0
         Buffers: shared hit=892
 Planning Time: 0.122 ms
 Execution Time: 302.693 ms
(8 rows)

PG16 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------
 GroupAggregate (actual rows=10 loops=1)
   Group Key: a
   Buffers: shared hit=892
   ->  Index Only Scan using aggtest_a_b_idx on aggtest (actual rows=1000000 loops=1)
         Heap Fetches: 0
         Buffers: shared hit=892
 Planning Time: 0.061 ms
 Execution Time: 115.534 ms
(8 rows)

除了 PostgreSQL 16 执行查询的速度是 PG15 的两倍多之外,上述EXPLAIN ANALYZE输出中这种变化的唯一迹象是 PostgreSQL 16 输出中不存在的temp read=4540 written=4560。在 PG15 中,这是由溢出到磁盘的隐式排序引起的。

3. 允许在 UNION ALL 上记忆

Memoize计划节点最初是在 PostgreSQL 14 中引入的。Memoize plan 节点充当 parameterized Nested Loop 和 Nested Loop 内侧之间的缓存层。当需要多次查找相同的值时,Memoize 可以很好地提高性能,因为当所需的行已经被查询并缓存时,它可以跳过执行其子节点。
PostgreSQL 16 查询计划程序现在将考虑在MemoizeUNION ALL查询出现在 parameterized Nested Loop 的内侧时使用。

-- Setup
CREATE TABLE t1 (a INT PRIMARY KEY);
CREATE TABLE t2 (a INT PRIMARY KEY);
CREATE TABLE lookup (a INT);

INSERT INTO t1 SELECT x FROM generate_Series(1,10000) x;
INSERT INTO t2 SELECT x FROM generate_Series(1,10000) x;
INSERT INTO lookup SELECT x%10+1 FROM generate_Series(1,1000000)x;

ANALYZE t1,t2,lookup;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT * FROM (SELECT * FROM t1 UNION ALL SELECT * FROM t2) t
INNER JOIN lookup l ON l.a = t.a;

PG15 EXPLAIN 输出
QUERY PLAN
-------------------------------------------------------------------------------
 Nested Loop (actual rows=2000000 loops=1)
   ->  Seq Scan on lookup l (actual rows=1000000 loops=1)
   ->  Append (actual rows=2 loops=1000000)
         ->  Index Only Scan using t1_pkey on t1 (actual rows=1 loops=1000000)
               Index Cond: (a = l.a)
               Heap Fetches: 1000000
         ->  Index Only Scan using t2_pkey on t2 (actual rows=1 loops=1000000)
               Index Cond: (a = l.a)
               Heap Fetches: 1000000
 Planning Time: 0.223 ms
 Execution Time: 1926.151 ms
(11 rows)

PG16 EXPLAIN 输出
QUERY
---------------------------------------------------------------------------------
 Nested Loop (actual rows=2000000 loops=1)
   ->  Seq Scan on lookup l (actual rows=1000000 loops=1)
   ->  Memoize (actual rows=2 loops=1000000)
         Cache Key: l.a
         Cache Mode: logical
         Hits: 999990  Misses: 10  Evictions: 0  Overflows: 0  Memory Usage: 2kB
         ->  Append (actual rows=2 loops=10)
               ->  Index Only Scan using t1_pkey on t1 (actual rows=1 loops=10)
                     Index Cond: (a = l.a)
                     Heap Fetches: 10
               ->  Index Only Scan using t2_pkey on t2 (actual rows=1 loops=10)
                     Index Cond: (a = l.a)
                     Heap Fetches: 10
 Planning Time: 0.229 ms
 Execution Time: 282.120 ms
(15 rows)

在上面的 PostgreSQL 16 EXPLAIN 输出中,您可以看到Memoize节点位于Append节点的顶部,这导致 的数量loopsAppend从 PG15 中的 100 万减少到 PG16 中的 10 个。每次Memoize节点有缓存命中时,都无需执行 Append to fetch records。这会导致查询在 PostgreSQL 16 上的运行速度提高约 6 倍。

4. 允许以不可为空的输入作为内部关系来执行反连接

执行 for Hash Join an INNER JOIN 时,PostgreSQL 更喜欢在两个表中较小的一个上构建哈希表。较小的哈希表更好,因为构建它们的工作量更少。较小的表也更好,因为它们对 CPU 的缓存更友好,并且 CPU 在等待数据从主内存到达时不太可能停止。
在 PostgreSQL 16 之前的版本中,如果您在查询中使用NOT EXISTS,可能会看到 an Anti Join 始终将 NOT EXISTS part 中提到的表放在联接的内侧。这意味着无法灵活地对两个表中较小的表进行哈希处理,从而导致可能必须在较大的表上构建哈希表。
PostgreSQL 16 查询计划程序现在可以选择对两个表中较小的一个进行哈希处理。现在可以这样做,因为 PostgreSQL 16 支持 Right Anti Join .

-- Setup
CREATE TABLE small(a int);
CREATE TABLE large(a int);
INSERT INTO small
SELECT a FROM generate_series(1,100) a;
INSERT INTO large
SELECT a FROM generate_series(1,1000000) a;
VACUUM ANALYZE small,large;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT * FROM small s
WHERE NOT EXISTS(SELECT 1 FROM large l WHERE s.a = l.a);

PG15 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------
 Hash Anti Join (actual rows=0 loops=1)
   Hash Cond: (s.a = l.a)
   ->  Seq Scan on small s (actual rows=100 loops=1)
   ->  Hash (actual rows=1000000 loops=1)
         Buckets: 262144  Batches: 8  Memory Usage: 6446kB
         ->  Seq Scan on large l (actual rows=1000000 loops=1)
 Planning Time: 0.103 ms
 Execution Time: 139.023 ms
(8 rows)

PG16 EXPLAIN 输出

QUERY PLAN
-----------------------------------------------------------
 Hash Right Anti Join (actual rows=0 loops=1)
   Hash Cond: (l.a = s.a)
   ->  Seq Scan on large l (actual rows=1000000 loops=1)
   ->  Hash (actual rows=100 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 12kB
         ->  Seq Scan on small s (actual rows=100 loops=1)
 Planning Time: 0.094 ms
 Execution Time: 77.076 ms
(8 rows)

从上面的EXPLAIN ANALYZE输出中可以看到,由于 PG16 的规划器选择使用 ,Hash Right Anti JoinPostgreSQL 16 Memory Usage 中的带宽比 PostgreSQL 15 中的少得多,Execution Time几乎减少了一半。

5. 允许并行化 FULL 和内部右 OUTER 哈希连接

PostgreSQL 11 引入了 Parallel Hash Join .这允许并行查询中的多个并行工作程序协助构建单个哈希表。在 11 之前的版本中,每个 worker 都会构建自己的相同哈希表,从而导致额外的内存开销。
在 PostgreSQL 16 中,Parallel Hash Join已得到改进,现在支持 FULL JOIN RIGHT 类型。这允许并行执行具有 的FULL OUTER JOIN查询,还允许Right Joins并行执行计划。

-- Setup
CREATE TABLE odd (a INT);
CREATE TABLE even (a INT);
INSERT INTO odd
SELECT a FROM generate_series(1,1000000,2) a;
INSERT INTO even
SELECT a FROM generate_series(2,1000000,2) a;
VACUUM ANALYZE odd, even;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT COUNT(o.a),COUNT(e.a) FROM odd o FULL JOIN even e ON o.a = e.a;

PG15 EXPLAIN 输出
QUERY PLAN
-------------------------------------------------------------------
 Aggregate (actual rows=1 loops=1)
   ->  Hash Full Join (actual rows=1000000 loops=1)
         Hash Cond: (o.a = e.a)
         ->  Seq Scan on odd o (actual rows=500000 loops=1)
         ->  Hash (actual rows=500000 loops=1)
               Buckets: 262144  Batches: 4  Memory Usage: 6439kB
               ->  Seq Scan on even e (actual rows=500000 loops=1)
 Planning Time: 0.079 ms
 Execution Time: 220.677 ms
(9 rows)

PG16 EXPLAIN 输出
QUERY PLAN
--------------------------------------------------------------------------------
 Finalize Aggregate (actual rows=1 loops=1)
   ->  Gather (actual rows=2 loops=1)
         Workers Planned: 1
         Workers Launched: 1
         ->  Partial Aggregate (actual rows=1 loops=2)
               ->  Parallel Hash Full Join (actual rows=500000 loops=2)
                     Hash Cond: (o.a = e.a)
                     ->  Parallel Seq Scan on odd o (actual rows=250000 loops=2)
                     ->  Parallel Hash (actual rows=250000 loops=2)
                           Buckets: 262144  Batches: 4  Memory Usage: 6976kB
                           ->  Parallel Seq Scan on even e (actual rows=250000 loops=2)
 Planning Time: 0.161 ms
 Execution Time: 129.769 ms
(13 rows)

输出EXPLAIN显示 PostgreSQL 16 能够并行执行连接,这导致查询的 Execution Time .

6. 当 RANGE 模式处于活动状态但不必要时,允许窗口函数使用更快的 ROWS 模式

当查询包含窗口函数(如 row_number() 、 、 rank()dense_rank()percent_rank()cume_dist()ntile() )时,如果 window 子句未指定ROWS选项,则 PostgreSQL 将始终使用 default RANGE 选项。该RANGE选项使 executor 向前看,直到找到第一个 “non-peer” 行。对等行是窗口框架中的一行,根据 window 子句的ORDER BY子句进行同等比较。如果没有ORDER BY子句,则窗口框架中的所有行都是对等的。当处理具有许多行的记录时,这些行根据 window 子句的ORDER BY子句进行相等排序,用于识别这些对等行的额外处理可能成本很高。
无论 ROWS 在 query 的 window 子句中指定 或 ,RANGE上面提到的 window 函数的行为都没有任何不同。但是,16 之前的 PostgreSQL 版本中的执行程序并不知道这一点,并且由于某些窗口函数确实关心 ROWS /RANGE 选项,因此执行程序在所有情况下都必须对对等行执行检查。
PostgreSQL 16 查询计划程序知道哪些窗口函数关心 /RANGE 选项,ROWS并将此信息传递给执行程序,以便它可以跳过不必要的额外处理。
当用于限制查询中的结果数量时row_number(),此优化效果特别好,如以下示例所示。

-- Setup
CREATE TABLE scores (id INT PRIMARY KEY, score INT);
INSERT INTO scores SELECT s,random()*10 FROM generate_series(1,1000000)s;
CREATE INDEX ON scores(score);
VACUUM ANALYZE scores;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT * FROM (
    SELECT id,ROW_NUMBER() OVER (ORDER BY score) rn,score
    FROM scores
) m WHERE rn <= 10;

PG15 EXPLAIN 输出
QUERY PLAN
-------------------------------------------------------------------------------
 WindowAgg (actual rows=10 loops=1)
   Run Condition: (row_number() OVER (?) <= 10)
   ->  Index Scan using scores_score_idx on scores (actual rows=50410 loops=1)
 Planning Time: 0.096 ms
 Execution Time: 29.775 ms
(5 rows)

PG16 EXPLAIN 输出
QUERY PLAN
----------------------------------------------------------------------------
 WindowAgg (actual rows=10 loops=1)
   Run Condition: (row_number() OVER (?) <= 10)
   ->  Index Scan using scores_score_idx on scores (actual rows=11 loops=1)
 Planning Time: 0.191 ms
 Execution Time: 0.058 ms
(5 rows)

上面 PG15 EXPLAIN 输出中的Index Scan节点显示,在执行停止之前,必须从scores_score_idx索引中读取 50410 行。而在 PostgreSQL 16 中,由于执行程序意识到一旦row_number达到 11 行,就没有更多符合<= 10条件的行了。这和使用 ROWS window 子句选项的执行程序都导致此查询在 PostgreSQL 16 上的运行速度提高了 500 倍以上。

7. 优化不断增加的窗口函数 ntile() 、 cume_dist() 和 percent_rank()

此更改扩展了 PostgreSQL 15 中完成的工作。在 PG15 中,查询计划程序被修改为允许执行程序提前停止处理WindowAgg执行程序节点。当WHERE子句中的项以某种方式筛选窗口函数时,可以执行此操作,一旦条件变为 false,它就永远不会再次为 true。

row_number()是一个函数示例,它可以提供这样的保证,因为它是一个单调递增的函数,即同一分区中的后续行永远不会有低于前一行的row_number。

PostgreSQL 16 查询计划程序扩展了此优化的覆盖范围,还涵盖了 ntile()cume_dist()percent_rank() 。在 PostgreSQL 15 中,这仅适用于 row_number() 、 、 rank()dense_rank()count()count(*)

-- Setup
CREATE TABLE marathon (id INT PRIMARY KEY, time INTERVAL NOT NULL);
INSERT INTO marathon
SELECT id,'03:00:00'::interval + (CAST(RANDOM() * 3600 AS INT) || 'secs')::INTERVAL - (CAST(RANDOM() * 3600 AS INT) || ' secs')::INTERVAL
FROM generate_series(1,50000) id;
CREATE INDEX ON marathon (time);
VACUUM ANALYZE marathon;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT * FROM (SELECT *,percent_rank() OVER (ORDER BY time) pr
FROM marathon) m WHERE pr <= 0.01;

PG15 EXPLAIN 输出
QUERY PLAN
-----------------------------------------------------------------------
 Subquery Scan on m (actual rows=500 loops=1)
   Filter: (m.pr <= '0.01'::double precision)
   Rows Removed by Filter: 49500
   ->  WindowAgg (actual rows=50000 loops=1)
         ->  Index Scan using marathon_time_idx on marathon (actual rows=50000 loops=1)
 Planning Time: 0.108 ms
 Execution Time: 84.358 ms
(7 rows)

PG16 EXPLAIN 输出
QUERY PLAN
-----------------------------------------------------------------------
 WindowAgg (actual rows=500 loops=1)
   Run Condition: (percent_rank() OVER (?) <= '0.01'::double precision)
   ->  Index Scan using marathon_time_idx on marathon (actual rows=50000 loops=1)
 Planning Time: 0.180 ms
 Execution Time: 19.454 ms
(5 rows)

从上面的 PostgreSQL 16 EXPLAIN 输出中,您可以看到 planner 能够将pr <= 0.01条件用作 ,Run Condition而在 PostgreSQL 15 中,此子句在子查询上显示为 aFilter。在 PG16 中,run 条件用于提前中止WindowAgg节点的执行。这导致 Execution Time PG16 比 PG15 快 4 倍多。

8. 允许在分区表上删除左连接和唯一连接

很长一段时间以来,PostgreSQL 一直能够删除查询中不需要左侧联接表中的 LEFT JOIN where 列,并且联接不可能复制任何行。
但是,在 PostgreSQL 16 之前的版本中,不支持对分区表进行左联接删除。为什么?因为 Planner 用于确定任何 inner-side row 是否有可能复制任何 outer-side 行的证明对于分区表不存在。
PostgreSQL 16 查询计划程序现在允许对分区表进行LEFT JOIN删除优化。
这种联接消除优化更有可能对视图有所帮助,因为通常并非始终查询视图中存在的所有列。

-- Setup
CREATE TABLE part_tab (id BIGINT PRIMARY KEY, payload TEXT) PARTITION BY HASH(id);
CREATE TABLE part_tab_p0 PARTITION OF part_tab FOR VALUES WITH (modulus 2, remainder 0);
CREATE TABLE part_tab_p1 PARTITION OF part_tab FOR VALUES WITH (modulus 2, remainder 1);
CREATE TABLE normal_table (id INT, part_tab_id BIGINT);

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT nt.* FROM normal_table nt LEFT JOIN part_tab pt ON nt.part_tab_id = pt.id;

PG15 EXPLAIN 输出
QUERY PLAN
-------------------------------------------------------------------
 Merge Right Join (actual rows=0 loops=1)
   Merge Cond: (pt.id = nt.part_tab_id)
   ->  Merge Append (actual rows=0 loops=1)
         Sort Key: pt.id
         ->  Index Only Scan using part_tab_p0_pkey on part_tab_p0 pt_1 (actual rows=0 loops=1)
               Heap Fetches: 0
         ->  Index Only Scan using part_tab_p1_pkey on part_tab_p1 pt_2 (actual rows=0 loops=1)
               Heap Fetches: 0
   ->  Sort (actual rows=0 loops=1)
         Sort Key: nt.part_tab_id
         Sort Method: quicksort  Memory: 25kB
         ->  Seq Scan on normal_table nt (actual rows=0 loops=1)
 Planning Time: 0.325 ms
 Execution Time: 0.037 ms
(14 rows)

PG16 EXPLAIN 输出
QUERY PLAN
-----------------------------------------------------
 Seq Scan on normal_table nt (actual rows=0 loops=1)
 Planning Time: 0.244 ms
 Execution Time: 0.015 ms
(3 rows)

这里需要注意的重要一点是,PostgreSQL 16 计划不包括 join to part_tab 意味着所要做的就是 scan normal_table .

9. 尽可能使用 Limit 而不是 Unique 来实现 DISTINCT

PostgreSQL 查询计划程序在检测到所有行都包含相同的值时,能够避免包含计划节点以删除重复的结果。检测这一点是微不足道的,当可以应用优化时,它可以带来巨大的性能提升。

-- Setup
CREATE TABLE abc (a int, b int, c int);
INSERT INTO abc SELECT a%10,a%10,a%10 FROM generate_series(1,1000000)a;
VACUUM ANALYZE abc;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT DISTINCT a,b,c FROM abc WHERE a = 5 AND b = 5 AND c = 5;

PG15 EXPLAIN 输出
QUERY PLAN
------------------------------------------------------------------------
 Unique (actual rows=1 loops=1)
   ->  Gather (actual rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Unique (actual rows=1 loops=3)
               ->  Parallel Seq Scan on abc (actual rows=33333 loops=3)
                     Filter: ((a = 5) AND (b = 5) AND (c = 5))
                     Rows Removed by Filter: 300000
 Planning Time: 0.114 ms
 Execution Time: 30.381 ms
(10 rows)

PG16 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------
 Limit (actual rows=1 loops=1)
   ->  Seq Scan on abc (actual rows=1 loops=1)
         Filter: ((a = 5) AND (b = 5) AND (c = 5))
         Rows Removed by Filter: 4
 Planning Time: 0.109 ms
 Execution Time: 0.025 ms
(6 rows)

如果您仔细查看 SQL 查询,您会注意到DISTINCT子句中的每一列在WHERE子句中也有一个相等条件。这意味着查询中的所有输出行在每列中都将具有相同的值。PostgreSQL 16 查询计划器能够利用这些知识,并将查询结果简单地LIMIT转换为 1 行。PostgreSQL 15 通过读取整个结果并使用Unique运算符将所有行减少到一行来生成相同的查询结果。Execution Time PostgreSQL 16 的速度比 PostgreSQL 15 快 1200 倍以上。

10. 放宽 select_ outer_ pathkeys_ for_ merge() 中过于严格的规则

在 PostgreSQL 16 之前,当查询计划程序考虑执行 a Merge Join 时,它会检查合并的排序顺序是否适合任何上层计划操作(例如 DISTINCTGROUP BYORDER BY ),并且仅在它与上层的要求完全匹配时使用该顺序。这个选择有点过时了,因为Incremental Sorts可以用于这些上层操作,并且增量排序可以利用仅由结果需要作为排序依据的一些前导列预排序的结果。
PostgreSQL 16 查询计划程序将考虑Merge Join顺序时使用的规则从“行的顺序必须完全匹配”调整为“必须至少有 1 个前导列正确排序”。这允许 Planner 使用 Incremental Sorts 将行按正确的顺序进行上级操作。我们从本博客的前面部分了解到,在可能的情况下,增量排序比完全排序需要更少的工作,因为增量排序能够利用部分排序的输入并以较小的批次执行排序,从而减少内存消耗和整体排序比较。

-- Setup

CREATE TABLE a (a INT, b INT);
CREATE TABLE b (x INT, y INT);
INSERT INTO a SELECT a,a FROM generate_series(1,1000000) a;
INSERT INTO b SELECT a,a FROM generate_series(1,1000000) a;
VACUUM ANALYZE a, b;

SET enable_hashjoin=0;
SET max_parallel_workers_per_gather=0;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT a,b,count(*) FROM a INNER JOIN b ON a.a = b.x GROUP BY a,b ORDER BY a DESC, b;

PG15 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------------------
 GroupAggregate (actual rows=1000000 loops=1)
   Group Key: a.a, a.b
   ->  Sort (actual rows=1000000 loops=1)
         Sort Key: a.a DESC, a.b
         Sort Method: external merge  Disk: 17664kB
         ->  Merge Join (actual rows=1000000 loops=1)
               Merge Cond: (a.a = b.x)
               ->  Sort (actual rows=1000000 loops=1)
                     Sort Key: a.a
                     Sort Method: external merge  Disk: 17664kB
                     ->  Seq Scan on a (actual rows=1000000 loops=1)
               ->  Materialize (actual rows=1000000 loops=1)
                     ->  Sort (actual rows=1000000 loops=1)
                           Sort Key: b.x
                           Sort Method: external merge  Disk: 11768kB
                           ->  Seq Scan on b (actual rows=1000000 loops=1)
 Planning Time: 0.175 ms
 Execution Time: 1010.738 ms
(18 rows)

PG16 EXPLAIN 输出
QUERY PLAN
---------------------------------------------------------------------------
 GroupAggregate (actual rows=1000000 loops=1)
   Group Key: a.a, a.b
   ->  Incremental Sort (actual rows=1000000 loops=1)
         Sort Key: a.a DESC, a.b
         Presorted Key: a.a
         Full-sort Groups: 31250  Sort Method: quicksort  Average Memory: 26kB  Peak Memory: 26kB
         ->  Merge Join (actual rows=1000000 loops=1)
               Merge Cond: (a.a = b.x)
               ->  Sort (actual rows=1000000 loops=1)
                     Sort Key: a.a DESC
                     Sort Method: external merge  Disk: 17672kB
                     ->  Seq Scan on a (actual rows=1000000 loops=1)
               ->  Materialize (actual rows=1000000 loops=1)
                     ->  Sort (actual rows=1000000 loops=1)
                           Sort Key: b.x DESC
                           Sort Method: external merge  Disk: 11768kB
                           ->  Seq Scan on b (actual rows=1000000 loops=1)
 Planning Time: 0.140 ms
 Execution Time: 915.589 ms
(19 rows)

在上面的 PG16 EXPLAIN 输出中,您可以看到使用了 an Incremental Sort (与 PG15 相比,PG15 改用 a Sort ),这导致 PG16 Execution Time 中的查询略有减少,而用于执行排序的内存大幅减少。

Conclusion结论

来自世界各地的许多工程师在 PostgreSQL 16 中完成了大量工程工作来改进查询计划器。我要感谢所有通过审查我参与的作品提供帮助的人,以及所有对更改提供反馈的人。
上述 PostgreSQL 16 计划程序的 10 项改进中的每一项都是默认启用的,并且要么应用于可以优化的所有情况,要么由查询计划程序在认为优化会有所帮助时选择性地应用。
如果您运行的是旧版本的 PostgreSQL,我建议您在 PostgreSQL 16 上尝试工作负载,看看哪些查询更快。与往常一样,欢迎在 pgsql-general@postgresql.org 邮件列表中提供有关 PostgreSQL 实际使用情况的反馈 — 您不必只提交问题,也可以随时分享积极的体验。因此,请告诉我们您使用 PostgreSQL 16 规划器的体验。


归属:David Rowley 撰写的这篇关于 Postgres 16 查询计划程序改进的博客文章最初发布在 Citus Open Source Blog 上。

文章来自个人专栏
PostgreSQL内核代码分析
2 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0