摘要:从一次技术争论出发,通过性能测试和源码分析,揭秘COUNT()、COUNT(1)、COUNT(column)的真实差异。涵盖InnoDB和MyISAM的不同处理方式、覆盖索引的优化技巧、以及大表COUNT的5种优化方案,让你彻底搞懂"为什么COUNT()最快"、"什么时候用COUNT(column)"等面试高频问题。
💥 翻车现场
周五下午,哈吉米正在优化慢查询,南北绿豆路过看了一眼代码。
-- 哈吉米的SQL
SELECT COUNT(*) FROM order_info WHERE status = 1;
南北绿豆:"你怎么用COUNT(*)?应该用COUNT(1),性能更好!"
哈吉米:"???有区别吗?"
阿西噶阿西(凑过来):"我听说COUNT(主键)最快,你们俩都错了!"
南北绿豆:"不对不对,COUNT(1)是常量,不需要取值,肯定最快!"
哈吉米:"那……COUNT(column)呢?"
阿西噶阿西:"那个最慢,因为要判断NULL!"
三人争论了半小时,谁也说服不了谁。
哈吉米:"别吵了!咱们测试一下不就知道了?"
🤔 先说结论:COUNT(*)最快!
测试结果(100万条数据):
| SQL | 执行时间 | 扫描行数 |
|---|---|---|
COUNT(*) | 0.18秒 | 1000000 |
COUNT(1) | 0.18秒 | 1000000 |
COUNT(id) | 0.21秒 | 1000000 |
COUNT(status) | 0.35秒 | 1000000 |
结论:
- ✅ COUNT(*) 和 COUNT(1) 性能完全一样(MySQL优化器会优化成一样的)
- ✅ COUNT(主键) 稍慢(需要读主键值)
- ❌ COUNT(普通列) 最慢(需要判断NULL)
哈吉米:"卧槽,COUNT(*) 居然最快?我一直以为它最慢!"
南北绿豆:"那网上说COUNT(1)快是咋回事?"
阿西噶阿西:"来,我给你们讲讲原理。"
🔍 三种写法的本质区别
COUNT(*) - 统计所有行
定义:统计表的总行数,包括NULL值。
SELECT COUNT(*) FROM order_info;
-- 含义:统计表中有多少行记录
MySQL的优化:
- InnoDB会选择最小的索引来扫描(通常是二级索引)
- 不需要读取数据行,只需要统计索引记录数
COUNT(1) - 统计所有行(常量)
定义:统计表的总行数,包括NULL值。
SELECT COUNT(1) FROM order_info;
-- 含义:为每一行记录返回常量1,然后统计
实际执行:
- MySQL优化器会把COUNT(1)优化成COUNT(*)
- 执行计划完全一样
证明:
EXPLAIN SELECT COUNT(*) FROM order_info;
EXPLAIN SELECT COUNT(1) FROM order_info;
-- 两个EXPLAIN结果完全相同
+----+-------------+------------+-------+------+---------+
| id | select_type | table | type | key | rows |
+----+-------------+------------+-------+------+---------+
| 1 | SIMPLE | order_info | index | idx | 1000000 |
+----+-------------+------------+-------+------+---------+
南北绿豆:"所以COUNT(1)和COUNT(*)完全一样?"
阿西噶阿西:"对!MySQL优化器会把它们优化成同样的执行计划。"
COUNT(column) - 统计非NULL行
定义:统计某一列非NULL的行数。
SELECT COUNT(status) FROM order_info;
-- 含义:统计status列不为NULL的行数
执行过程:
- 扫描索引或表
- 读取status列的值
- 判断是否为NULL
- 如果不是NULL,计数+1
为什么慢?
- 需要读取列的值(IO开销)
- 需要判断NULL(CPU开销)
示例:
-- 表数据
id | status
---|-------
1 | 1
2 | 2
3 | NULL
4 | 1
5 | NULL
-- 查询结果
SELECT COUNT(*); -- 5(所有行)
SELECT COUNT(1); -- 5(所有行)
SELECT COUNT(status); -- 3(非NULL行)
📊 InnoDB vs MyISAM:COUNT(*)的不同处理
哈吉米:"为什么有时候COUNT(*)很快,有时候很慢?"
南北绿豆:"因为不同存储引擎的实现不同!"
MyISAM的COUNT(*)(秒级)
-- MyISAM表
SELECT COUNT(*) FROM myisam_table;
-- 执行时间:0.001秒(极快)
原理:
- MyISAM会在表的元数据中记录总行数
- COUNT(*)时直接返回这个值,不扫描表
- 前提:没有WHERE条件
验证:
-- 无WHERE条件(极快)
SELECT COUNT(*) FROM myisam_table;
-- 0.001秒
-- 有WHERE条件(慢)
SELECT COUNT(*) FROM myisam_table WHERE status = 1;
-- 5.2秒(需要扫描全表)
InnoDB的COUNT(*)(需要扫描)
-- InnoDB表
SELECT COUNT(*) FROM order_info;
-- 执行时间:0.18秒(需要扫描)
原理:
- InnoDB 不记录总行数(因为MVCC,不同事务看到的行数不同)
- 必须扫描索引或表来统计
- 会选择最小的索引来扫描(减少IO)
为什么不记录总行数?
事务A:
START TRANSACTION;
SELECT COUNT(*) FROM order_info; -- 结果:100万
事务B(同时进行):
START TRANSACTION;
INSERT INTO order_info VALUES (...); -- 插入了1条
SELECT COUNT(*) FROM order_info; -- 结果:100万 + 1
问题:同一时刻,两个事务看到的行数不同(MVCC)
所以InnoDB不能简单记录一个总行数
阿西噶阿西:"这就是为什么InnoDB的COUNT(*)比MyISAM慢的原因!"
🎯 COUNT(*)的优化策略
哈吉米:"那InnoDB的COUNT(*)怎么优化?"
南北绿豆:"有5种方案!"
优化1️⃣:利用覆盖索引
原理:MySQL会选择最小的索引扫描。
示例:
-- 表结构
CREATE TABLE order_info (
id BIGINT PRIMARY KEY, -- 8字节
order_no VARCHAR(32), -- 最多128字节
user_id BIGINT, -- 8字节
amount DECIMAL(10, 2),
status TINYINT, -- 1字节
create_time DATETIME,
INDEX idx_user_id(user_id), -- 二级索引:8字节
INDEX idx_status(status) -- 二级索引:1字节(最小)
);
-- COUNT(*)时,MySQL会选择idx_status索引
SELECT COUNT(*) FROM order_info;
-- Using index (idx_status)
EXPLAIN验证:
EXPLAIN SELECT COUNT(*) FROM order_info\G
*************************** 1. row ***************************
type: index
key: idx_status ← 选择了最小的索引
key_len: 1
Extra: Using index ← 覆盖索引
优化建议:
- 建一个占用空间最小的索引(如TINYINT列)
- MySQL会自动选择这个索引来COUNT
优化2️⃣:用近似值(EXPLAIN的rows)
原理:如果不需要精确值,可以用EXPLAIN的rows估算。
-- 不执行SQL,只看预估行数
EXPLAIN SELECT * FROM order_info\G
*************************** 1. row ***************************
rows: 982345 ← 预估行数(误差10%左右)
适用场景:
- 展示"大约XX万条数据"
- 数据看板(不需要精确)
优点:
- ✅ 极快(不扫描表)
- ✅ 误差可接受(10%以内)
优化3️⃣:用单独的计数表
原理:维护一个专门的计数表。
表结构:
CREATE TABLE count_cache (
table_name VARCHAR(50) PRIMARY KEY,
total_count BIGINT,
update_time DATETIME
);
更新计数:
-- 插入订单时,更新计数
START TRANSACTION;
INSERT INTO order_info VALUES (...);
-- 更新计数表(同一个事务)
INSERT INTO count_cache (table_name, total_count, update_time)
VALUES ('order_info', 1, NOW())
ON DUPLICATE KEY UPDATE
total_count = total_count + 1,
update_time = NOW();
COMMIT;
查询计数:
-- 极快(主键查询)
SELECT total_count FROM count_cache WHERE table_name = 'order_info';
优点:
- ✅ 查询极快(主键查询)
- ✅ 精确值
缺点:
- ❌ 维护成本高(每次INSERT/DELETE都要更新)
- ❌ 分布式场景下可能不一致
优化4️⃣:用Redis缓存
原理:把计数存到Redis,定期同步。
代码:
@Service
public class OrderService {
@Autowired
private RedisTemplate<String, Long> redisTemplate;
@Autowired
private OrderMapper orderMapper;
private static final String COUNT_KEY = "order:count";
/**
* 获取订单总数(从Redis)
*/
public Long getOrderCount() {
Long count = redisTemplate.opsForValue().get(COUNT_KEY);
if (count == null) {
// Redis没有,从MySQL查询并缓存
count = orderMapper.count();
redisTemplate.opsForValue().set(COUNT_KEY, count, 10, TimeUnit.MINUTES);
}
return count;
}
/**
* 创建订单(更新Redis)
*/
@Transactional
public void createOrder(Order order) {
// 1. 插入MySQL
orderMapper.insert(order);
// 2. Redis计数+1
redisTemplate.opsForValue().increment(COUNT_KEY);
}
}
定期同步:
// 定时任务:每小时同步一次
@Scheduled(cron = "0 0 * * * ?")
public void syncCountToRedis() {
Long count = orderMapper.count();
redisTemplate.opsForValue().set(COUNT_KEY, count);
}
优点:
- ✅ 查询极快(内存读取)
- ✅ 降低MySQL压力
缺点:
- ⚠️ 可能不精确(Redis和MySQL有延迟)
- ⚠️ 需要定期同步
优化5️⃣:用SHOW TABLE STATUS
原理:查看表的统计信息(近似值)。
SHOW TABLE STATUS LIKE 'order_info'\G
*************************** 1. row ***************************
Name: order_info
Rows: 982345 ← 近似行数
Data_length: 52428800
优点:
- ✅ 极快(不扫描表)
缺点:
- ❌ 不精确(误差可能较大)
- ❌ 只能统计整表,不能加WHERE条件
五种方案对比
| 方案 | 速度 | 精确度 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 覆盖索引 | ⭐⭐⭐ | ✅ 精确 | ⭐ | 通用方案 |
| EXPLAIN rows | ⭐⭐⭐⭐⭐ | ⚠️ 近似 | ⭐ | 不需要精确值 |
| 计数表 | ⭐⭐⭐⭐⭐ | ✅ 精确 | ⭐⭐⭐ | 实时性要求高 |
| Redis缓存 | ⭐⭐⭐⭐⭐ | ⚠️ 可能延迟 | ⭐⭐ | 高并发场景 |
| SHOW TABLE STATUS | ⭐⭐⭐⭐⭐ | ❌ 不精确 | ⭐ | 快速估算 |
🎓 面试高频题
题目1:COUNT(*) 和 COUNT(1) 哪个快?
标准答案:
性能完全一样。MySQL优化器会把COUNT(1)优化成COUNT(*),执行计划完全相同。
推荐用COUNT(*),因为:
- ✅ 符合SQL标准
- ✅ 语义清晰(统计行数)
- ✅ 所有数据库都支持
题目2:COUNT(*) 和 COUNT(主键) 哪个快?
标准答案:
COUNT(*) 更快。
原因:
- COUNT(*):MySQL会选择最小的索引扫描,不需要读取列值
- COUNT(主键):需要读取主键值,有额外开销
性能差距:约10-20%
题目3:COUNT(column) 什么时候用?
标准答案:
当你需要统计非NULL值的数量时。
示例:
-- 统计有手机号的用户数
SELECT COUNT(phone) FROM user;
-- 统计所有用户数
SELECT COUNT(*) FROM user;
注意:COUNT(column)最慢,因为需要读取列值并判断NULL。
题目4:如何优化大表的COUNT(*)?
标准答案:
5种方案:
- 覆盖索引:建一个最小的索引
- 近似值:用EXPLAIN的rows估算
- 计数表:维护专门的计数表
- Redis缓存:计数存Redis,定期同步
- 分表:单表过大时,分表后分别COUNT再求和
推荐方案:Redis缓存(高并发)或计数表(实时性)
🎉 结束语
晚上6点,三人终于测试完了所有方案。
哈吉米:"原来COUNT(*)和COUNT(1)完全一样,以前一直被误导了!"
南北绿豆:"对,而且COUNT(*)语义更清晰,推荐用这个。"
阿西噶阿西:"大表COUNT还是要优化,用Redis缓存最实用。"
哈吉米:"我回去就把代码里的COUNT(1)都改成COUNT(*)!"
南北绿豆:"别急,改不改都行,反正性能一样😂"
哈吉米:"……"
记忆口诀:
COUNT*最推荐,语义清晰性能好
COUNT数字优化同,列名统计判NULL
InnoDB要扫描,MyISAM有缓存
大表优化有五招,Redis缓存最实用
希望这篇文章能帮你彻底搞懂COUNT的性能问题!下次面试被问到,你就是最靓的仔!💪