预聚合需严格对齐分片键、控制聚合粒度、规避高基数字段,否则反而拖慢查询;分片键必须与原集合一致,聚合字段须被分片键前缀覆盖,配合索引、路由代理、增量一致性机制才能生效。

预聚合不是加个 allowDiskUse: true 就能扛住高并发查询的,它本质是用空间换时间,但必须对齐分片键、控制粒度、规避高基数字段,否则反而拖慢查询。
预聚合表的分片键必须和原集合完全一致
如果原集合按 { region: 1, user_id: 1 } 分片,预聚合表也得用这个键。否则 mongos 无法路由到目标 shard,查询会退化成广播扫描——所有 shard 都参与响应,延迟直接翻倍甚至超时。
- 常见错误:预聚合表用了
{ day: 1 }或{ user_id: 1 }单字段分片,导致每次查询都打满全部分片 - 验证方式:执行
sh.status()对比两个集合的 chunk 分布,确保 key pattern 和 balancer 状态一致 - 补救成本高:分片键一旦建错,只能导出重建,无法在线修改
聚合字段必须能被分片键前缀覆盖
比如原分片键是 { region: 1, user_id: 1 },那预聚合的 $group 阶段的 _id 至少要包含 region 字段,例如 { region: "$region", day: { $dateToString: { format: "%Y-%m-%d", date: "$created_at" } } }。这样每个 shard 才能独立完成子聚合,mongos 只需归并少量结果。
- 危险模式:按
user_id或order_id分组 → 每个 shard 产出数万文档,网络传输和 mongos 归并全崩 - 降维技巧:先按
{ region: 1, hour: 1 }聚,再用定时任务上卷到日粒度,避免单次聚合膨胀 - 索引配套:在预聚合表上建
{ region: 1, day: 1 }索引,支撑高频$match过滤
应用层必须自动识别并路由到预聚合表
硬编码判断时间范围或写两套查询逻辑,迟早出错。可靠做法是封装一个代理函数,在 aggregate() 入口统一拦截。
- 触发条件要严格:只对整日/整小时范围(如
$gte+$lt精确覆盖)且含分片键字段(如region)才跳转 - fallback 必须存在:缺
region或时间跨度过大(如查 3 年数据),就退回到原表 +allowDiskUse: true - 禁用
$facet双路查询:它会让两条路径都执行,白白消耗资源,不如一次判断清楚
预聚合数据一致性不能靠定时全量刷
每天凌晨跑一遍 mapReduce 或全量 aggregate,既占资源又滞后。真正可用的方案是写时标记 + 异步补漏 + TTL 索引组合。
- 写入时打标:在原始文档加
agg_status: "pending",聚合任务处理完置为"done" - 监听变更:用 change stream 捕获
agg_status: "pending"的文档,触发增量聚合 - TTL 保底:预聚合表加
{ expireAt: 1 }索引,过期自动清理,防止脏数据堆积
最难的不是建表或写脚本,而是让预聚合的粒度、分片键、路由逻辑、一致性机制四者咬合严丝合缝——漏掉任意一环,延迟问题只会从数据库转移到应用层,甚至更难排查。











