count(*)是最稳妥的选择,它不跳过null、不依赖索引字段、语义明确且mysql优化器处理成熟;innodb在有主键或非空唯一索引时通常走索引统计,实际耗时远低于全表扫描。

直接用 COUNT(*) 是最稳妥的选择
绝大多数情况下,COUNT(*) 就是你要的答案。它不跳过 NULL,不依赖索引字段,语义明确,MySQL 优化器对它的处理也足够成熟。别被“慢”吓住——只要表有主键或非空唯一索引,InnoDB 通常能走索引统计(尤其在 8.0+),实际耗时远低于全表扫描。
常见错误现象:COUNT(1) 或 COUNT(id) 被误认为更快;其实 InnoDB 下三者执行计划几乎一致,但 COUNT(id) 在 id 允许为 NULL 时结果可能出错。
- 始终优先写
COUNT(*),不是习惯,是语义和兼容性需要 - 避免
COUNT(column_name)除非你明确要排除该列的 NULL 行 - 如果表超千万且实时性要求不高,考虑把行数缓存在应用层或单独计数表里,而不是每次查
为什么 information_schema.TABLES 不可靠
查 information_schema.TABLES 看 TABLE_ROWS 字段,看起来快,但它是估算值——InnoDB 不维护精确行数,只在 ANALYZE TABLE 时更新,且受事务隔离级别、MVCC 快照影响。线上表刚删了 50 万行,这里可能还显示旧数字。
使用场景:仅适用于运维巡检、粗略容量评估,绝不能用于业务逻辑判断(比如“是否为空表”)。
-
TABLE_ROWS对 MyISAM 是精确的,但 MyISAM 已基本淘汰 - InnoDB 下该值偏差可能达 10%~50%,尤其在频繁增删的表上
- 即使刚执行过
ANALYZE TABLE,当前事务中看到的仍可能是旧快照
大表 COUNT(*) 卡住怎么办
卡,往往不是函数本身慢,而是锁等待或 MVCC 版本链太长。InnoDB 的 COUNT(*) 需要遍历一个索引(通常是聚簇索引),过程中要检查每行是否对当前事务可见——如果并发事务多、历史版本堆积严重,就会拖慢。
性能影响关键点:是否加锁?是否走索引?是否受隔离级别干扰?
- READ-COMMITTED 和 REPEATABLE-READ 下行为一致,但 RC 更早释放部分锁
- 显式加锁(如
SELECT COUNT(*) FROM t FOR UPDATE)会加剧阻塞,绝对禁止 - 确保表有主键——没主键时 MySQL 可能退化为全表扫描,且无法利用高效索引遍历
- 临时方案:用
EXPLAIN SELECT COUNT(*) FROM t看是否走了type: index,而不是ALL
不同存储引擎下的行为差异
MyISAM 和 InnoDB 处理 COUNT(*) 的底层机制完全不同,这直接影响你对“快”和“准”的预期。
MyISAM 把行数存在磁盘头里,COUNT(*) 是 O(1);InnoDB 是 O(n),但 n 是索引树节点数,不是数据行数,所以比想象中快得多。
- MyISAM:快且准,但不支持事务、崩溃后易损坏,已不推荐新项目使用
- InnoDB:慢但准,且一致性有保障;8.0 引入了更激进的索引统计优化,小表基本无感
- Memory 引擎:行数也存内存里,但重启即丢,不适合持久化场景
COUNT(*) 查一次几百毫秒,比引入缓存、双写、定时任务带来的复杂度低得多。真正麻烦的从来不是函数本身,而是没想清楚“为什么需要这个总数”。











