count(*)在千万级innodb表上慢的根本原因是其不缓存行数且默认全扫描聚簇索引;优化关键在于使用not null列的二级索引,使count(该列)仅遍历索引页即可快速统计。

COUNT(*) 在千万级表上变慢,根本原因不是数据多,而是 InnoDB 每次都得老老实实数一遍 —— 它不缓存精确行数,且默认走主键索引(聚簇索引)全扫描。 即便加了索引,如果没用对,照样慢。下面直说怎么用二级索引辅助加速。
为什么 COUNT(*) 不走二级索引?
InnoDB 的 COUNT(*) 优化逻辑是:优先选“最窄、非空、覆盖性好”的索引。但前提是该索引能保证“所有行都可被代表”。主键索引天然满足(每行必有主键),而普通二级索引只包含非 NULL 值的键 —— 所以 COUNT(*) 默认不信任它,除非你显式引导。
常见错误现象:EXPLAIN SELECT COUNT(*) FROM t WHERE status = 1 显示 type=ref,但实际执行仍慢;原因是虽然 status 有索引,但优化器发现该索引允许 NULL 或未建在 NOT NULL 列上,就放弃用它做行数统计,转而回退到主键扫描。
- 确保被索引的列定义为
NOT NULL(如status TINYINT NOT NULL) - 建联合索引时,把高频过滤字段放前面,例如
CREATE INDEX idx_status_created ON t (status, created_at) - 避免在
COUNT(*)中混用WHERE和OR、函数或隐式类型转换,否则索引失效,直接退化为全表扫描
COUNT(索引列) 比 COUNT(*) 快的本质
当某列有二级索引且 NOT NULL 时,COUNT(该列名) 会强制走该索引 —— 因为索引 B+ 树的叶子节点里,每个键对应一行(不含 NULL),只要遍历索引页就能得出总数,无需回表、无需读数据行。
对比示例:
SELECT COUNT(*) FROM user_factor_auth_record; -- 走主键,1350万行,耗时 ~3.8s<br>SELECT COUNT(id) FROM user_factor_auth_record; -- id 是主键,等价于 COUNT(*),无改善<br>SELECT COUNT(status) FROM user_factor_auth_record; -- 若 status NOT NULL + 有索引,仅扫描二级索引页,耗时可压至 ~0.2s
-
COUNT(status)快的前提:该列NOT NULL+ 有单列或前导索引 - 不要用
COUNT(任意允许 NULL 的列),否则 MySQL 仍需逐行判断是否为 NULL,失去索引优势 - 若业务允许,把统计字段设为
NOT NULL DEFAULT 0,再建索引,是最稳妥的加速方式
EXPLAIN 看不出问题?那是没盯紧 key_len 和 rows
EXPLAIN 显示走了索引,不代表 COUNT 就快 —— 关键要看 key_len 是否合理、rows 是否接近真实匹配数。如果 key_len 过小(比如只用了索引前缀)或 rows 高达百万,说明索引没被充分利用。
典型陷阱:
- 对
VARCHAR(255)字段建索引但没指定前缀长度,MySQL 可能只用前 768 字节,导致key_len偏小、索引选择率低 - 复合索引中,
WHERE条件没用上最左前缀,COUNT()就无法利用该索引做高效扫描 -
rows值远大于预期(比如WHERE status = 1实际只有 10 万行,但EXPLAIN显示rows=8000000),说明统计信息过期,需执行ANALYZE TABLE t
真正有效的二级索引辅助计数,不是“加了索引就行”,而是“让索引变成可信赖的行数代理”。这意味着列必须 NOT NULL、索引要窄、查询条件要严格匹配索引结构。一旦走错一步,InnoDB 就会默默切回主键全扫 —— 这个切换过程完全静默,连 EXPLAIN 都不报错,只能靠 key_len 和实际耗时交叉验证。











