$lookup 后的聚合过滤器

IT技术 javascript mongodb mongodb-query aggregation-framework
2021-01-30 14:45:52

如何在 $lookup 之后添加过滤器或者是否有任何其他方法可以做到这一点?

我的数据收集测试是:

{ "_id" : ObjectId("570557d4094a4514fc1291d6"), "id" : 100, "value" : "0", "contain" : [ ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d7"), "id" : 110, "value" : "1", "contain" : [ 100 ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d8"), "id" : 120, "value" : "1", "contain" : [ 100 ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d9"), "id" : 121, "value" : "2", "contain" : [ 100, 120 ] }

我选择 id 100 并聚合孩子:

db.test.aggregate([ {
  $match : {
    id: 100
  }
}, {
  $lookup : {
    from : "test",
    localField : "id",
    foreignField : "contain",
    as : "childs"
  }
}]);

我回来了:

{  
  "_id":ObjectId("570557d4094a4514fc1291d6"),
  "id":100,
  "value":"0",
  "contain":[ ],
  "childs":[ {  
      "_id":ObjectId("570557d4094a4514fc1291d7"),
      "id":110,
      "value":"1",
      "contain":[ 100 ]
    },
    {  
      "_id":ObjectId("570557d4094a4514fc1291d8"),
      "id":120,
      "value":"1",
      "contain":[ 100 ]
    },
    {  
      "_id":ObjectId("570557d4094a4514fc1291d9"),
      "id":121,
      "value":"2",
      "contain":[ 100, 120 ]
    }
  ]
}

但我只想要与“值:1”匹配的孩子

最后我期待这个结果:

{  
  "_id":ObjectId("570557d4094a4514fc1291d6"),
  "id":100,
  "value":"0",
  "contain":[ ],
  "childs":[ {  
      "_id":ObjectId("570557d4094a4514fc1291d7"),
      "id":110,
      "value":"1",
      "contain":[ 100 ]
    },
    {  
      "_id":ObjectId("570557d4094a4514fc1291d8"),
      "id":120,
      "value":"1",
      "contain":[ 100 ]
    }
  ]
}
1个回答

这里的问题实际上是关于不同的东西,根本不需要$lookup但是对于任何纯粹从“$lookup 后过滤”的标题来到这里的人来说,这些技术适合您:

MongoDB 3.6 - 子管道

db.test.aggregate([
    { "$match": { "id": 100 } },
    { "$lookup": {
      "from": "test",
      "let": { "id": "$id" },
      "pipeline": [
        { "$match": {
          "value": "1",
          "$expr": { "$in": [ "$$id", "$contain" ] }
        }}
      ],
      "as": "childs"
    }}
])

早些时候 - $lookup + $unwind + $match 合并

db.test.aggregate([
    { "$match": { "id": 100 } },
    { "$lookup": {
        "from": "test",
        "localField": "id",
        "foreignField": "contain",
        "as": "childs"
    }},
    { "$unwind": "$childs" },
    { "$match": { "childs.value": "1" } },
    { "$group": {
        "_id": "$_id",
        "id": { "$first": "$id" },
        "value": { "$first": "$value" },
        "contain": { "$first": "$contain" },
        "childs": { "$push": "$childs" }
     }}
])

如果您质疑为什么要在数组$unwind上使用而不是$filter在数组上使用,请阅读Aggregate $lookup 匹配管道中文档的总大小超过最大文档大小的所有详细信息,了解为什么这通常是必要的并且更加优化。

对于 MongoDB 3.6 及以后的版本,更具表现力的“子管道”通常是您想要在任何东西返回到数组之前“过滤”外部集合的结果。

回到答案,尽管它实际上描述了为什么所问的问题根本不需要“加入”......


原来的

$lookup像这样使用并不是在这里做你想做的最“有效”的方式。但稍后会详细介绍。

作为一个基本概念,只需$filter在结果数组上使用:

db.test.aggregate([ 
    { "$match": { "id": 100 } }, 
    { "$lookup": {
        "from": "test",
        "localField": "id",
        "foreignField": "contain",
        "as": "childs"
    }},
    { "$project": {
        "id": 1,
        "value": 1,
        "contain": 1,
        "childs": {
           "$filter": {
               "input": "$childs",
               "as": "child",
               "cond": { "$eq": [ "$$child.value", "1" ] }
           }
        }
    }}
]);

或者$redact改用:

db.test.aggregate([ 
    { "$match": { "id": 100 } }, 
    { "$lookup": {
        "from": "test",
        "localField": "id",
        "foreignField": "contain",
        "as": "childs"
    }},
    { "$redact": {
        "$cond": {
           "if": {
              "$or": [
                { "$eq": [ "$value", "0" ] },
                { "$eq": [ "$value", "1" ] }
              ]
           },
           "then": "$$DESCEND",
           "else": "$$PRUNE"
        }
    }}
]);

两者都得到相同的结果:

{  
  "_id":ObjectId("570557d4094a4514fc1291d6"),
  "id":100,
  "value":"0",
  "contain":[ ],
  "childs":[ {  
      "_id":ObjectId("570557d4094a4514fc1291d7"),
      "id":110,
      "value":"1",
      "contain":[ 100 ]
    },
    {  
      "_id":ObjectId("570557d4094a4514fc1291d8"),
      "id":120,
      "value":"1",
      "contain":[ 100 ]
    }
  ]
}

最重要的是,$lookup它本身不能“还”查询以仅选择某些数据。所以所有的“过滤”都需要在$lookup

但实际上对于这种类型的“自连接”,您最好根本不使用$lookup并完全避免额外读取和“哈希合并”的开销。只需获取相关项目,$group而是:

db.test.aggregate([
  { "$match": { 
    "$or": [
      { "id": 100 },
      { "contain.0": 100, "value": "1" }
    ]
  }},
  { "$group": {
    "_id": {
      "$cond": {
        "if": { "$eq": [ "$value", "0" ] },
        "then": "$id",
        "else": { "$arrayElemAt": [ "$contain", 0 ] }
      }
    },
    "value": { "$first": { "$literal": "0"} },
    "childs": {
      "$push": {
        "$cond": {
          "if": { "$ne": [ "$value", "0" ] },
          "then": "$$ROOT",
          "else": null
        }
      }
    }
  }},
  { "$project": {
    "value": 1,
    "childs": {
      "$filter": {
        "input": "$childs",
        "as": "child",
        "cond": { "$ne": [ "$$child", null ] }
      }
    }
  }}
])

这只是有点不同,因为我故意删除了无关的字段。如果您真的想将它们添加到自己中:

{
  "_id" : 100,
  "value" : "0",
  "childs" : [
    {
      "_id" : ObjectId("570557d4094a4514fc1291d7"),
      "id" : 110,
      "value" : "1",
      "contain" : [ 100 ]
    },
    {
      "_id" : ObjectId("570557d4094a4514fc1291d8"),
      "id" : 120,
      "value" : "1",
      "contain" : [ 100 ]
    }
  ]
}

所以这里唯一真正的问题是“过滤”null数组中的任何结果,当当前文档是parent处理项目时创建的$push


您在这里似乎还缺少的是,您正在寻找的结果根本不需要聚合或“子查询”。您已经得出结论或可能在其他地方找到的结构是“设计好的”,以便您可以在单个查询请求中获得一个“节点”及其所有“子节点”。

这意味着只有“查询”才是真正需要的,而数据收集(这是所有正在发生的事情,因为没有真正“减少”内容)只是迭代游标结果的函数:

var result = {};

db.test.find({
  "$or": [
    { "id": 100 },
    { "contain.0": 100, "value": "1" }
  ]
}).sort({ "contain.0": 1 }).forEach(function(doc) {
  if ( doc.id == 100 ) {
    result = doc;
    result.childs = []
  } else {
    result.childs.push(doc)
  }
})

printjson(result);

这完全相同:

{
  "_id" : ObjectId("570557d4094a4514fc1291d6"),
  "id" : 100,
  "value" : "0",
  "contain" : [ ],
  "childs" : [
    {
      "_id" : ObjectId("570557d4094a4514fc1291d7"),
      "id" : 110,
      "value" : "1",
      "contain" : [
              100
      ]
    },
    {
      "_id" : ObjectId("570557d4094a4514fc1291d8"),
      "id" : 120,
      "value" : "1",
      "contain" : [
              100
      ]
    }
  ]
}

并证明您在这里真正需要做的就是发出“单一”查询以选择父项和子项。返回的数据是一样的,你在服务器或客户端上所做的一切都是“按摩”成另一种收集的格式。

这是其中一种情况,您可能会“陷入”思考如何在“关系”数据库中做事,而没有意识到由于数据的存储方式已经“改变”,您不再需要使用同样的方法。

这正是文档示例“带有子引用的模型树结构”在其结构中的意义所在,它可以轻松地在一个查询中选择父项和子项。

很好的例子,用于学习 $lookup + $unwind + $match + $filter 并有适当的解释。我已经用子参考更新了模型树结构的参考。
2021-03-24 14:45:52
谢谢,但是当我们查看性能时。那么最好做 2 个查询,一个是为了获取文档,另一个是为孩子们?
2021-04-03 14:45:52
@PhillipBartschinski 我认为您仍然停留在“关系思维”中,这让您在实际上不需要“子查询”或“自连接”时思考。在这里添加了纯“无聚合”方法来演示这一点。
2021-04-04 14:45:52
@PhillipBartschinski 在“性能”方面,$lookup有效地进行了“两个”查询,但在“服务器”上。“客户端”可以执行“两个”查询,但当然涉及网络和响应开销会减慢进程。因此你为什么这样做$lookup但我也说你的情况不需要这个。所有项目都已经在同一个集合中,因此只需选择它们并$group相应地选择它们就“性能”而言,这是三者中更好的选择。
2021-04-12 14:45:52
是的,我是面向文档的主题的新手,看看有什么区别。我稍后会尝试,看看如何构建我的复杂结构。但现在谢谢你
2021-04-12 14:45:52