PostgreSQL 19: 规划器尽可能替换 COUNT(ANY) 为 COUNT(*)

John Doe 十二月 2, 2025

你是否曾纠结应该使用COUNT(*)还是COUNT(1),或者一直循规蹈矩地在非空列上使用COUNT(id)

image

特性提交日志

规划器在可行时将COUNT(ANY)替换为COUNT(*)。

本次提交新增了SupportRequestSimplifyAggref机制,允许pg_proc.prosupport函数接收聚合函数引用(Aggref),并判断该聚合函数调用是否存在优化空间。

同时,本次提交还新增了一个支持函数,用于将COUNT(ANY)转换为COUNT(*)。当指定的 “ANY”(任意表达式或列)不可能为 NULL,且聚合函数引用(Aggref)内部不存在ORDER BYDISTINCT子句时,这种转换即可实现。该转换具有实际价值:现实中,人们常使用COUNT(1),而在此之前,这种写法会带来不必要的开销;此外,对非空(NOT NULL)列进行计数时,开销可能更高,因为这可能需要对元组进行更多转换操作(提取数据),而对于包含大量列的大型真实表,这种开销会更为显著。

未来或可针对其他聚合函数添加prosupport函数。例如,我们可考虑在某些聚合函数调用中移除ORDER BY子句:以MAX(c ORDER BY c)为例,其中的ORDER BY子句实际上并无意义。

讨论:https://postgr.es/m/CAApHDvqGcPTagXpKfH=CrmHBqALpziThJEDs_MrPqjKVeDF9wA@mail.gmail.com

优化目的

本次提交,为一种极为常见的 SQL 模式带来了实用性优化:对于SELECT COUNT(h)(其中hNOT NULL列)这类查询,性能提升最高可达 64%。

在 PostgreSQL 的历史版本中,COUNT(*)的执行速度一直快于COUNT(1)COUNT(列名),三者的差异如下:

  • COUNT(*):查询规划器明确其只需统计行数,无需检查任何特定列是否为NULL
  • COUNT(列名):执行器必须逐行获取该列的值,并在统计前检查其是否为NULL。此过程涉及元组转换(即提取数据),会增加额外开销。
  • COUNT(1):用户常将其视为更快的替代方案,但 PostgreSQL 会将其等同于COUNT(列名)处理,这里的“列名”本质是常量1

本次提交彻底改变了这一行为。

如今,规划器能够检测出当你在统计一个通用表达式COUNT(ANY)时,若满足以下两个条件:

  1. 表达式不可能为NULL:例如常量1,或定义为NOT NULL的列。
  2. 无额外子句:聚合函数内部不存在DISTINCTORDER BY

在这种情况下,规划器会自动将查询转换为使用COUNT(*)执行。

性能测试

该优化消除了上述场景下元组转换和“NULL 值检查”的开销。

为验证优化效果,针对提交前后的 PostgreSQL 版本进行基准测试:针对一张包含 1000 万行数据的表,对比COUNT(h)h为非空列)、COUNT(1)(常量)与COUNT(*)(行数统计)的执行效率。

本次提交前

  • SELECT count(h) FROM t:约 195 毫秒
  • SELECT count(1) FROM t:约 126 毫秒
  • SELECT count(*) FROM t:约 119 毫秒

观察结果COUNT(h)速度明显更慢(相较于count(*),额外开销约 64%);COUNT(1)虽比COUNT(h)快,但仍慢于COUNT(*)

本次提交后

  • SELECT count(h) FROM t:约 117 毫秒
  • SELECT count(1) FROM t:约 116 毫秒
  • SELECT count(*) FROM t:约 114 毫秒

观察结果COUNT(h)获得了显著提升,性能大幅接近COUNT(*)COUNT(1)也有小幅提速,最终与COUNT(*)的最优性能趋同。

这意味着,出于风格偏好而选择COUNT(1)的用户,或是习惯统计特定非空列的用户,如今无需手动修改代码,就能自动获得与COUNT(*)一致的最优性能。

非常不错的新特性,感谢社区的所有相关人员。

参考

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