优化亿级数据group by需避免全表扫描和临时表,核心是建where与group by字段顺序匹配的复合索引(如(dt, channel))、启用分区裁剪、高基数字段降维、禁用select*,并优先采用预聚合表替代实时分组。

亿级数据上直接写 GROUP BY 基本等于主动触发数据库的性能熔断——不是慢,是卡死或 OOM。优化核心不是“怎么写对”,而是“怎么让数据库少算、不扫、不建临时表”。
为什么 GROUP BY 在亿级表上会崩?
MySQL 或 Hive 执行 GROUP BY 时,典型流程是:全表扫描 → 内存哈希分组(hash_group)→ 超出内存就落磁盘临时表 → 排序/聚合 → 返回。一旦数据量超 tmp_table_size 或 work_mem,就会写磁盘,I/O 爆涨;若分组键基数高(如 user_id 是 UUID),哈希桶数量爆炸,内存直接耗尽。
常见错误现象包括:
-
EXPLAIN显示Using temporary; Using filesort - 查询执行数分钟甚至超时,
SHOW PROCESSLIST中状态长期卡在Copying to tmp table - MySQL 报错
ERROR 1105 (HY000): Out of memory或 Hive 报java.lang.OutOfMemoryError: Java heap space
必须建的复合索引长什么样?
索引不是“有就行”,而是要同时满足过滤 + 分组 + 覆盖。顺序和字段选择错了,等于没建。
假设你常跑这个查询:
SELECT dt, channel, COUNT(*), SUM(pv) FROM event_log WHERE dt >= '2026-07-01' GROUP BY dt, channel;
对应索引应为:
-
(dt, channel)——WHERE字段dt必须最左,GROUP BY字段按顺序紧随其后 - 如果还要查
SUM(pv),且pv频繁访问,建议扩展为(dt, channel, pv),形成覆盖索引,避免回表 - 禁用
INDEX(channel, dt)或INDEX(dt, pv, channel):前者无法加速WHERE dt >= ...,后者破坏分组字段连续性,优化器大概率弃用
先过滤、再分组,但 WHERE 不是万能的
单纯加 WHERE 只能减少扫描行数,不能解决高基数分组或聚合函数开销大的问题。
更有效的前置过滤方式包括:
- 用分区裁剪代替
WHERE:把event_log按dt建PARTITION BY RANGE (TO_DAYS(dt)),查询自动只读目标分区,跳过 99% 数据 - 对高基数字段降维:不直接
GROUP BY user_id,改用GROUP BY FLOOR(ABS(CRC32(user_id)) % 100)做抽样分桶(适合监控类场景) - 用子查询预聚合:比如先按小时聚合 PV,再按天合并,把十亿行压成万行后再
GROUP BY dt - 禁用
SELECT *和无意义列:每多一列都增加哈希内存占用和网络传输压力
预聚合表比“优化 SQL”更有效
对固定维度(如按天+渠道+设备类型)的统计,硬扛实时 GROUP BY 是反模式。真正稳定的解法是把计算搬离查询路径。
可落地的预聚合策略:
- 每日凌晨跑一次任务,将当日明细写入汇总表
event_daily_summary(dt, channel, device_type, total_pv, uniq_users),查询直接SELECT * FROM event_daily_summary - Hive 中建物化视图(
CREATE MATERIALIZED VIEW)或使用INSERT OVERWRITE TABLE ... SELECT ... GROUP BY定时刷新 - MySQL 中用事件(
EVENT)定时更新汇总表,或借助 Binlog + Flink 做流式预聚合 - 注意:预聚合表需配套维护机制,比如删除过期数据、校验一致性(可用
CHECKSUM TABLE或抽样比对)
最容易被忽略的一点:预聚合不是“加个定时任务”就完事——它要求业务能接受 T+1 延迟,并且所有下游报表、API 必须切换到新表。否则一边查明细表一边查汇总表,数据对不上,问题更难排查。











