MySQL 笔记:count(*)、count(1)、count(列) 的区别

· Mysql笔记 MySQL SQL优化 InnoDB · 1 阅读 · 约 6 分钟

前言

count(*) 和 count(1) 谁快,几乎是面试必问。但先把语义搞清楚,性能讨论才有意义。

语义区别

count(表达式) 的规则很简单:统计表达式结果非 NULL 的行数。

SELECT COUNT(*), COUNT(1), COUNT(id), COUNT(category_id) FROM article;
  • COUNT(*):SQL 标准里表示"统计行数",不关心任何列,NULL 行也计入
  • COUNT(1):常量 1 对每行都非 NULL,所以结果和 COUNT(*) 完全一致
  • COUNT(列):该列为 NULL 的行不计入,结果可能更小
  • COUNT(DISTINCT a, b):按列组合去重后统计非全 NULL 的组合

所以如果表里 category_id 允许 NULL,COUNT(category_id) 统计出来的是"有分类的行数",而不是总行数。这一条经常被当成 bug。

那性能呢

在 MySQL 里 COUNT(1) 和 COUNT(*) 没有任何区别,优化器把 count(*) 特殊处理成"统计行数",不会真的去取所有列。至于 count(主键) 更快这种说法,方向反了:主键非空所以结果一样,但 InnoDB 不一定非要扫主键。

拿本机的一张真实表测一下:

EXPLAIN SELECT COUNT(*) FROM blog_article;
+----+-------------+--------------+-------+---------------+---------+---------+------+-------+-------------+
| id | select_type | table        | type  | possible_keys | key     | key_len | ref  | rows  | Extra       |
+----+-------------+--------------+-------+---------------+---------+---------+------+-------+-------------+
|  1 | SIMPLE      | blog_article | index | NULL          | idx_top | 1       | NULL |   106 | Using index |
+----+-------------+--------------+-------+---------------+---------+---------+------+-------+-------------+

它挑了 idx_top(只有 is_top 一个 TINYINT 列),而不是主键——因为 COUNT(*) 只需要数行数,哪棵索引树最小就扫哪棵,页更少、IO 更省。换成 COUNT(status),优化器就改用 idx_list,因为它得看 status 这一列在不在索引里。结论是:

  • 想让 count(*) 快,靠的是"存在一个更小的二级索引",不是改写 count(1)
  • count(某列) 反而限制了优化器可选的索引,还要多做非空判断

InnoDB 为什么不缓存总行数

MyISAM 把精确行数存在表头元数据里,无 WHERE 的 COUNT(*) 直接返回,这是它的一个优势。InnoDB 做不到:MVCC 下每个事务看到的行集本来就可能不同(别人有未提交的插入、有人拿的是更早的快照),因此只能实时扫。

顺带一个读法:快照与计数

在 REPEATABLE READ 的同一个事务里连续两次 COUNT(*),结果是一样的,因为复用同一个 ReadView,看到的行集固定。但两个并发事务之间读到不同行数是完全正常的,这正是 InnoDB 没法把总数缓存在表头的根本原因。写监控脚本或者对账程序时,别拿两个连接各自的 COUNT(*) 差异当故障报警,要么在同一事务里读,要么用带条件的、确定性的口径统计。

需要"总数"时的几种做法

-- 1. 统计信息里的估算值(InnoDB 是估算,不是精确值)
SELECT TABLE_ROWS FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'blog_php' AND TABLE_NAME = 'blog_article';

这个数受统计信息新鲜度影响,innodb_stats_on_metadata 在 5.7 默认关闭,所以它随时可能差得比较多,只能用来做数量级展示,不能当业务计数。

其余几条按场景选:

  • 带条件的计数:保证条件列有索引,能覆盖更好,避免全表扫
  • 分页总数:单独一次 COUNT(*) 加缓存(Redis 或应用缓存),别让每页都重复统计
  • 计数器需求:用 UPDATE ... SET cnt = cnt + 1 维护一张计数表,或者 Redis INCR
  • 分区表:按分区分别统计再求和
  • 极宽表:直接限制查询时间窗,比如只统计最近一年的总数

小结

  • 语义上先记住一句话:count(列) 排除 NULL,count(*) 不排除
  • count(1) 不比 count(*) 快,官方推荐写法就是 count(*)
  • InnoDB 的 count(*) 会挑最小的索引扫,建索引比改函数名有用
  • 真正的大表总数应该走缓存或估算,不要指望一句 COUNT(*) 撑住高并发