由 John Doe 九月 18, 2025
PostgreSQL 社区一直持续致力于优化器的改进。现在,又改进了反连接的查询执行。
特性提交日志
为 ANTI JOIN(反连接)启用 Memoize 功能支持。
目前,我们暂不支持为 SEMI JOIN(半连接)和 ANTI JOIN(反连接)使用 Memoize 功能,原因是:嵌套循环式的 SEMI JOIN 和 ANTI JOIN 不会完整扫描内层关系,这导致 Memoize 无法将缓存条目标记为 “已完成”。
有人可能会提出,或许可以在获取到第一条内层元组后,就将缓存条目标记为 “已完成”,但这种做法并不安全:若第一条内层元组与当前外层元组不满足连接条件,而存在第二条与参数匹配的内层元组时,该元组会发现缓存条目已被标记为 “已完成”,进而导致查询结果错误。
不过,若内层关系可证明具有唯一性(即针对任意外层参数,内层最多返回一条匹配记录),则上述问题不会出现,因为不存在第二条匹配的元组。尽管如此,这一逻辑对 SEMI JOIN 并无帮助:对于内层可证明唯一的 SEMI JOIN,优化器会通过reduce_unique_semijoins
机制自动将其简化为普通内连接,因此无需为其单独启用 Memoize。
因此,本次提交中,我们针对 ANTI JOIN 场景,新增了 “内层关系是否可证明唯一” 的判断逻辑;当满足该条件时,便允许为其启用 Memoize 功能。
讨论:https://postgr.es/m/CAMbWs48FdLiMNrmJL-g6mDvoQVt0yNyJAqMkv4e2Pk-5GKCZLA@mail.gmail.com
示例
在 PostgreSQL 数据库的查询优化中,Memoize
是一项重要的性能优化技术。它通过缓存内层查询结果,避免外层查询重复执行相同的内层计算,尤其适用于嵌套循环连接中“外层数据重复度高、内层查询开销大”的场景。但长期以来,Memoize
仅支持普通内连接、外连接,无法用于 SEMI JOIN(半连接)和 ANTI JOIN(反连接),导致这类查询在面对重复数据时只能重复执行内层逻辑,性能受限。
通过本次提交 ,PostgreSQL 社区打破了这一限制:为满足“内层关系可证明唯一”条件的 ANTI JOIN 启用 Memoize 支持。这一优化让反连接查询在特定场景下性能大幅提升,例如查找在 A 表中存在但在 B 表中无匹配的记录时。
该特性无需额外配置,PostgreSQL 优化器会自动判断“内层是否唯一”并启用 Memoize,可以通过EXPLAIN ANALYZE
验证优化是否生效。下面,让我们来进行一下测试。
创建包含重复数据的反连接测试表,并确保内层关系唯一:
-- 创建反连接测试表(外层表)
CREATE TABLE tab_anti (a int, b boolean);
-- 插入 100 行数据,a字段重复度高(取值为1、2、0循环)
INSERT INTO tab_anti
SELECT i%3, false
FROM generate_series(1, 100) i;
-- 收集统计信息,确保优化器识别数据分布
ANALYZE tab_anti;
编写包含“内层唯一”条件的 ANTI JOIN 查询,通过EXPLAIN ANALYZE
查看执行计划中Memoize
是否生效:
-- 执行反连接查询,内层用 DISTINCT ON 确保唯一
EXPLAIN ANALYZE
SELECT COUNT(*)
FROM tab_anti t1
LEFT JOIN LATERAL (
-- 内层:DISTINCT ON (a) 确保每个a仅返回一条记录(唯一)
SELECT DISTINCT ON (a) a, b, t1.a AS x
FROM tab_anti t2
) t2 ON t1.a + 1 = t2.a -- 外层参数:t1.a+1(重复度高)
WHERE t2.a IS NULL; -- 反连接条件:无匹配内层记录
若计划中包含Memoize
节点,且Cache Key
对应外层参数(如t1.a + 1
),说明优化已生效。上面的查询执行计划输出如下:
Aggregate (actual rows=1 loops=1)
-> Nested Loop Anti Join (actual rows=33 loops=1)
-> Seq Scan on tab_anti t1 (actual rows=100 loops=1)
-> Memoize (actual rows=0.67 loops=100) -- Memoize 节点生效
Cache Key: (t1.a + 1), t1.a -- 缓存键:外层参数(t1.a+1)
Cache Mode: binary -- 缓存模式(二进制存储,高效)
Hits: 97 Misses: 3 Evictions: 0 Overflows: 0 Memory Usage: 8kB
-- 缓存统计:97次命中,仅3次未命中(重复度高,缓存效果显著)
-> Subquery Scan on t2 (actual rows=0.67 loops=3)
Filter: ((t1.a + 1) = t2.a)
Rows Removed by Filter: 2
-> Unique (actual rows=2.67 loops=3) -- 内层唯一保障(DISTINCT ON)
-> Sort (actual rows=67.33 loops=3)
Sort Key: t2_1.a
Sort Method: quicksort Memory: 25kB
-> Seq Scan on tab_anti t2_1 (actual rows=100 loops=3)
从计划中可提取关键信息:
- 缓存命中率:
Hits: 97
,说明 100 次外层循环中,97 次直接复用缓存,仅3次执行内层查询,内层计算量减少 97%; - 内层唯一保障:
Unique
节点(由DISTINCT ON
触发)确保内层结果唯一,满足 Memoize 启用条件; - 性能收益:若内层查询是更复杂的计算(如关联多表、聚合),97% 的缓存命中率可带来数十倍的性能提升。
非常不错的特性。感谢社区的所有相关人员。
参考
提交日志:https://git.postgresql.org/pg/commitdiff/0da29e4cb161f78a5ef534b3fb4467756a422e25