千万级表group by卡顿主因是执行计划退化为sort+groupaggregate或io瓶颈,应查执行计划、调优内存参数、建匹配索引、用预聚合替代物化视图、识别并处理数据倾斜、权衡精度延迟资源。

GROUP BY 在千万级表上卡死,先看执行计划里有没有 HashAggregate 或 Sort
PostgreSQL 和大多数现代数据库在大数据量 GROUP BY 时,默认倾向用 HashAggregate(内存哈希聚合),但若内存不足或分组键基数太高,会退化成 Sort + GroupAggregate,而排序阶段极易成为瓶颈。MySQL 则更常依赖索引排序,没合适索引就全表扫描+临时表,IO 拉满。
实操建议:
- 用
EXPLAIN (ANALYZE, BUFFERS)看实际执行路径,重点关注Actual Total Time和Buffers中的shared read量 - 检查
work_mem(PG)或sort_buffer_size(MySQL)是否被低估——但别盲目调大,多个并发查询会吃光内存 - 确认分组字段是否有有效索引:复合索引顺序必须匹配
GROUP BY字段顺序,且避免在索引列上用函数或表达式
实时场景下用物化视图还是增量聚合?别直接选物化视图
PG 的 MATERIALIZED VIEW 虽然能固化结果,但刷新是全量重算,对实时性要求高的场景(比如秒级更新的运营看板),REFRESH MATERIALIZED VIEW CONCURRENTLY 仍会锁住旧数据,且无法反映刷新窗口内的新写入。
实操建议:
- 优先考虑“预聚合表 + 定时增量合并”:例如每分钟跑一次
INSERT INTO agg_1min SELECT ... GROUP BY time_bucket('1 minute', ts), tag_id(用 TimescaleDB 或自建时间分区) - 若用 ClickHouse,直接上
ReplacingMergeTree+MATERIALIZED VIEW,它支持真正的流式插入和后台异步去重聚合 - 避免在 OLTP 主库上建复杂聚合视图——哪怕加了索引,
SELECT COUNT(*) FROM view_name仍可能触发底层全表扫描
并行查询不是开个开关就加速:注意数据倾斜和线程争抢
PostgreSQL 10+ 默认开启并行查询,但 max_parallel_workers_per_gather 设为 4 不代表每次都能跑满 4 个 worker——如果 GROUP BY 键分布极不均匀(比如 90% 的数据属于同一个 user_id),所有 worker 都得等那个最慢的归并,反而比单线程还慢。
实操建议:
- 用
SELECT count(*), user_id FROM t GROUP BY user_id ORDER BY count(*) DESC LIMIT 5快速识别头部倾斜键 - 对已知倾斜键单独处理:先
FILTER WHERE user_id != 'hot_id'并行聚合,再UNION ALL单独查 hot_id 的结果 - MySQL 8.0 的并行查询仅限于
INFORMATION_SCHEMA查询或特定引擎(如 NDB),普通 InnoDB 表不支持真正并行聚合
实时分组聚合绕不开的取舍:精度、延迟、资源消耗三选二
想让 GROUP BY 结果毫秒级返回,又要求完全精确、还不占额外存储和 CPU?不存在的。比如用 Redis HyperLogLog 做去重计数,快且省内存,但结果是概率性误差;用 Kafka + Flink 做窗口聚合,延迟可控但运维成本陡增;纯 SQL 加索引硬扛,资源消耗随数据量线性上涨。
实操建议:
- 明确业务可接受的误差范围:UV 统计允许 ±2%?订单金额总和必须 100% 准确?这直接决定能否引入近似算法
- 时间维度聚合尽量对齐自然窗口(如整点、整分),避免
GROUP BY floor(extract(epoch from ts)/60)这类表达式——它会让索引失效,且无法利用分区裁剪 - 别忽略写入链路的影响:如果聚合依赖的原始表每秒写入 10 万行,而你的
GROUP BY查询每 5 秒跑一次,那 PG 的 WAL 和 autovacuum 压力可能比查询本身还致命
最常被跳过的一步是压测时没模拟真实写入压力——只测空库或静态数据集,结果上线后发现查询响应从 200ms 涨到 8s,因为 buffer cache 被写操作持续冲刷,聚合时反复读盘。










