根本原因是innodb必须按当前事务的read view逐行判断可见性以保证mvcc一致性,无法缓存全局行数;优化器虽会选最小索引遍历,但仍需访问所有叶子节点,i/o开销大。

MySQL的count(*)在InnoDB中执行慢,根本原因不是“没索引”,而是它必须尊重MVCC可见性
你执行SELECT COUNT(*) FROM order_info时,InnoDB不会直接返回一个缓存数字——因为对当前事务来说,“哪些行可见”是动态的。比如事务A刚插入但未提交的行,事务B就看不到;事务C可能还读着快照里的旧数据。InnoDB没法存一个“全局准确行数”,只能老老实实按当前事务的Read View,一行行判断是否该计入总数。
它其实已经在选最快的路了,但“最快”不等于“快”
优化器会自动挑最小的索引树来遍历(比如只存主键值的二级索引),避免读整行数据。但这仍要访问所有索引叶子节点:
- 如果表只有主键索引(聚簇索引),就得扫完整个数据文件,I/O压力大
- 如果有非空的窄二级索引(如
INDEX idx_status (status)),且你查的是COUNT(*) WHERE status = 1,那它可能走这个索引+回表过滤,反而更慢 -
EXPLAIN里看到type: index或type: ALL,说明确实在扫索引或全表,不是没走索引,是不得不扫
count(*)和count(1)、count(id)性能差异极小,别指望靠改写语法提速
在 MySQL 5.7+ InnoDB 中:
-
COUNT(*)和COUNT(1)被优化器等价处理,都不取字段值,纯按行累加 -
COUNT(id)(id为主键)要取出主键值再判断非空,多一次拷贝,略慢一点 -
COUNT(status)(status可为NULL)必须把每行status值读出来、判空,开销明显更大
所以别迷信“用COUNT(1)代替COUNT(*)能提速”,实测基本没差别;真正拖慢的是带WHERE却没索引,或者字段允许NULL还误用了COUNT(字段)。
最常被忽略的两个实际瓶颈:AUTOCOMMIT=OFF 和分库分表跨节点
这两个问题不会报错,但会让COUNT(*)突然变卡,且难以定位:
- 显式开启事务后(
AUTOCOMMIT=OFF)执行COUNT(*),会一直持有当前Read View,阻塞purge线程,undo日志膨胀,后续查询也跟着慢 - 分库分表场景下,中间件(如ShardingSphere)默认把
COUNT(*)下发到每个物理分片,再合并结果——网络往返+各节点并发扫描,耗时是单机的N倍,且无法用普通索引优化
这类问题不会出现在EXPLAIN里,得看事务状态和分片路由日志。线上大表的count慢,十次有七次不是SQL本身的问题,而是环境或架构层面的隐性约束。











