根本原因是索引超出数组长度,MongoDB不兜底而直接报错;需在$arrayElemAt前用$cond或$min校验索引有效性,并在更新时绑定状态与范围校验,分页和冷热数据也须类似防护。

聚合中用$arrayElemAt取数组元素时崩了
根本原因是索引超出了数组长度,比如$arrayElemAt传入5但approvers只有3个ID。MongoDB不会自动截断或兜底,而是直接报错Array index out of bounds。
常见于审批流里读取“下一个待审人”:用$currentApproverIndex去取$approvers,但没校验索引是否有效。
- 必须在
$arrayElemAt前加$cond或$let做越界保护,例如:{ $arrayElemAt: [ "$approvers", { $min: ["$currentApproverIndex", { $subtract: [{ $size: "$approvers" }, 1] }] } ] } - 更稳妥的做法是先用
$size判断:{ $cond: { if: { $lt: ["$currentApproverIndex", { $size: "$approvers" }] }, then: { $arrayElemAt: ["$approvers", "$currentApproverIndex"] }, else: null } } - 别依赖前端或业务层传来的
currentApproverIndex值——它可能被篡改或未初始化,聚合阶段必须自校验
updateOne里用$inc更新index导致负数或超长
$inc: { currentApproverIndex: 1 }看似简单,但没人拦着它一路加到100、1000,最终在聚合里触发越界。这不是bug,是设计遗漏。
关键点在于:状态流转和索引更新必须绑定,不能只靠原子操作。
- 更新时必须同时校验
currentApproverIndex范围,例如:db.collection.updateOne( { _id: ..., status: "pending", currentApproverIndex: { $lt: { $size: "$approvers" } } }, { $inc: { currentApproverIndex: 1 } } ) - 如果要跳过某人(比如审批人离职),别用
$inc,改用$set直接写新索引值,再确保该值$gte 0且$lt { $size: "$approvers" } - 索引字段本身建议加
min: 0校验(应用层),MongoDB schema validation不支持动态$size约束,所以只能靠代码拦截
分页查询里用$skip/$limit却忘了total边界
这和Java里Math.min()防越界是一个逻辑:聚合管道里的$skip和$limit参数如果来自用户输入,没做截断,就会让MongoDB尝试跳过10000条——而集合总共才200条,结果返回空或报错。
尤其在审批历史列表、事件日志这类分页场景高频出现。
- 计算
skip值时,用{ $min: [{ $multiply: [{ $subtract: ["$pageNum", 1] }, "$pageSize"] }, { $subtract: [{ $size: "$items" }, 1] }] }兜底 - 更推荐把分页逻辑移到应用层:先
countDocuments拿到总数,再用Math.min()算安全的skip/limit,最后发给MongoDB - 避免在聚合里嵌套多层
$size计算——WiredTiger对大数组的$size有性能损耗,特别是approval_history这种可能很长的子数组
冷热分离迁移后聚合查不到冷数据
不是越界,但表现类似:你写了聚合管道查events,却发现最近30天的数据没了——因为冷数据已迁出MongoDB,进了S3或Parquet文件,而路由层没切过去。
这本质是数据位置和查询路径脱节,比数组越界更隐蔽。
- 所有涉及
event的聚合必须前置判断createdAt时间戳,决定走MongoDB热库还是调用冷存储API - 不要在聚合里硬写
{ createdAt: { $gt: { $dateSubtract: [new Date(), { unit: "day", amount: 30 }] } } }——这个条件无法命中冷数据,也不能触发跨源联合查询 - 冷热路由必须在服务入口统一处理,聚合管道只负责“单源内运算”,否则
$lookup跨集群或$unionWith跨存储会极大拖慢响应
真正容易被忽略的是:数组越界错误往往只在特定路径下暴露,比如审批走到最后一人、分页翻到最后一页、冷数据刚迁移完那几分钟——它不常驻,但每次出现都意味着状态机某处漏了守门人。











