COUNT(*) 查询的优化策略
COUNT(*) 是常见的”性能杀手”
SELECT COUNT(*) FROM order; -- 全表扫描 1 亿行
对于 OLTP 系统,这种查询在数据量大时会导致严重的性能问题。
COUNT 的不同写法
-- 四种等价的写法
COUNT(*) -- 统计行数,包含 NULL 值的行
COUNT(1) -- 统计行数,等价于 COUNT(*)
COUNT(id) -- 统计 id 不为 NULL 的行数
COUNT(status) -- 统计 status 不为 NULL 的行数
关键区别:
– COUNT(*) 和 COUNT(1) 完全等价,都统计所有行(包括所有列为 NULL 的行)
– COUNT(column) 只统计该列不为 NULL 的行
– MySQL 8.0 中 COUNT(*) 已经针对 InnoDB 做了优化,性能等于 COUNT(1)
InnoDB 下 COUNT(*) 的性能瓶颈
MyISAM vs InnoDB
-- MyISAM:COUNT(*) 是 O(1) 操作
-- MyISAM 表有单独的行数计数器
SELECT COUNT(*) FROM myisam_table; -- 瞬间返回
-- InnoDB:COUNT(*) 需要扫描索引
SELECT COUNT(*) FROM innodb_table; -- 需要扫描索引
InnoDB 不能像 MyISAM 那样缓存行数,因为 InnoDB 支持 MVCC,不同事务看到的行数可能不同。
为什么 InnoDB 的 COUNT(*) 慢
InnoDB 执行 COUNT(*) 的过程:
1. 选择一个最小的二级索引(索引的叶子节点不包含完整行数据,更紧凑)
1 页 B+Tree 可存 1000 个键值 vs 数据页只能存约 100 行
2. 扫描整个索引页,累加行数
3. 索引越大,扫描越慢
优化策略
策略一:使用近似值
很多业务场景不需要精确的行数:
-- 精确 COUNT
SELECT COUNT(*) FROM order WHERE status = 1;
-- 使用 INFORMATION_SCHEMA 获取近似值
SELECT TABLE_ROWS
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'order';
-- TABLE_ROWS 是估算值,可能不精确,但非常快
使用场景:后台统计、数据看板、展示”约 X 条结果”。
策略二:使用独立的计数表
-- 创建计数表
CREATE TABLE row_count (
table_name VARCHAR(100) PRIMARY KEY,
count BIGINT NOT NULL DEFAULT 0
);
-- 在业务代码中维护计数
-- 插入时 +1
UPDATE row_count SET count = count + 1 WHERE table_name = 'order';
-- 删除时 -1
UPDATE row_count SET count = count - 1 WHERE table_name = 'order';
-- 查询时直接读取
SELECT count FROM row_count WHERE table_name = 'order';
注意:事务中 +1 和插入操作需要在同一个事务中保证一致性。如果并发高,计数表本身会成为热点。
策略三:定时统计缓存到 Redis
// 每 5 分钟统计一次订单总数,缓存到 Redis
@Component
public class CountCacheJob {
@Scheduled(cron = "0 */5 * * * ?")
public void refreshOrderCount() {
long count = orderMapper.count();
redisTemplate.opsForValue().set("order_count", count);
}
// API 接口查询
public long getOrderCount() {
Long count = redisTemplate.opsForValue().get("order_count");
return count != null ? count : 0L;
}
}
适用于对实时性要求不高的统计场景。
策略四:使用 SHOW TABLE STATUS
SHOW TABLE STATUS LIKE 'order';
-- 输出中 Rows 字段是 InnoDB 的估算值(基于采样)
-- 在数据变化不频繁时,这个估算值也可以接受
策略五:优化带 WHERE 条件的 COUNT
-- 优化 COUNT(*) WHERE column = value
-- 确保 WHERE 条件列上有索引
-- 坏(无索引):
SELECT COUNT(*) FROM order WHERE status = 1;
-- 全表扫描
-- 好(有索引):
CREATE INDEX idx_status ON order(status);
SELECT COUNT(*) FROM order WHERE status = 1;
-- 只需要扫描 idx_status 索引
-- 更好(覆盖索引):
CREATE INDEX idx_status_created ON order(status, created_at);
SELECT COUNT(*) FROM order WHERE status = 1 AND created_at > '2024-01-01';
-- 直接在索引中完成 COUNT,不用回表
策略六:用数据仓库或 ES 替代
对于复杂的统计分析(如查指定任意时间段的数据量),使用 OLAP 系统或 Elasticsearch:
-- MySQL 负责 OLTP,不承担统计压力
-- 通过 binlog 同步到 ClickHouse 或 ES
-- 统计查询走 ES / ClickHouse
不同数据量级的优化选择
| 数据量 | 优化策略 |
|---|---|
| < 100 万 | 直接 COUNT,不需要优化 |
| 100 万 – 1000 万 | 确保索引正确,执行计划走索引扫描 |
| 1000 万 – 1 亿 | 使用计数表或定时缓存 |
| > 1 亿 | 数据仓库/OLAP/ES 分担 |
面试要点
- InnoDB 的 COUNT(*) 需要扫描索引(区别于 MyISAM 的 O(1))
- 优化 COUNT 的核心思路是:计数缓存或减小索引扫描范围
- 使用近似值在很多业务场景下是可以接受的(如”搜索到 X 条结果”)
- 精确计数在分布式系统中代价极高,能接受最终一致性就最好
- 理解 COUNT(*) 和 COUNT(column) 的细微区别
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容