MySQL 8.0+ 中 COUNT(*) 和 COUNT(1) 性能几乎无差异,优化器均选择最小可用索引遍历,不读取行数据;COUNT(列名) 则仅统计非 NULL 值,行为与前两者本质不同。

MySQL 8.0+ 下 count(*) 和 count(1) 几乎没区别
在现代 MySQL(5.7.18 起,尤其是 8.0+)中,COUNT(*) 和 COUNT(1) 的执行计划完全一致:优化器会自动选择最小的可用二级索引(或主键索引)来遍历,不读取实际数据行。两者都统计所有行,包括含 NULL 值的行。
常见错误现象是以为 COUNT(1) “更轻量”,其实它只是语法糖;COUNT(*) 反而更规范、语义明确,且被各版本优化得更彻底。实测百万级表下,二者耗时差异通常在毫秒级,可忽略。
- 如果表有
NOT NULL的二级索引(如create_date),COUNT(*)会优先走它,key_len小 → 扫描成本低 - 若只有主键索引,两者都走
PRIMARY,type=index - 手动写
COUNT(1)并不会绕过优化器逻辑,也不会“跳过列解析”——这是过时认知
count(列名) 统计的是非 NULL 行数,不是总行数
COUNT(列名) 的行为和前两者有本质差异:它只统计该列值不为 NULL 的行。哪怕该列定义为 NOT NULL,只要查询时表达式可能产出 NULL(如 COUNT(IF(age > 18, 1, NULL))),就会被过滤掉。
使用场景很明确:你关心“某字段有效值有多少”,而不是“这张表一共多少行”。比如统计有手机号的用户数:COUNT(phone);但若误用它替代 COUNT(*),在 gender 列有 50 万 NULL 的表里,结果会少一半。
-
COUNT(id)(id 是主键)≈COUNT(*),因为主键不可能为NULL -
COUNT(name)若name允许NULL,且真有空值,结果一定 ≤COUNT(*) - 如果该列有二级索引,
COUNT(列名)可能走索引(因索引项不含NULL),但前提是优化器认为它比扫主键更优
性能关键不在函数写法,而在索引和表结构
真正影响 COUNT 速度的,从来不是括号里写 * 还是 1 或列名,而是:有没有可用的窄索引、是否需要回表、存储引擎是否支持索引覆盖计数。
InnoDB 的 COUNT 必须遍历索引树(不能直接读元数据,除非是 information_schema 的近似值)。所以:
- 有
NOT NULL的短字段二级索引(如TINYINT列)→COUNT(*)会优先选它,比扫BIGINT主键快 - 全表无二级索引 → 只能扫聚集索引(即主键 B+ 树),IO 和 CPU 开销最大
-
COUNT(列名)若该列允许NULL,且没有对应索引,则可能触发全表扫描(连索引都用不上) - 分区表或大宽表下,
COUNT(*)即使走索引,也可能因 B+ 树深度增加而变慢
别信“count(1) 比 count(*) 快”的老黄历
这个说法源于 MySQL 5.6 及更早版本:当时 COUNT(*) 确实会尝试检查所有列是否可为空,而 COUNT(1) 被当作常量跳过。但从 5.7.18 开始,优化器已统一处理,COUNT(*) 内部直接映射到最廉价的索引遍历路径。
现在还坚持用 COUNT(1),除了增加代码理解成本,毫无收益。更危险的是,有人把 COUNT(1) 当成“能用索引”的暗示,结果在无索引列上写 COUNT(1),误以为很快——其实它和 COUNT(*) 一样要扫全表。
真正该盯住的,是 EXPLAIN SELECT COUNT(*) FROM t 里的 key 和 rows 字段,而不是括号里填什么。

















