前言
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维护一张计数表,或者 RedisINCR - 分区表:按分区分别统计再求和
- 极宽表:直接限制查询时间窗,比如只统计最近一年的总数
小结
- 语义上先记住一句话:
count(列)排除 NULL,count(*)不排除 count(1)不比count(*)快,官方推荐写法就是count(*)- InnoDB 的
count(*)会挑最小的索引扫,建索引比改函数名有用 - 真正的大表总数应该走缓存或估算,不要指望一句
COUNT(*)撑住高并发