系列文章:
MySQL 慢查询优化:从定位到根治的完整思路
分库分表:为什么我不建议一上来就分表
Redis 都有哪些用处?都会用在什么地方?
分布式与分布式锁:从访问量到数据一致性
从最左前缀到地址索引:我对 MySQL 索引的一次完整复盘
- (本篇)
核心观点:排行榜不是算出来的,是提前算好的。
一、一个很"狼性"的业务场景
假设我们有这样一张订单表:
- 数据量:1 亿条
- 业务员:1000 人
- 字段:
salesman_id(业务员ID)、amount(订单金额)
业务需求很直接:
实时统计每个业务员的总业绩,并按金额排名,展示 TOP 1 / TOP 10 ,用于"狼性竞争"排行榜,前端页面高频刷新。
对应的 SQL,很多同学第一反应是:
vbnet
SELECT salesman_id, SUM(amount) AS total
FROM orders
GROUP BY salesman_id
ORDER BY total DESC
LIMIT 10;
但问题是------这条 SQL,在生产环境是跑不起来的。
二、为什么不能直接 GROUP BY?
我们先看索引:
scss
CREATE INDEX idx_salesman_amount ON orders(salesman_id, amount);
看起来很合理,对吧?
但现实是:
-
GROUP BY salesman_id:需要对 1000 万个分组进行聚合 -
SUM(amount):必须扫描 1 亿行数据 -
即使有索引,MySQL 也只能:
- 用索引定位
salesman_id - 但仍然要遍历所有分组
- 无法避免全量聚合
- 用索引定位
在 1 亿数据量下:
- 单次查询:秒级甚至分钟级
- 高频刷新:数据库直接被打爆
- 连接池耗尽、接口超时、雪崩效应
👉 结论很明确:
索引可以加速"单业务员查询",但救不了"全量聚合排名"。
三、正确思路:预聚合(汇总表)
这是99% 互联网公司做排行榜的标准解法。
1️⃣ 建一张"业务员业绩汇总表"
sql
CREATE TABLE salesman_rank (
salesman_id BIGINT PRIMARY KEY,
total_amount DECIMAL(18,2) NOT NULL,
update_time DATETIME,
INDEX idx_total (total_amount DESC)
);
这张表只有 1000 行(对应 1000 个业务员)。
2️⃣ 定时任务:离线计算,实时查询
方案 A:全量刷新(简单可靠)
sql
TRUNCATE salesman_rank;
INSERT INTO salesman_rank (salesman_id, total_amount, update_time)
SELECT salesman_id, SUM(amount), NOW()
FROM orders
GROUP BY salesman_id;
- 每天 / 每小时跑一次
- 业务上完全可以接受"分钟级延迟"
- 查询压力几乎为零
方案 B:增量更新(更优雅)
vbnet
UPDATE salesman_rank r
JOIN (
SELECT salesman_id, SUM(amount) AS inc
FROM orders
WHERE create_time > :last_update_time
GROUP BY salesman_id
) t ON r.salesman_id = t.salesman_id
SET r.total_amount = r.total_amount + t.inc,
r.update_time = NOW();
3️⃣ 实时查询:毫秒级返回
sql
SELECT salesman_id, total_amount
FROM salesman_rank
ORDER BY total_amount DESC
LIMIT 10;
- 扫描 10 行
- 走索引
- 高并发毫无压力
- 前端轮询 / WebSocket 推送都没问题
四、如果一定要"秒级实时"呢?
在某些极端场景(如直播带货实时榜单),分钟级延迟不够,怎么办?
👉 引入 Redis Sorted Set(ZSet)
1️⃣ 订单写入时,双写 Redis
ZINCRBY salesman:rank 199.5 salesman_42
2️⃣ 查询 TOP 10
ZREVRANGE salesman:rank 0 9 WITHSCORES
3️⃣ 数据一致性保障
- MySQL 仍是最终权威数据源
- Redis 只做展示层缓存
- 定时任务定期从 MySQL 修复 Redis 数据
✅ 读:O(log N)
✅ 写:极低开销
✅ 天生适合排行榜
五、一个非常重要的认知
很多同学会问:
那我给
salesman_id + amount建联合索引,不就可以了吗?
答案是:
可以加速"单业务员查询",但不能避免"全量聚合"。
排行榜的本质问题是:
- 不是过滤行
- 而是汇总所有行
而索引擅长的是"过滤",不是"汇总"。
六、总结:排行榜的三层架构
| 层级 | 技术方案 | 适用场景 |
|---|---|---|
| 离线层 | 汇总表 + 定时任务 | 绝大多数业务 |
| 近实时层 | 汇总表 + 增量更新 | 分钟级延迟 |
| 实时层 | Redis ZSet | 秒级榜单 |
一句话总结:
排行榜不是算出来的,是提前算好的。
七、面试加分回答
如果面试官问你:
"一亿数据,怎么实时统计 TOP 10?"
你可以这样答:
我不会直接 GROUP BY 原表,因为会触发全表扫描和全量聚合,性能无法支撑高频查询。
正确做法是做预聚合:建一张业务员业绩汇总表,通过定时任务维护总金额,查询时直接从汇总表取 TOP 10。
如果对实时性要求极高,可以用 Redis ZSet,在订单写入时增量更新排名,查询走内存。
MySQL 原表即使有联合索引,也只能加速单业务员查询,不能避免全量聚合,所以排行榜一定要引入汇总表或内存结构。
八、写在最后
这个问题,其实是数据库架构思维的一个分水岭:
- 初级:想着怎么写 SQL
- 中级:想着怎么建索引
- 高级:想着怎么避免算
这也是为什么我们常说:
好的架构,不是算得快,而是算得少。