在数据库开发的面试和日常交流中,COUNT(1)和COUNT(*)的性能之争出现的频率极高。流传甚广的说法是COUNT(1)只扫一个常量列所以更快,COUNT(*)要解析所有字段所以更慢。这个说法听起来有道理,但实际上在MySQL、PostgreSQL、SQL Server等主流数据库中并不成立。要弄清楚真正的原因,得从SQL的执行流程和优化器的工作方式说起。

三种COUNT写法的语义差异
先明确一点,COUNT括号里的内容决定了它的统计口径。SQL标准中COUNT有几种典型形态:COUNT(*)统计整行,不管字段是否为NULL;COUNT(1)括号里是常量表达式,每一行都会被计算成1,本质上也是统计行数;COUNT(列名)则只统计该列不为NULL的行数。理解这个差异是讨论性能的前提。
很多人以为COUNT(*)需要读取所有字段,这是对SQL语义的误解。星号在这里并不代表所有列的值,而是代表一整行这个概念。优化器在生成执行计划时,根本不需要真的把每一行的所有列都取出来数一遍,它只需要知道这一行存在即可。所以COUNT(*)在语义上就是纯粹的行数统计,和具体列无关。
COUNT(1)同样如此。括号里的1是一个常量,数据库不会为每一行分配空间存储这个1,只是在逻辑上把每行映射为1然后计数。从统计行数这个目标看,COUNT(*)和COUNT(1)要做的事情一模一样,性能差异自然也就无从谈起,除非某个数据库的优化器实现得比较粗糙。
不同数据库优化器的实际处理方式
在MySQL的InnoDB引擎中,官方文档明确说明COUNT(*)是专门为统计行数优化的写法。优化器会直接选择最小的可用索引来完成扫描,因为二级索引通常比主键索引更小,扫描代价更低。也就是说,COUNT(*)反而能享受优化器的特殊照顾。下面通过执行计划验证一下:
-- 建表并插入测试数据 CREATE TABLE t_user ( id BIGINT PRIMARY KEY, name VARCHAR(50), age INT, KEY idx_age (age) ) ENGINE=InnoDB; INSERT INTO t_user VALUES (1,'张三',20),(2,'李四',NULL),(3,NULL,30); -- 对比三种写法的执行计划 EXPLAIN SELECT COUNT(*) FROM t_user; EXPLAIN SELECT COUNT(1) FROM t_user; EXPLAIN COUNT(age) FROM t_user;
执行上述语句会发现,前两条EXPLAIN的输出几乎完全一致,Extra列都会出现Using index,表示走了覆盖索引扫描,优化器选择了idx_age这个更小的二级索引。第三条COUNT(age)虽然也会走索引,但统计结果会跳过age为NULL的那一行,返回2而不是3,这就是语义差异带来的结果差异。
PostgreSQL的处理更加直接。它的优化器在解析阶段就会把COUNT(1)重写成COUNT(*),两者在执行计划层面是同一个东西。可以用EXPLAIN ANALYZE观察:
EXPLAIN ANALYZE SELECT COUNT(*) FROM orders; -- 输出类似: -- Aggregate (cost=... rows=1) (actual time=... rows=1) -- -> Seq Scan on orders (cost=... rows=...) EXPLAIN ANALYZE SELECT COUNT(1) FROM orders; -- 计划与上面完全相同,执行时间在同一量级波动
SQL Server的情况类似,它的查询优化器在早期编译阶段就会对常量表达式做规范化处理,COUNT(1)与COUNT(*)生成的执行计划哈希值相同。换句话说,你以为自己写得更快的COUNT(1),在编译完成后已经变成了COUNT(*)。这些事实说明,所谓COUNT(1)更快的说法在现代数据库中基本没有依据。
性能差异的传言从何而来
这个说法并非凭空出现。早期的Oracle版本中,确实存在一些 anecdotes 级别的性能观察,某些特定场景下COUNT(1)稍快,某些场景下COUNT(ROWID)更快,这些经验被写进博客、传入论坛,再被后来的文章不断引用,逐渐演变成COUNT(1)优于COUNT(*)的定论。另外一些测试者用MyISAM引擎做实验,MyISAM维护了表级行数元数据,任何形式的COUNT在无WHERE条件时都是O(1)返回,这种测试根本测不出写法差异。
还有一类测试误差来自缓存和统计信息。第一次执行COUNT(*)时可能触发了冷读IO,第二次执行COUNT(1)时数据已经在缓冲池里,于是得出前者更慢的结论。正确的测试方法应该包括预热、多次执行取中位数、交替执行两种写法,并观察EXPLAIN ANALYZE的真实耗时。MySQL官方文档在聚合函数章节特别提醒,COUNT(*)在InnoDB中是经过优化的,推荐使用它来统计行数。
什么时候COUNT才真正遇到性能问题
与其纠结写法,不如关注真正影响性能的因素。大表上执行COUNT且带有WHERE条件时,数据库必须扫描满足条件的所有行,这个代价与写COUNT(*)还是COUNT(1)毫无关系,而与过滤条件能否命中索引直接相关。一张几千万行的表,如果过滤列没有索引,无论怎么改写COUNT都会慢。
对超大表的精确计数需求,常见的优化手段包括:利用较小的二级索引做覆盖扫描;维护一张计数表,通过触发器或应用层在写入时同步更新;使用Redis缓存计数并定期校准;或者接受近似值,直接查询information_schema.TABLES的TABLE_ROWS字段(InnoDB下是估算值)。这些方案的取舍取决于业务对实时性和精确性的要求。
总结一下结论:在主流数据库中,COUNT(*)和COUNT(1)性能没有实质区别,MySQL官方甚至对COUNT(*)做了专门优化,写SQL时放心使用COUNT(*)统计行数即可。当需要统计非NULL值的行数时,用COUNT(列名);当统计成为瓶颈时,该优化的不是括号里的内容,而是索引结构和统计方案本身。把精力花在理解优化器的执行计划上,远比记住一条来路不明的性能口诀有价值。