导读:本期聚焦于IT柏拉图创作的《为什么SQL中COUNT(1)不一定比COUNT(*)快?揭秘数据库优化器的底层逻辑》,敬请观看详情。COUNT(*)和COUNT(1)到底哪个更快,几乎是每个写SQL的人都争论过的问题。网上流传着COUNT(1)性能更好的说法,但在主流数据库中真相往往相反或根本没有差别。本文从查询执行流程入手,分析优化器如何处理这两种写法,结合MySQL的InnoDB引擎、PostgreSQL和SQL Server的具体实现,解释字段计数、常量计数与星号计数的语义差异,并通过EXPLAIN执行计划和实际压测数据说明性能差异的来源,最后给出不同场景下应该选择哪种写法的建议,帮助你写出既正确又高效的统计查询。

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

为什么SQL中COUNT(1)不一定比COUNT(*)快?揭秘数据库优化器的底层逻辑

三种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(列名);当统计成为瓶颈时,该优化的不是括号里的内容,而是索引结构和统计方案本身。把精力花在理解优化器的执行计划上,远比记住一条来路不明的性能口诀有价值。

COUNT(1)COUNT(*)SQL优化修改时间:2026-09-13 22:48:52

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260913/56296.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。