MyISAM的COUNT(*)是O(1)操作,因其在.frm文件中缓存精确行数并原子更新;InnoDB则需遍历索引树逐行判断MVCC可见性,本质为快照读,性能较差且无全局行数缓存。

MyISAM直接读磁盘元数据,COUNT(*)是O(1)操作
MyISAM在表的.frm文件(或独立元数据区)里存了一个精确的table_rows值,每次INSERT/DELETE都会原子更新它。执行COUNT(*)时,MySQL根本不访问数据页或索引树,只从磁盘读这个整数——所以无论表有10行还是1亿行,耗时几乎不变。
但这个“快”是有代价的:
- 不支持事务:长事务中
COUNT(*)看到的不是快照,而是实时变更后的行数,可能和SELECT结果不一致 - 并发写入下
table_rows可能丢失更新(虽然概率低,但严格一致性无法保证) - 一旦加
WHERE条件,比如COUNT(*) WHERE status = 1,MyISAM也得走索引或全表扫描,速度立刻掉下来
InnoDB必须逐行判断MVCC可见性,本质是一次快照读
InnoDB没有全局行数缓存,因为每个事务的ReadView不同,同一时刻COUNT(*)对事务A、B、C返回的值可能完全不同。它只能遍历索引树(通常是主键聚簇索引),对每一行调用row_search_mvcc判断是否对该事务可见,再累加——这等价于一次轻量级快照读。
优化器会选最小的索引树来扫,比如你建了KEY idx_status(status)且status列很短,它就可能跳过主键索引去扫这个二级索引,减少I/O和内存拷贝。但即使这样,CPU开销和行锁等待(尤其被长事务阻塞时)仍不可忽视。
常见误区:
-
COUNT(1)和COUNT(*)性能几乎一样,官方已优化,不用刻意替换 -
COUNT(id)比COUNT(*)略慢,因为要解析行取出id字段 -
COUNT(col)最慢,每行都要取值并判NULL,即使col定义为NOT NULL也会做隐性校验
SHOW TABLE STATUS里的table_rows不准,别当真用
这个值来自InnoDB的采样估算(默认约10个数据页),官方文档明确说误差可达40%–50%。刚大批量DELETE后,它可能还显示旧值;数据分布不均(比如某几页特别满、其他页空)时偏差更大。
它只适合运维监控趋势,比如发现table_rows突降30%,提示可能误删;绝不能用于分页总数、库存校验或业务逻辑判断。
想手动刷新?可以跑ANALYZE TABLE t,但它会短暂锁表,千万别在高峰期执行。
真正要快,就得绕开InnoDB的一致性约束
如果业务能接受最终一致性(比如后台报表、用户看到的“约2.3万条”),外部计数是唯一实用解法;若必须强一致,就得牺牲写性能换读性能。
写时维护方案最常用:
- 建一张单行
counter表:CREATE TABLE counter (id TINYINT PRIMARY KEY, cnt BIGINT DEFAULT 0) - 在业务代码里,每次
INSERT/DELETE后同步执行UPDATE counter SET cnt = cnt + 1 WHERE id = 1(或-1) - 这个
UPDATE极快,且与原表事务同属一个上下文,不会破坏一致性
注意点:高并发写入时,UPDATE counter可能成为热点,需考虑分段计数或应用层批量合并。



















