$project是结构重塑的起点,用于重定义字段、计算新字段、嵌套或删除字段;需显式声明保留字段,避免_id冗余或语法错误。
$project 阶段是转换数据模型最直接、最常用的方式,不是“可选”,而是绝大多数结构重塑场景的起点。
$project 重定义字段结构$project 不只是“选字段”,它能计算新字段、重命名、嵌套、删字段,本质是输出文档的“模具”。 常见错误是把它当成 SELECT 简单投影,结果漏掉 _id: 0 导致冗余字段干扰后续阶段,或误用 1/0 混合写法导致语法报错。 - 必须显式声明所有要保留的字段,未声明的默认丢弃(除非用 _id: 1 显式保留) - 计算字段直接写表达式,比如 full_name: { $concat: ["$first_name", " ", "$last_name"] } - 嵌套结构支持任意深度:profile: { age: "$age", city: "$address.city" } - 删除字段就设为 0,但不能和表达式混用在同一字段名下(MongoDB 会报错)orders.items),想展开成多条记录再聚合,$unwind 是绕不开的。 但它极易引发“爆炸式膨胀”——一个含 5 个 item 的订单,$unwind 后变成 5 条,若没及时 $match 过滤,性能会断崖下跌。 - 优先在 $unwind 前加 $match 缩小输入集,比如先筛出 status: "completed" 的订单 - 若数组可能为空或缺失,加 preserveNullAndEmptyArrays: true 参数,否则该文档会被丢弃 - 展开后常需 $group 回收,比如按 order_no 聚合计量,此时注意用 $first 提取非数组字段(如用户信息)$lookup 报 “no match” 很大概率不是逻辑错,而是 ID 类型不一致:一边是 ObjectId,另一边是字符串 "123"。 硬编码转换(比如在应用层拼字符串)不可靠,必须在管道里用类型转换操作符。 - 用 $toString 把 ObjectId 转字符串去匹配字符串 ID 字段 - 反过来,用 $toObjectId 把字符串转 ObjectId(前提是字符串格式合法) - 如果目标字段可能为空或格式混乱,先用 $cond 判断再转换,避免整个 stage 失败 - $lookup 的 pipeline 选项里也能嵌套这些转换,别只盯着外层$out 和 $merge 都能落地结果,但行为完全不同,选错会导致数据被清空或冲突覆盖。 - $out 是“全量替换”:目标集合存在则清空重写,不存在则新建;适合定时跑批生成报表表 - $merge 是“增量更新”:可指定匹配键(如 on: "_id"),已存在就 replace 或 keepExisting,适合持续同步场景 - 本地调试时建议先用 .toArray() 或 Compass 预览,确认逻辑无误再加 $out,否则误操作没法撤回 真正难的不是写对单个 stage,而是 stage 之间的数据形态衔接——上一阶段输出的字段名、类型、是否可能为 null,直接决定下一阶段能不能安全引用。这点在嵌套 $lookup 或多次 $unwind 时尤其容易翻车。