count(*)在innodb大表上慢是因为每次执行都需扫描聚簇索引,不缓存、不走索引统计;show table status的rows字段可提供毫秒级估算值,误差约±10%~40%。

为什么 COUNT(*) 在大表上慢得离谱
因为 MySQL 的 COUNT(*) 在 InnoDB 引擎下默认要扫一遍聚簇索引——哪怕你只想要个数字。它不走索引统计,也不缓存行数,每次执行都真实遍历(或至少按 MVCC 快照扫描)。10 亿行的表,哪怕只是估算,也可能卡住几秒到几十秒。
常见错误现象:SELECT COUNT(*) FROM huge_table 执行时间波动大、拖慢监控看板、触发超时告警;用 EXPLAIN 看到 rows 是准确值但实际执行仍慢——说明优化器没帮你绕过扫描。
- 不是索引缺失的问题,加索引对
COUNT(*)没用(除非改写成COUNT(索引列)且该列非 NULL) - MyISAM 表快是因为它直接读元数据,但 MyISAM 不支持事务,现在基本不用
-
information_schema.TABLES里的TABLE_ROWS是估算值,InnoDB 下经常不准,不能用于业务逻辑
用 SHOW TABLE STATUS 快速取近似值
这是最轻量、无需改表结构、不引入额外组件的方案。MySQL 内部维护了每个表的行数估算(基于采样),SHOW TABLE STATUS LIKE 'table_name' 返回的 Rows 字段就是它。
使用场景:后台监控、管理界面显示“约 XX 条记录”、定时任务做粗略判断是否需要归档。
- 执行快(毫秒级),不锁表,不触发大量 I/O
- 误差通常在 ±10%~40%,极端稀疏/密集更新后可能偏差更大
- 不同 MySQL 版本采样策略不同:8.0.23+ 默认更激进,5.7 和 8.0 早期版本偏保守
- 注意字段名是
Rows(大写 R),不是rows;且只在ENGINE = 'InnoDB'时为估算值
用触发器 + 统计表做预计算
如果你需要「相对准确」(误差
核心做法:建一张单行统计表,用 INSERT/UPDATE/DELETE 触发器实时维护目标表的行数。
- 必须确保所有写入口都走这个触发器(比如禁止直接
LOAD DATA INFILE或物理导入) - 触发器里用
AFTER INSERT增 1、AFTER DELETE减 1;AFTER UPDATE要小心——只有主键变更才影响行数,一般可忽略 - 统计表本身要加唯一约束(如
PRIMARY KEY (dummy) CHECK (dummy = 1)),避免多行误写 - 高并发写入下,触发器会成为瓶颈,尤其当目标表 QPS > 500/s 时,建议先压测
示例统计表定义:
CREATE TABLE huge_table_count ( dummy TINYINT PRIMARY KEY DEFAULT 1, cnt BIGINT NOT NULL DEFAULT 0, CHECK (dummy = 1) );
别踩这些坑
近似统计不是万能解药,几个关键边界容易被忽略:
-
COUNT(*)和COUNT(非空列)在有 NULL 值时结果不同,但触发器预计算通常只对行数负责,不区分语义 - 用
TABLE_ROWS或SHOW TABLE STATUS时,如果刚执行过ANALYZE TABLE,估算会刷新,但不会立刻生效——InnoDB 有内部延迟 - 触发器方案在主从复制中需确认 binlog 格式是
ROW,否则从库可能丢失触发逻辑 - 分区表的
COUNT(*)更慢,且SHOW TABLE STATUS返回的是总估算,无法按分区拆分
真正难的不是选哪个方案,而是想清楚:这个 count 值到底被谁用、多久刷一次、容忍多少误差、能不能接受写入变慢。这几个问题没答案之前,代码写得再漂亮也白搭。











