count()和count(1)在innodb中性能几乎一致,均为当前事务可见行的精确计数;count(列)跳过null值,语义不同不可混用;实时count()本质是o(n)操作,无法避免全索引扫描。

直接说结论:COUNT(*) 和 COUNT(1) 在 InnoDB 下性能几乎一致,且是当前事务可见行的精确值;COUNT(列) 语义不同(跳过该列为 NULL 的行),不能混用;真要“快”,就别依赖实时 COUNT(*) —— 它天生就是 O(n) 操作。
为什么 COUNT(*) 在 InnoDB 里必须扫索引
InnoDB 不像 MyISAM 那样把总行数存在磁盘元数据里,因为 MVCC 要求每个事务看到的“可见行数”可能不同。哪怕你只查 COUNT(*),InnoDB 也得一行行读出来、判断该行对当前事务是否可见,再累加。这不是慢在“函数本身”,而是慢在“必须做可见性判断”。
- 有主键时,优化器通常选主键索引(
type=index),比全表扫描快,但仍是遍历整棵 B+ 树 - 没主键、只有普通索引时,优化器倾向选“最窄的非空索引”;但如果索引列允许
NULL,某些 MySQL 版本可能退化为聚簇索引扫描 -
SHOW TABLE STATUS LIKE 't'返回的Rows是采样估算值,误差常达 40%–50%,不能用于业务逻辑
COUNT(*)、COUNT(1)、COUNT(列) 到底差在哪
三者执行计划可能相同,但语义和实际行为有明确分界:
-
COUNT(*):统计满足 WHERE 条件的所有行,不关心字段是否为空,MySQL 优化器会自动选成本最低的索引 -
COUNT(1):等价于COUNT(*),官方文档明确说二者无性能差异;它不取任何字段值,Server 层直接塞一个常量 1 判断非空 -
COUNT(age):只统计age IS NOT NULL的行;如果age允许为NULL且实际 NULL 很多,结果会显著小于COUNT(*)
注意:COUNT(主键) 看似安全,但它仍需从引擎层取出主键值再传给 Server 层,比 COUNT(*) 多一次字段解析和拷贝开销,实测略慢一点。
GROUP BY 或 DISTINCT 场景下 COUNT 容易翻车的地方
很多人以为 COUNT(*) 在 GROUP BY 里会自动过滤掉分组字段为 NULL 的组,其实不会:
-
SELECT gender, COUNT(*) FROM users GROUP BY gender会产生一行NULL | 12,只要gender有 NULL 值 - 想排除 NULL 分组?必须显式写
WHERE gender IS NOT NULL - 想统计每组中
name非空的人数?用COUNT(name),不是COUNT(*)—— 后者统计的是组内总行数,不管name是不是 NULL -
COUNT(DISTINCT email)本身就跳过email IS NULL的行,无需额外WHERE
大表上真要“快速”拿到总数,别硬刚 COUNT(*)
千万级以上的表,实时 COUNT(*) 就是 IO 密集型操作,没有银弹。可行路径只有三条:
- 用缓存:应用层维护一个计数器,增删时用原子操作(如 Redis
INCR/DECR)同步更新 - 用异步统计表:建一张
table_counts,通过定时任务或 Binlog 解析异步更新COUNT(*)结果 - 接受近似值:仅用于后台监控类场景,可用
ANALYZE TABLE后查information_schema.STATISTICS,但别用于前端展示或权限校验
最关键的一点容易被忽略:COUNT(*) 返回的是当前隔离级别下“可见”的行数,不是物理行数。RR 级别下,它反映的是事务启动时刻的快照,不是最新状态 —— 这不是 bug,是 MVCC 的必然结果。











