count(*)走二级索引更快,因二级索引更小、i/o更少;优化器在无where/group by/having且索引非空时自动选用最小二级索引;explain中key为null不代表未用索引,需结合rows、extra及handler_read_next判断。

为什么 COUNT(*) 走二级索引比走主键快?
因为 InnoDB 的主键索引(聚簇索引)叶子节点存的是整行数据,而二级索引叶子节点只存主键值 + 索引列。当表很大时,二级索引更小、更紧凑,能塞进更多页到 buffer pool,扫描起来 I/O 更少。
MySQL 优化器在满足“无 WHERE 条件 + 无 GROUP BY + 无 HAVING”的情况下,会自动选择最小的可用二级索引做全索引扫描来替代全表扫描——前提是该索引不包含 NULL 值(否则可能被跳过)。
- 必须是
COUNT(*),不是COUNT(列名);后者会过滤 NULL,无法用任意索引代替 - 表上至少有一个非空的二级索引(比如有
UNIQUE或NOT NULL的普通索引) - 如果只有主键索引,或所有二级索引都含可为 NULL 的列,优化器大概率 fallback 到主键扫描
EXPLAIN 看不出用了哪个索引?
执行 EXPLAIN SELECT COUNT(*) FROM t 时,key 字段常显示 NULL,不代表没走索引——这是 MySQL 的显示缺陷。它只在显式使用 USE INDEX 或 FORCE INDEX 时才填 key,自动选择的二级索引不会暴露。
真正判断是否受益,得看 rows 和 Extra:
- 如果
rows明显小于总行数(比如 10 万行的表显示rows=5000),说明走了某个二级索引 - 如果
Extra出现Using index,基本可确认用了覆盖索引(即二级索引) - 用
SHOW STATUS LIKE 'Handler_read%'对比前后值:Handler_read_next上升但Handler_read_rnd_next几乎为 0,说明是顺序索引扫描,而非随机回表
手动指定索引反而变慢?
加 USE INDEX 强制走某个二级索引,不一定更快。优化器自动选的,通常是“最窄 + 非空 + 存在时间早”的那个索引;你硬指定一个宽索引(比如联合索引前导列很多、或含 TEXT 类型),反而增加 I/O 和内存压力。
典型踩坑场景:
- 对
CREATE TABLE t (id INT PRIMARY KEY, a VARCHAR(200), b INT, INDEX idx_b (b))执行SELECT COUNT(*) FROM t USE INDEX (idx_b)—— 没问题,idx_b很轻量 - 但若改成
INDEX idx_ab (a, b),由于a是长字符串,索引体积大,USE INDEX (idx_ab)可能比扫主键还慢 - 更隐蔽的是:某些版本 MySQL 对含前缀索引(如
INDEX idx_a (a(10)))不视为有效计数索引,即使EXPLAIN显示Using index,实际仍走主键
什么时候 COUNT(*) 就是快不了?
不是所有情况都能靠二级索引加速。核心限制在于:InnoDB 必须逐页读取索引并计数,没有全局行数缓存。所以以下情况依然慢:
- 表刚经历大批量删除(尤其没 VACUUM/ANALYZE),索引统计信息不准,优化器误判,或页内实际空洞多,物理扫描量没减少
- 启用了
innodb_stats_persistent = OFF且很久没更新统计信息,SHOW INDEX中的Cardinality严重失真 - 查询并发高,大量
COUNT(*)同时争抢 buffer pool 中的索引页,导致频繁换入换出 - 表上有
TEXT/BLOB列且未单独建索引,所有二级索引都得回表取长度信息(部分旧版本行为),失去覆盖优势
真正要扛高并发计数,得靠应用层缓存或定期落表(比如每小时写一次 INSERT INTO table_count SELECT NOW(), 't', COUNT(*) FROM t),而不是指望单条 COUNT(*) 多快。










