前言
很多开发者都纠结过 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) - NULL 检查:检查计算结果是否为 NULL(虽然
1永远不是 NULL) - Tuple Deform(解构元组):如果表达式涉及列值,数据库可能需要从磁盘行格式中提取出完整的列数据。对于列很多的宽表,这个开销会非常明显
而 COUNT(*) 走的是特化路径——什么都不算,直接数行,天然就少了这些开销。
PG 19 的优化:让规划器替你省事儿
PostgreSQL 19 引入了一个重要的性能补丁(Commit 42473b3),标题就是:
“Have the planner replace COUNT(ANY) with COUNT(*), when possible”
简单说:查询规划器现在会自动把冗余的 COUNT(表达式) 重写为更高效的 COUNT(*)。
触发条件
要触发这个优化,必须同时满足:
COUNT()里的表达式保证不会为 NULL(比如常量1、NOT NULL 约束的列)- 不包含
ORDER BY或DISTINCT子句
-- 优化前: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(*)依然是最佳实践。











