一亿数据下,如何实时统计“业务员业绩 TOP 10”?别再 GROUP BY 了

系列文章:

  1. MySQL 慢查询优化:从定位到根治的完整思路

  2. 分库分表:为什么我不建议一上来就分表

  3. Redis 都有哪些用处?都会用在什么地方?

  4. 分布式与分布式锁:从访问量到数据一致性

  5. 从最左前缀到地址索引:我对 MySQL 索引的一次完整复盘

  6. (本篇)

核心观点:排行榜不是算出来的,是提前算好的。


一、一个很"狼性"的业务场景

假设我们有这样一张订单表:

  • 数据量: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
  • 中级:想着怎么建索引
  • 高级:想着怎么避免算

这也是为什么我们常说:

好的架构,不是算得快,而是算得少。

相关推荐
Oneslide1 小时前
systemd-journald 使用与配置指南
后端
神奇小汤圆1 小时前
30分钟实现自己的AI聊天机器人(SpringBoot接入DeepSeek API)
后端
再吃一根胡萝卜1 小时前
从最左前缀到地址索引:我对 MySQL 索引的一次完整复盘
后端
郡杰1 小时前
Spring Boot 请求流转与 RESTful
后端
再吃一根胡萝卜2 小时前
Redis 都有哪些用处?都会用在什么地方?
后端
再吃一根胡萝卜2 小时前
分布式与分布式锁:从访问量到数据一致性
后端
再吃一根胡萝卜2 小时前
分库分表:为什么我不建议一上来就分表
后端
再吃一根胡萝卜2 小时前
MySQL 慢查询优化:从定位到根治的完整思路
后端
Nturmoils3 小时前
订单查询丢数据,查了半天原来是 WHERE 惹的祸
数据库·后端