一亿数据下,如何实时统计“业务员业绩 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
  • 中级:想着怎么建索引
  • 高级:想着怎么避免算

这也是为什么我们常说:

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

相关推荐
QuZhengRong5 分钟前
【Spring】后端接收的请求参数多了一个逗号的处理办法
java·后端·spring
打工仔折腾 AI8 分钟前
从零写一个CAD 02:实体容器、Esc取消与键盘失灵的排查
人工智能·后端·python·性能优化
用户83562907805122 分钟前
使用 Python 拆分和提取 PDF 页面
后端·python
用户83562907805123 分钟前
使用 Python 操作 PowerPoint 中的图片
后端·python
对象存储与RustFS44 分钟前
自建对象存储的第一个决定:单机就够,还是必须上分布式
后端·rust·开源
用户8181870627461 小时前
第37章 Java应用在K8s里的经典坑:容器内存/CPU limit与JVM参数不匹配导致的OOMKilled
java·后端
小小张说故事1 小时前
pandas 读大 CSV 太慢?6 个实测提速技巧(dtype / 分块 / 引擎选择)
后端·python·pandas
喵个咪1 小时前
GoWind Admin|风行 — 开箱即用的企业级全栈中后台框架:多租户与行级数据隔离
后端·go
一直在努力的小宁1 小时前
【阅读笔记】具身操作的数采方案概览
后端·json·restful·具身智能·vla·vlm
喵个咪1 小时前
GoWind Admin|风行 — 开箱即用的企业级全栈中后台框架:脚本系统实战
javascript·后端·go