亿级表GROUP BY统计必须配合索引设计、数据裁剪、预计算,否则COUNT(DISTINCT)会触发临时表膨胀和磁盘排序;核心瓶颈在于隐式开销:去重导致磁盘排序、回表随机IO、全表扫描及索引失效。

直接说结论:亿级表的 GROUP BY 统计不能靠“写对SQL”就搞定,必须配合索引设计、数据裁剪、预计算三板斧,否则哪怕加了索引,COUNT(DISTINCT) 依然会触发临时表膨胀和磁盘排序。
为什么 GROUP BY 在亿级表上容易慢?
核心不是分组动作本身,而是数据库执行时的隐式开销:
-
COUNT(DISTINCT)必须去重,MySQL/Oracle 默认走Using temporary; Using filesort,内存撑不住就刷到磁盘,IO飙升; - 没覆盖索引时,
GROUP BY需回表读原始行,亿级扫描+随机IO是性能黑洞; - 如果
WHERE条件没走索引,先全表扫描再分组,等于把10亿行全拉进内存排队; - 复合索引列顺序错(比如建了
(time, channel)却只查channel),索引直接失效。
必须建的复合索引怎么设计?
不是“给 GROUP BY 字段加索引”就行,得按查询模式反推。以交易流水表 BANK_OPER 为例:
- 典型查询带
WHERE OPER_TIME BETWEEN ... AND ... AND BIZ_CHANNEL IN (...)+GROUP BY BIZ_CHANNEL; - 最优索引是
INDEX idx_time_channel (OPER_TIME, BIZ_CHANNEL, SERIAL_ID, PAY_ACCT, ENTERPRISE_CODE, OPER_AMOUNT); - 前两列满足最左前缀用于过滤,后四列覆盖所有聚合字段,避免回表;
- 注意:
SERIAL_ID等字段放索引里只为覆盖,不参与排序,所以顺序无关紧要; - 别用
TEXT或大字段建索引,会拖慢索引构建和更新。
怎么避免 COUNT(DISTINCT) 拖垮查询?
这是亿级统计最常卡死的点,光靠索引解决不了:
- 业务允许误差时,用近似函数:MySQL 8.0+ 可用
APPROX_COUNT_DISTINCT(SERIAL_ID),PostgreSQL 用approx_count_distinct(); - 不允许误差但查询频次高,提前建预聚合表:每天凌晨跑一次
INSERT INTO daily_channel_stats SELECT BIZ_CHANNEL, COUNT(DISTINCT SERIAL_ID), ... FROM BANK_OPER WHERE OPER_TIME = '2026-08-11' GROUP BY BIZ_CHANNEL; - 硬要实时查,把
DISTINCT拆成两层:先用子查询按渠道抽样 ID 列表,再在外层GROUP BY计数,减少中间集大小; - 绝对不要在
WHERE里对索引列用函数,比如WHERE DATE(OPER_TIME) = '2026-08-11'—— 这会让整个时间索引失效。
分区表和统计信息容易被忽略的细节
分区不是银弹,用错反而更慢:
- 按
OPER_TIME分区时,必须确保WHERE条件能精准命中分区(如BETWEEN '2026-08-01' AND '2026-08-31'),否则会扫全部分区; - 分区后仍要对每个分区建局部索引,全局索引在分区表上效果差;
- 统计信息过期会导致优化器选错执行计划:执行
DBMS_STATS.GATHER_TABLE_STATS(Oracle)或ANALYZE TABLE(MySQL)后,再查EXPLAIN确认rows估值是否合理; - 别信“自动收集”,尤其在批量导入后,手动触发一次统计更新比等自动任务靠谱得多。
真正卡住亿级分组统计的,往往不是语法或逻辑,而是索引是否覆盖、COUNT(DISTINCT) 是否被当成黑盒硬算、以及分区裁剪有没有生效——这三处任何一个没对齐,查询就会从秒级掉到分钟甚至小时级。










