MySQL 中 count(*)、count(1)、count(字段) 的性能差异

在 MySQL 面试中,COUNT(*)COUNT(1)COUNT(字段) 的性能差异是一个高频考点。很多开发者对此存在误解,比如认为 COUNT(1)COUNT(*) 快,或者 COUNT(*) 会全表扫描导致性能最差。事实究竟如何?本文将从底层原理、执行计划和实践建议三个维度,彻底讲清这个问题。

一、先明确语义:三者到底在数什么

在讨论性能之前,必须先区分它们的语义,因为语义不同,优化器的处理方式也不同。

  • COUNT(*):统计所有行数,包括 NULL 值所在的行。它不关心任何具体列。
  • COUNT(1):同样统计所有行数,1 是一个常量表达式,对每一行都返回非 NULL,因此等价于统计行数。
  • COUNT(字段):统计该字段非 NULL 的行数。如果字段允许为 NULL,那么 NULL 行不会被计入。

也就是说,COUNT(*)COUNT(1) 在结果上完全等价,而 COUNT(字段) 的结果可能更少。性能讨论的前提是:在结果一致的情况下比较,或者在理解语义差异的基础上比较

二、InnoDB 的 COUNT 为什么慢

要理解性能差异,必须先理解 InnoDB 的存储结构。

MyISAM 会把表的总行数保存在磁盘上,执行 COUNT(*) 时直接读取这个值,复杂度是 O(1)。但 InnoDB 不支持这样做,原因是 MVCC(多版本并发控制)

在 InnoDB 中,不同事务在同一时刻看到的行数可能不同。比如事务 A 开启后,事务 B 插入了一行并提交,事务 A 再执行 COUNT(*) 时,按照可重复读隔离级别,它不应该看到 B 插入的行。因此 InnoDB 无法维护一个全局的、对所有事务都正确的行数,只能逐行判断当前事务是否可见,这就是 COUNT(*) 在 InnoDB 中较慢的根本原因。

三、执行计划层面的差异

1. COUNT(*) 和 COUNT(1)

在 MySQL 8.0 及大多数 5.7 版本中,优化器对 COUNT(*)COUNT(1) 的处理是完全一致的。你可以用 EXPLAIN 验证:

EXPLAIN SELECT COUNT(*) FROM users;
EXPLAIN SELECT COUNT(1) FROM users;

两者的执行计划通常都显示 type: indexkey: 某个二级索引Extra: Using index。优化器会选择最小的二级索引来扫描,而不是聚簇索引(主键索引),因为二级索引的叶子节点只存储索引列和主键,页更小,扫描的 I/O 更少。

所以,“COUNT(1)COUNT(*) 快”是一个流传甚广的谣言。在 MySQL 官方文档中明确写道:InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.

2. COUNT(字段)

COUNT(字段) 的情况要分两种:

(1)字段有 NOT NULL 约束

如果字段被定义为 NOT NULL,优化器知道每一行该字段都不为 NULL,因此它可以像 COUNT(*) 一样处理,选择最小的二级索引扫描。此时性能与 COUNT(*) 基本一致。

(2)字段允许为 NULL

如果字段允许 NULL,优化器必须逐行读取该字段的值,判断是否为 NULL。这时它不能随意选择最小的索引,而必须扫描包含该字段的索引,或者回表读取该字段。如果该字段上没有索引,就只能走聚簇索引全表扫描,性能会明显下降。

-- 假设 name 允许 NULL 且无索引
EXPLAIN SELECT COUNT(name) FROM users;
-- 可能显示 type: ALL,全表扫描

四、性能排序与常见误区

综合来看,在结果语义一致的前提下:

COUNT(*) ≈ COUNT(1) ≈ COUNT(非空字段) > COUNT(可空字段)

几个常见误区需要澄清:

  • 误区一:COUNT(1) 比 COUNT(*) 快。 错误。MySQL 优化器对两者处理完全相同。
  • 误区二:COUNT(*) 会全表扫描。 不一定。优化器会优先选择最小的二级索引,只有当表上没有任何二级索引时,才会扫描聚簇索引。
  • 误区三:COUNT(字段) 一定比 COUNT(*) 快。 错误。如果字段可空且无索引,反而更慢。
  • 误区四:COUNT(*) 会读取所有列。 错误。它只统计行数,不读取任何列的数据。

五、MyISAM 与 InnoDB 的对比

顺带一提,如果表是 MyISAM 引擎,COUNT(*) 是 O(1) 的,直接从元数据读取。但 MyISAM 不支持事务,实际生产中很少使用。InnoDB 才是主流,所以面试中讨论的“COUNT 慢”主要指 InnoDB。

六、优化 COUNT 查询的实践建议

既然 InnoDB 的 COUNT(*) 无法避免扫描,那在实际业务中如何优化?

1. 利用二级索引

确保表上有二级索引,优化器会选择最小的那个来扫描。这是最基础的优化。

2. 使用近似值

如果业务允许误差,可以用 EXPLAINrows 估算值,或者查询 information_schema.tables 中的 TABLE_ROWS

SELECT TABLE_ROWS FROM information_schema.tables
WHERE TABLE_SCHEMA = 'db' AND TABLE_NAME = 'users';

注意这个值是估算的,InnoDB 下误差可能较大。

3. 维护计数表

对于需要精确计数的场景,可以单独建一张计数表,在插入和删除时用事务更新计数。这是很多高并发系统的做法。

4. 使用 Redis 等缓存

把计数放在 Redis 中,用 INCR/DECR 维护,读的时候直接取。但要注意缓存与数据库的一致性问题。

5. 避免在 WHERE 中使用 COUNT(可空字段)

如果只是统计行数,永远用 COUNT(*),不要用 COUNT(字段)

七、面试回答模板

如果面试官问:“COUNT(*)COUNT(1)COUNT(字段) 哪个快?”

可以这样回答:

在 InnoDB 中,COUNT(*)COUNT(1) 的性能完全一致,优化器对它们的处理方式相同,都会选择最小的二级索引扫描,不存在谁比谁快的问题。COUNT(字段) 则取决于字段是否允许 NULL:如果字段是 NOT NULL,性能与 COUNT(*) 接近;如果允许 NULL,优化器需要逐行判断 NULL,可能无法使用最优索引,性能会下降。所以结论是:统计行数时优先用 COUNT(*),语义清晰且性能不差;不要迷信 COUNT(1) 更快的说法。

八、总结

表达式 语义 性能
COUNT(*) 统计所有行 优化器选最小二级索引,性能好
COUNT(1) 统计所有行 COUNT(*) 完全相同
COUNT(非空字段) 统计非空行 COUNT(*) 接近
COUNT(可空字段) 统计非空行 可能全表扫描,性能较差

核心结论只有一句话:在 InnoDB 中,COUNT(*)COUNT(1) 没有性能差异,COUNT(字段) 的性能取决于字段的可空性和索引情况。 理解 MVCC 导致的无法缓存行数,才是理解这个问题的关键。

未经允许不得转载:任鹏个人博客 » MySQL 中 count(*)、count(1)、count(字段) 的性能差异

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏