$lookup + $match 筛空数组是最常用可靠方式,需确保字段类型一致、from集合foreignField有索引、避免千万级无索引关联,并通过两次互换方向的$lookup获取完整双向差集。

直接用 $lookup + $match 筛空数组是最常用、最可靠的方式,但必须注意字段类型一致、索引缺失和大集合性能这三点,否则查不出结果或查到一半就超时。
为什么 $lookup 后 matched: { $eq: [] } 能筛出差异文档
因为 $lookup 总是返回一个数组(哪怕没匹配到),匹配失败时就是空数组 []。所以判断 matched 字段是否等于 [],本质是在确认“该文档在目标集合中找不到任何对应项”。这不是近似判断,而是精确的集合存在性检查。
常见错误现象:
- 写成
matched: { $size: 0 }—— 没问题,但$eq: []更语义清晰且兼容性略好 - 写成
matched: null或matched: { $exists: false }—— 完全无效,$lookup一定会生成该字段 - 漏掉
$project去掉matched字段 —— 返回结果里带冗余数组,影响后续处理或传输体积
字段不一致会导致 $lookup 全部失配
如果两个集合用于关联的字段类型不同(比如一个存的是 NumberLong(123),另一个是字符串 "123"),$lookup 就不会命中任何文档,最终所有结果都被误判为“差异项”。
实操建议:
- 先用
db.collectionA.distinct("keyField", {}, { hint: "_id_" })和db.collectionB.distinct("keyField", {}, { hint: "_id_" })抽样检查前几条值的类型 - 必要时在
$lookup前加$addFields统一转类型,例如:{ $toString: "$keyField" }或{ $toInt: "$keyField" } - 避免用
_id直接跨集合比对:如果集合 B 的_id是 ObjectId,而集合 A 的关联字段是字符串 ID,必须显式转换
千万级集合下 $lookup 明显变慢甚至中断
当集合 A 或 B 超过 500 万文档,且关联字段未建索引时,$lookup 会触发全表扫描,聚合可能卡住、内存溢出或被 maxTimeMS 中断。
必须做的优化:
- 确保
from集合(即被查的一方)的foreignField上有**支持查询的索引**,例如:db.collectionB.createIndex({ keyField: 1 }) - 给主集合(
collectionA)的localField加索引,加速 pipeline 输入阶段 - 加
maxTimeMS防死锁:.aggregate(pipeline, { maxTimeMS: 300000 })(5 分钟) - 拆分执行:按时间范围或分片键(如
tenantId)分批聚合,再合并结果
只查“单向差集”不够,双向对比才完整
一个 $lookup 只能查出“A 有 B 没有”,要得到完整差异(A 特有 + B 特有),必须运行两次聚合:
- 第一次:用
collectionA$lookupcollectionB→ 得 A 特有文档 - 第二次:用
collectionB$lookupcollectionA→ 得 B 特有文档
不能靠一次聚合 + $setDifference 替代——因为 $setDifference 只能操作数组字段,无法处理整个文档结构的差异;它适合比字段里的数组值,不适合比集合间文档存在性。
容易被忽略的点:两次聚合的 localField/foreignField 必须严格互换,字段名拼错一个字母就会返回空结果,且无报错提示。











