核心是分组键未对齐分片键导致中间结果跨节点搬运;需确保分组字段与分片键一致或可推导单一分片,通过EXPLAIN验证GroupBy是否下推至存储节点,并按过滤→分组→覆盖顺序设计复合索引。

跨分库分表的大规模聚合查询慢,核心不是GROUP BY本身慢,而是原始数据被拉到中心节点再聚合——中间结果动辄 GB 级,网络和内存全卡死。真正有效的优化,是让每个分片自己先算一部分,再合并。
确保分组字段与分片键对齐
这是最硬的前置条件。如果分片规则是user_id % 4,但你写GROUP BY region_code,那每个节点都得把全部数据发给协调器,局部聚合根本不会触发。
- 查清实际分片键:比如 ShardingSphere 配置里的
sharding-column,或 TiDB 的SHARD_ROW_ID_BITS对应字段 - 聚合字段必须是分片键,或能通过 WHERE 条件+索引推导出单一分片范围(例如
WHERE user_id BETWEEN 1000 AND 2000) - 避免隐式转换:分片键是
BIGINT,就别在 SQL 里传字符串'1001',否则路由失效,变成广播查询
强制下推聚合并验证执行计划
很多分布式数据库默认不启用两阶段聚合,得靠 Hint 或配置打开,且必须用 EXPLAIN 确认是否真下推了。
- TiDB:加
/*+ AGG_TO_COP() */,然后执行EXPLAIN FORMAT = 'VERBOSE',看GroupBy是否出现在TableReader下方 - StarRocks:确认
enable_local_shuffle_agg = true,且EXPLAIN输出中同时存在AGGREGATE (LOCAL)和AGGREGATE (GLOBAL) - Doris:检查
new_planner_agg_stage是否为2,并避免外层带HAVING或LIMIT——它们会让优化器跳过局部阶段
绕开无法下推的语法陷阱
某些常见写法会直接让优化器放弃局部聚合,哪怕分组键对齐也没用。
-
HAVING条件:如HAVING COUNT(*) > 100,必须改写成WHERE过滤(例如先筛status = 'paid')再聚合 -
COUNT(DISTINCT x):天然无法局部聚合,倾斜严重时建议换APPROX_COUNT_DISTINCT,或预建宽表 - 多列
GROUP BY+ 低基数字段:比如GROUP BY region, is_active,其中is_active只有 0/1,容易导致 coordinator 内存溢出 - SELECT 中出现非聚合、非分组列:如
SELECT region, MAX(salary), name FROM emp GROUP BY region,会强制拉全量行
实在不行,就别实时聚合
当业务允许秒级延迟,硬扛分布式实时 GROUP BY 往往是最贵的选择——它吃光网络、压垮 coordinator、还难调试。
- 高频小范围聚合(如按小时统计订单数):用
MATERIALIZED VIEW(Doris/TiDB 7.5+ 支持)或定时任务写入汇总表 - 大范围宽口径统计(如全站 UV):上 HyperLogLog(
HLL_UNION_AGG),误差可控且性能稳定 - 复杂嵌套聚合(含窗口函数):拆成两步,先存中间结果表,再查这张表做二次聚合
最容易被忽略的一点是:**聚合字段的索引顺序必须匹配访问路径——先过滤字段,再分组字段,最后覆盖 SELECT 字段**。建错一个复合索引,前面所有下推努力都白费。

















