COUNT(*)在千万级表上慢是因为InnoDB必须逐行扫描聚簇索引以保证MVCC事务可见性,无法缓存精确行数;优化可用覆盖索引、计数表或Redis缓存,非强一致场景可借information_schema.TABLE_ROWS估算。

COUNT(*) 在千万级表上直接查,基本就是全表扫描,不加干预的话 30 秒起步,不是慢,是卡死。
为什么 COUNT(*) 会变慢
MySQL 的 COUNT(*) 不是读个元数据就返回,而是真去数行——哪怕你只想要个总数。InnoDB 没有“精确行数缓存”,它得遍历聚集索引(主键 B+ 树)的叶子节点,每页扫一遍。1000 万行 ≈ 几百个数据页,IO 和 CPU 都扛不住。
常见错误现象:
-
SELECT COUNT(*) FROM user_login_log;执行超过 10 秒,且EXPLAIN显示type: ALL、rows接近总行数 - 并发执行多个
COUNT(*),导致大量磁盘争用,其他查询也被拖慢 - 在从库上执行,复制延迟加剧
用覆盖索引把 COUNT(*) 变成索引扫描
只要能把扫描对象从“聚簇索引”换成“更小的二级索引”,就能显著减少 IO。原理:二级索引的叶子节点只存索引列 + 主键,体积小、页少、加载快。
实操建议:
- 建一个最轻量的单列索引,比如
CREATE INDEX idx_dummy ON user_login_log (id);——id是主键,这个索引其实和主键索引结构重叠,但优化器可能更倾向用它做COUNT(*) - 更推荐建非空字段的索引,例如
CREATE INDEX idx_status ON user_login_log (status);(前提是status列NOT NULL),这样COUNT(*)就能走这个索引,避免回表 - 验证是否生效:
EXPLAIN SELECT COUNT(*) FROM user_login_log;看key是否显示你刚建的索引名,rows是否明显变小
替代方案:用近似值或业务逻辑绕过精确 COUNT
99% 的场景根本不需要实时精确总数。硬扛 COUNT(*) 是典型“为正确而牺牲可用”。
可选路径:
- 查
information_schema.TABLES表:SELECT table_rows FROM information_schema.TABLES WHERE table_schema = 'your_db' AND table_name = 'user_login_log';—— 返回的是 InnoDB 的估算值,误差通常在 ±10%,但毫秒级返回 - 写入时维护计数器:在业务层或用触发器,对增删操作同步更新一张
table_counts表,SELECT count_val FROM table_counts WHERE table_name = 'user_login_log'; - 带条件的 COUNT,优先走复合索引:比如
SELECT COUNT(*) FROM user_login_log WHERE status = 1 AND created_at > '2025-01-01';,必须给(status, created_at)建联合索引,否则照样扫全表
别碰 COUNT(1) 或 COUNT(主键),它们和 COUNT(*) 没区别
很多人以为 COUNT(1) 比 COUNT(*) 快,这是误区。MySQL 5.7+ 对这三者做了等价优化:COUNT(*)、COUNT(1)、COUNT(主键) 全部走同样的执行路径,优化器不会因为写法不同就跳过扫描。
真正起作用的是索引存在与否、是否 NOT NULL、有没有 WHERE 条件。写 COUNT(1) 只会让代码显得更迷惑,没任何收益。
最容易被忽略的一点:即使加了索引,如果 WHERE 条件里用了函数(比如 DATE(created_at)),索引照样失效,COUNT 还是回到全表扫描。所有优化的前提,是先让条件本身能走索引。


















