PostgreSQL 17: 改进空值匹配条件的规划

John Doe 五月 22, 2025

你有遇到带有空值匹配条件的慢查询吗?现在,PostgreSQL 改进了空值匹配条件的规划。

在山坡奔跑的大象

特性提交日志

改进 IS [NOT] NULL 条件的规划。

到目前为止,PostgreSQL 在优化那些针对定义为 NOT NULL 的列的 “IS NOT NULL” 基础条件时,表现得不够智能。对这些不必要条件的计算会增加开销。通常情况下,如果有人对此提出抱怨,可能只会被告知:如果查询中不需要该条件,就不要在查询中包含它。然而,最近的一份错误报告表明,这种情况可能并非总是可行。

当我们优化最小值/最大值的聚合计算时,规划器为了使重写后的计划忽略 NULL 值而添加的 “IS NOT NULL” 条件,可能会导致索引选择不佳的问题。该特定案例表明,其他条件(尤其是那些没有可用统计信息、规划器无法估计出近似选择率的条件),可能会由于带有 LIMIT 1 的情况下规划器更倾向于选择启动成本较低的路径,而导致索引选择不佳。

在此,我们采用一种通用方法来解决这个问题:让规划器检查 NOT NULL 列,当它们并不需要时,在所有查询中(而不仅仅是在优化最小值/最大值的聚合计算时)移除这些多余的条件。

此外,我们还会检测针对 NOT NULL 列的 “IS NULL” 条件,并将其转换为一个门控条件,这样我们就完全无需执行扫描操作。当某个列在任何外连接中都不可为空时,这种优化也适用于关系连接的场景。

这也有助于自连接移除的工作,因为必须用 “IS NOT NULL” 条件替换严格的连接条件,以确保与原始查询等价。

讨论:https://postgr.es/m/17540-7aa1855ad5ec18b4%40postgresql.org

示例

在某个列定义了“非空”约束的情况下,PostgreSQL 会如何处理 “IS NULL” 和 “IS NOT NULL” 的情况?

过去,如果您确信由于“非空”约束的存在,查询中不可能存在任何空值,通常就不会在查询中添加 “IS NOT NULL” 条件。如果仍然添加,则会产生一些开销,因为必须计算 “IS NOT NULL” 条件。这种情况在 PostgreSQL 新版本中发生了改变,因为优化器对此类情况的处理变得更加智能。

在 PostgreSQL 16 中,创建一个带有非空约束的列的简单表:

pg16=> create table t ( a int not null );
pg16=> insert into t select * from generate_series(1,1000000);
pg16=> analyze t;

在 PostgreSQL 新版本也创建一个同样的表:

pgnew=> create table t ( a int not null );
pgnew=> insert into t select * from generate_series(1,1000000);
pgnew=> analyze t;

如果我们在 PostgreSQL 16 中通过查询所有不为空的行,来从该表进行 SELECT,我们将看到以下内容:

pg16=> explain select * from t where a is not null;
                        QUERY PLAN                         
-----------------------------------------------------------
 Seq Scan on t  (cost=0.00..14425.00 rows=1000000 width=4)
   Filter: (a IS NOT NULL)
(2 rows)

即使我们知道由于非空约束,筛选条件 (a IS NOT NULL) 确实适用于所有行,它仍会进行计算。在 PostgreSQL 新版本中执行相同操作如下所示:

pgnew=> explain select * from t where a is not null;
                        QUERY PLAN                         
-----------------------------------------------------------
 Seq Scan on t  (cost=0.00..14425.00 rows=1000000 width=4)
(1 row)

筛选条件消失了,这节省了计算它的开销。另一种情况是要求所有行都为空,这显然是不可能的,因为约束不允许出现那样的行。

PostgreSQL 16 将对整个表进行并行顺序扫描:

pg16=> explain select * from t where a is null;
                            QUERY PLAN                             
-------------------------------------------------------------------
 Gather  (cost=1000.00..9591.77 rows=1 width=4)
   Workers Planned: 2
   ->  Parallel Seq Scan on t  (cost=0.00..8591.67 rows=1 width=4)
         Filter: (a IS NULL)
(4 rows)

PostgreSQL 新版本将不再这样做:

pgnew=> explain select * from t where a is null;
                QUERY PLAN                
------------------------------------------
 Result  (cost=0.00..0.00 rows=0 width=0)
   One-Time Filter: false
(2 rows)

非常不错的特性,感谢所有参与的社区人员。

参考

提交日志:https://git.postgresql.org/pg/commitdiff/b262ad440edecda0b1aba81d967ab560a83acb8a