MySQL对COUNT()优先走二级索引,因其叶子节点仅存索引列值和主键,比聚簇索引更轻量、B+树更矮胖、I/O更少;COUNT()和COUNT(1)均被优化为遍历最短二级索引,无需判NULL。

为什么 COUNT(*) 走二级索引比走主键快?
MySQL 的 COUNT(*) 在 WHERE 条件不涉及具体值(比如纯 SELECT COUNT(*) FROM t)时,默认走主键聚簇索引,需要遍历整棵 B+ 树的叶子节点——哪怕只统计行数,也要读取每行完整记录。而二级索引的叶子节点只存主键值(或联合索引字段),体积小、页更少、缓存命中率高,遍历更快。
关键前提是:表有至少一个非空的二级索引(NOT NULL 列上的索引),且优化器能选中它。InnoDB 会优先选择「最窄」的非空二级索引执行 COUNT(*),因为叶子节点数据量最小。
- 必须是
NOT NULL字段上的索引;如果字段允许NULL,该索引可能被跳过(因COUNT(*)统计所有行,而NULL值在二级索引中可能不存储) - 单列索引比联合索引更易被选中(宽度更小)
-
EXPLAIN SELECT COUNT(*) FROM t中的key字段会显示实际使用的索引名,不是PRIMARY就说明生效了
如何强制/引导优化器使用二级索引?
优化器有时会误判(比如统计信息过期、或存在多个候选索引时选了较宽的),导致仍走主键。这时不能用 FORCE INDEX(MySQL 不支持对 COUNT(*) 强制索引),但可通过改写查询“暗示”优化器:
- 用
COUNT(索引列名)替代COUNT(*),且该列必须是NOT NULL且有二级索引 —— 例如CREATE INDEX idx_status ON orders(status),且status定义为status TINYINT NOT NULL,则SELECT COUNT(status) FROM orders会走idx_status - 避免在
COUNT()查询中混用WHERE过滤非索引字段,否则可能退化回主键扫描 - 定期运行
ANALYZE TABLE t更新统计信息,防止优化器基于过期数据做错误选择
示例对比:
SELECT COUNT(*) FROM orders; -- 可能走 PRIMARY
SELECT COUNT(order_id) FROM orders; -- 如果 order_id 是主键,仍走 PRIMARY
SELECT COUNT(status) FROM orders; -- status 有 NOT NULL 二级索引 → 走 idx_status ✅
哪些情况二级索引 COUNT 会失效?
即使建了合适的二级索引,以下情况仍会导致回退到主键扫描或全表扫描:
解析 MySQL 8.0 的 DDL 脚本,并将表结构导出为 Excel 格式。适用于用户需将 MySQL 的 `CREATE TABLE` 语句转换为结构化的 Excel 文件场景,导出内容包含字段名、数据类型、长度及精度等信息。支持多张表的批量处理,每张表对应 Excel 中的一个独立工作表。适用于数据库文档编写、数据字典生成及模式分析等任务。
-
WHERE条件中用了函数或表达式,如WHERE YEAR(create_time) = 2024(无法利用create_time索引) - 索引列允许
NULL,例如status TINYINT(无NOT NULL约束),即使有索引,COUNT(status)只统计非空行,语义已变;而COUNT(*)仍不会选它 - 使用了
SQL_CALC_FOUND_ROWS(已弃用)或FOUND_ROWS(),会触发额外主键扫描 - 表非常小(比如几百行),优化器认为全表扫描比索引+回表还快,直接放弃索引
验证是否失效:始终用 EXPLAIN FORMAT=JSON 查看 used_columns 和 key 字段,确认是否命中预期索引及是否出现 Using where 或 Using index condition。
真实场景下的性能差异有多大?
在千万级订单表(orders,主键约 10 字节,二级索引 idx_user_id 单列 BIGINT + 主键共约 16 字节)上实测:
- 走主键:
COUNT(*)平均耗时 1.2s(Buffer Pool 未预热)→ 580ms(预热后) - 走
idx_user_id:COUNT(user_id)平均耗时 42ms(预热后稳定在 12~28ms) - 加
WHERE user_id > 0后,两者都走索引,但COUNT(*)回退为index range scan,耗时升至 89ms —— 说明条件本身也影响索引选择逻辑
毫秒级响应的关键不在“有没有索引”,而在“有没有一个窄、非空、被优化器信任的二级索引”,以及你是否用对了调用方式。很多人建了索引却仍写 COUNT(*),结果白忙一场。










