PostgreSQL 19 新特性:为什么 COUNT(*) 终于比 COUNT(1) 快了?

前言

很多开发者都纠结过 COUNT(*)COUNT(1) 到底哪个更快。PostgreSQL 19 给了一个明确的答案——​数据库会自动帮你选最快的那个​。

COUNT(*) vs COUNT(1):老生常谈的问题

在 SQL 里,COUNT 是最常用的聚合函数之一。关于这两种写法的争论已经持续了很多年:

  • COUNT(*)​:统计所有行数
  • COUNT(1)​:对每一行计算常量 1,然后统计非 NULL 的数量

理论上,如果表达式永不为 NULL(比如常量 1),这两种写法的结果​完全一样​。但在 PostgreSQL 内部,它们的执行路径却大不相同。

为什么以前 COUNT(1) 反而更慢?

在 PG 19 之前,PostgreSQL 对待 COUNT(1) 是”老实巴交”的:

  1. 表达式计算​:对每一行都要计算一次表达式值(虽然结果就是 1
  2. NULL 检查​:检查计算结果是否为 NULL(虽然 1 永远不是 NULL)
  3. Tuple Deform(解构元组)​:如果表达式涉及列值,数据库可能需要从磁盘行格式中提取出完整的列数据。对于列很多的宽表,这个开销会非常明显

COUNT(*) 走的是特化路径——​什么都不算,直接数行​,天然就少了这些开销。

PG 19 的优化:让规划器替你省事儿

PostgreSQL 19 引入了一个重要的性能补丁(Commit 42473b3),标题就是:

“Have the planner replace COUNT(ANY) with COUNT(*), when possible”

简单说:​查询规划器现在会自动把冗余的 COUNT(表达式) 重写为更高效的 COUNT(*)​。

触发条件

要触发这个优化,必须同时满足:

  1. COUNT() 里的表达式​保证不会为 NULL​(比如常量 1、NOT NULL 约束的列)
  2. 不包含 ORDER BYDISTINCT 子句
-- 优化前:COUNT(1) 有额外计算开销
SELECT COUNT(1) FROM orders;

-- 优化后:PG 19 自动重写为 COUNT(*),无需表达式计算
-- 执行计划内部等价于:
SELECT COUNT(*) FROM orders;

技术内幕:SupportRequestSimplifyAggref

这个补丁引入了一个新的支持请求类型 SupportRequestSimplifyAggref,允许聚合函数的支持函数(prosupport)检查自身的调用是否可以被简化。

对于 COUNT(),PG 新增了一个专门的支持函数,负责判断:

  • 输入表达式是否非空?
  • 有没有 ORDER BY / DISTINCT

如果条件满足,就在查询规划阶段直接把 COUNT(ANY) 替换为 COUNT(*),从源头上消除了不必要的表达式评估和 NULL 检查。

附带收益:更干净的执行计划

这个补丁还顺带改进了 expr_is_nonnullable() 函数,让它能正确识别常量表达式的非空性。

比如以前执行计划里可能出现的这种冗余过滤:

One-Time Filter: (100 IS NOT NULL)

现在规划器知道 100 肯定不是 NULL,直接把这个无用过滤器移除了,执行计划更清爽。

总结

写法PG 19 之前PG 19 及以后
COUNT(*)最快,直接数行依然最快
COUNT(1)慢,有表达式计算和 NULL 检查开销自动优化为 COUNT(*)
COUNT(not_null_col)慢,可能触发 tuple deform自动优化为 COUNT(*)

最终结论

  • PG 19 之前​:请直接写 COUNT(*),它是性能最优的写法。
  • PG 19 及以后​:写 COUNT(1) 也不会吃亏了,数据库会自动帮你优化。但出于习惯和对旧版本的兼容性,​COUNT(*) 依然是最佳实践​。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注