在 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: index,key: 某个二级索引,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. 使用近似值
如果业务允许误差,可以用 EXPLAIN 的 rows 估算值,或者查询 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(字段) 的性能差异

