优化 COUNT(*) 查询

优化 COUNT(*) 查询

COUNT(*) 的误区

很多人以为 COUNT(*) 是 MySQL 中最简单的操作,实际上它可能是最容易被误解的查询之一。

首先纠正一个常见错误认识:MyISAM 引擎的 COUNT(*) 确实极快(直接读行数计数器),但 InnoDB(MySQL 5.5+ 的默认引擎)不是。InnoDB 由于支持 MVCC 和事务隔离,COUNT(*) 需要逐行判断当前事务是否可见,不能使用独立的计数器

不同 COUNT 变体的对比

-- 四种 COUNT 写法,性能差异显著
COUNT(*)      -- 统计行数,不关心是否有 NULL
COUNT(1)      -- 统计行数,与 COUNT(*) 等价
COUNT(col)    -- 统计 col 列非 NULL 的行数
COUNT(DISTINCT col)  -- 统计 col 列的去重非 NULL 行数

注意:COUNT(*)COUNT(1) 在 MySQL 中完全等价。优化器会把 COUNT(1) 转换为 COUNT(*)

如何优化 COUNT(*)

方案一:使用辅助索引

-- 慢:走主键索引(聚簇索引,数据量大)
SELECT COUNT(*) FROM big_table;

-- 快:走辅助索引(二级索引比聚簇索引小)
-- MySQL 会选最小的辅助索引来计数

InnoDB 在执行 COUNT(*) 时,会自动选择最小的二级索引(最窄的索引树)来遍历,因为:
– 聚簇索引包含所有字段,树很大
– 二级索引只包含索引列和主键,树很小

所以,有一个二级索引甚至比没有索引更快。如果你经常对某张表做 COUNT(*),可以创建一个最小字段的复合索引用作计数。

方案二:使用近似值

对于不要求精确计数的场景(如后台列表的”共 XX 条”),使用 SHOW TABLE STATUS 的估算值。

-- 获取近似行数,几乎零成本
SHOW TABLE STATUS LIKE 'big_table'\G
-- 输出中的 Rows 是估算值(InnoDB 的采样估算)

或者用 EXPLAIN

-- EXPLAIN 的 rows 列也包含估算值
EXPLAIN SELECT COUNT(*) FROM big_table;

方案三:使用独立的计数表

创建一张小表专门维护行数:

CREATE TABLE row_count (
    table_name VARCHAR(50) PRIMARY KEY,
    count BIGINT NOT NULL
);

-- 在业务代码中维护,插入一条 count+1,删除一条 count-1
-- 或使用触发器

优点SELECT count FROM row_count WHERE table_name = 'orders' 是 O(1) 操作
缺点:需要额外的维护逻辑,在分布式环境下计数可能不准

方案四:使用 Redis 缓存计数

// 插入时
redis.incr("count:orders");
// 删除时
redis.decr("count:orders");
// 查询时
Long count = redis.get("count:orders");

优点:性能极高,O(1)
缺点:Redis 宕机可能丢失计数,需要做数据一致性

方案五:使用信息统计表

在系统设计时,直接设计一张统计表:

CREATE TABLE order_statistics (
    stat_date DATE PRIMARY KEY,
    total_count INT NOT NULL,
    yesterday_count INT NOT NULL
);

-- 每天通过定时任务维护统计
INSERT INTO order_statistics (stat_date, total_count)
VALUES ('2024-12-31', (SELECT COUNT(*) FROM orders))
ON DUPLICATE KEY UPDATE total_count = VALUES(total_count);

带条件的 COUNT 优化

-- 场景:统计某个月的订单数
SELECT COUNT(*) FROM orders WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31';

创建合适的索引是关键:

CREATE INDEX idx_create_time ON orders(create_time);

但即使有索引,如果条件过滤效果差(结果集很大),COUNT(*) 仍然需要扫描大量行。这时上面提到的计数表方案更合适。

海量数据下的终极方案

当表行数达到亿级时:
1. 放弃 COUNT(*) 实时精确计数
2. 改用预聚合 + 定时任务维护计数
3. 前端展示时加上”约”字(如”约 1.2 亿条”)

各方案对比

方案 精确度 性能 维护成本
辅助索引 COUNT 精确
SHOW TABLE STATUS 近似
计数表 精确 极高
Redis 近似 极高
预聚合统计表 精确 极高

总结

COUNT(*) 优化的核心思路是避免每次查询都扫描大量行。从最轻量的”利用最窄二级索引”,到中等成本的”计数表”,再到高成本的”Redis 计数”,根据业务对精确度和实时性的要求选择合适的方案。对于 TB 级表,实时精确的 COUNT 本身就是不可能的,要接受近似值或异步更新的设计方案。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容