COUNT(*)在百万级表上变慢是因为数据库需全表扫描以确保MVCC下事务快照的精确计数,即使有索引也无法避免I/O与CPU随数据量线性增长。

为什么 COUNT(*) 在百万级表上会变慢?
因为大多数数据库(如 MySQL InnoDB、PostgreSQL)执行 COUNT(<em>)</em> 时默认走全表扫描——即使有主键或索引,InnoDB 仍需遍历聚簇索引的每一行来确认“是否可见”(受 MVCC 影响),而 PostgreSQL 的 COUNT() 也绕不开 Heap Tuple 扫描。这不是“没建索引”的问题,而是语义决定的:它必须精确统计当前事务快照下所有满足条件的行数。
- 全表扫描意味着 I/O + CPU 开销随数据量线性增长
- 表越大,Buffer Pool 压力越大,可能挤出其他热点数据
- 如果表有大字段(
TEXT、JSON),实际读取的数据页更多
用 EXPLAIN 看清真实执行路径
别猜,先查执行计划。在 MySQL 中运行:
EXPLAIN SELECT COUNT(*) FROM orders;
重点关注:
-
type字段是否为index(走了索引覆盖)还是ALL(全表扫) -
key是否显示用了某个索引(哪怕只是主键) -
rows是否接近表总行数(说明没跳过数据)
PostgreSQL 则看:
EXPLAIN (ANALYZE, BUFFERS) SELECT COUNT(*) FROM orders;
注意 Seq Scan 和实际 shared hit/reads 数量。如果 Buffers 显示几百 MB 的读,就说明真在扫磁盘。
替代方案:什么时候能用近似值?
如果业务允许误差(比如后台监控、管理后台的“约XX万订单”),优先考虑统计信息或采样:
- MySQL:查
information_schema.TABLES的TABLE_ROWS字段(仅 InnoDB 近似值,不实时,重启或 ANALYZE TABLE 后更新) - PostgreSQL:查
pg_class.reltuples(同样为估算,依赖ANALYZE频率) - 更准一点:用
TABLESAMPLE SYSTEM (1)采样 1% 行再放大(PostgreSQL 9.5+):
SELECT CEIL(COUNT(*) * 100) FROM orders TABLESAMPLE SYSTEM (1);
但注意:采样不保证均匀,小表或倾斜分布下误差可能翻倍。
真要精确值?加覆盖索引或改查询逻辑
精确统计无法绕过数据一致性校验,但可以减少扫描成本:
- 如果只统计非空主键(如
COUNT(id)),且id是NOT NULL主键,MySQL 8.0+ 和 PostgreSQL 都可能优化为索引遍历(比聚簇索引轻) - 创建最小覆盖索引加速
COUNT(*):CREATE INDEX idx_count_stub ON orders ((1)); -- PostgreSQL 表达式索引
或 MySQL 中用虚拟列+索引(需 5.7+):ALTER TABLE orders ADD COLUMN _cnt TINYINT AS (1) STORED; CREATE INDEX idx_cnt ON orders(_cnt);
然后查COUNT(_cnt)—— 只要该索引足够小,就能大幅减少 I/O
真正难处理的是带 WHERE 的 COUNT(比如 COUNT(*) WHERE status = 'paid')。这时候索引选择、统计信息准确性、是否需要物化视图或定时汇总表,就得按具体过滤条件权衡了。
精确统计和性能之间没有银弹。越追求实时精确,代价越高;越接受延迟或误差,手段越灵活。最容易被忽略的是:很多“必须立刻知道总数”的需求,其实只是前端交互惯性——先确认业务是否真的需要毫秒级精确值。


















