由 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