直接全量聚合易因内存超限(默认100MB)或超时失败,需用ObjectId时间戳特性分批:先查min/max _id,按范围$match前置切片,每批单独聚合,$lt确保无重复,起始ID须取自真实数据。

为什么不能直接对大型集合用 aggregate() 全量跑
直接对千万级文档的集合执行 db.collection.aggregate([]) 容易触发内存限制(100MB 默认)或超时,尤其当管道中含 $group、$sort 等消耗型阶段时。MongoDB 不会自动分页或流式处理聚合结果 —— 它默认把中间结果全加载进内存再输出。
用 $match 配合 ObjectId 范围实现分批的核心逻辑
利用 ObjectId 的时间戳特性(前 4 字节是秒级时间戳),它天然有序且可比较。只要集合按插入顺序写入(默认行为),_id 就是近似单调递增的,可当作“隐式自增主键”来切片。
- 先查出最小和最大
_id:db.collection.findOne({}, { sort: { _id: 1 } })和db.collection.findOne({}, { sort: { _id: -1 } }) - 手动划分区间,例如每次取 10 万条:
{ _id: { $gte: ObjectId("..."), $lt: ObjectId("...") } } - 每个批次单独加
$match阶段,放在管道最前面 —— 这样能走索引,避免全表扫描
实际分批代码怎么写(Shell / Node.js 均适用)
以 Shell 为例,手动分三批处理:
db.collection.aggregate([
{ $match: { _id: { $gte: ObjectId("600000000000000000000001"), $lt: ObjectId("6000000000000000000186a0") } } },
{ $group: { _id: "$category", total: { $sum: 1 } } }
])
注意两点:
-
$lt必须用,不能用$lte—— 否则最后一批可能重复包含边界文档 - 起始
ObjectId不要硬写"000000000000000000000001",而应从真实数据中取:db.collection.findOne({},{_id:1, sort:{_id:1}}) - 如果集合有删除/重建历史,
_id可能不严格连续,但只要没大量乱序插入,分片误差可控(实测偏差通常
容易被忽略的坑:索引、时区与 ObjectId 构造
$match 对 _id 范围查询只有在 _id 索引存在时才真正高效 —— 幸运的是,MongoDB 默认为 _id 建唯一索引,无需额外操作。
- 别在应用层用字符串拼接生成
ObjectId,比如"507f1f77bcf86cd799439011"直接塞进查询 —— 必须用驱动提供的构造函数(如 Node.js 的new ObjectId("...")),否则类型不匹配,走不了索引 - Shell 中用
ObjectId("...")是安全的;但在 Python PyMongo 或 Java Driver 中,必须显式 import 并调用对应类 -
ObjectId时间部分基于 UTC,如果你按本地时间估算范围,结果会偏移 —— 所有时间换算务必转 UTC











