MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Aggregationsfilter nach $lookup

Die Frage hier bezieht sich eigentlich auf etwas anderes und benötigt kein $lookup überhaupt. Aber für alle, die nur wegen des Titels "Filtern nach $lookup" hierher kommen, dann sind dies die Techniken für Sie:

MongoDB 3.6 – Unterpipeline

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

Früher - $lookup + $unwind + $match-Koaleszenz

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" }
     }}
])

Wenn Sie sich fragen, warum würden Sie $unwind im Gegensatz zur Verwendung von $filter auf dem Array, dann lesen Sie Aggregate $lookup Die Gesamtgröße der Dokumente in der übereinstimmenden Pipeline überschreitet die maximale Dokumentgröße für alle Details, warum dies im Allgemeinen notwendig und weitaus optimaler ist.

Für Versionen von MongoDB 3.6 und höher ist die aussagekräftigere „Sub-Pipeline“ im Allgemeinen das, was Sie möchten, um die Ergebnisse der Fremdsammlung zu „filtern“, bevor überhaupt etwas in das Array zurückgegeben wird.

Aber zurück zur Antwort, die tatsächlich beschreibt, warum die gestellte Frage überhaupt "no join" braucht ....

Original

Verwenden von $lookup wie dies nicht der "effizienteste" Weg ist, um das zu tun, was Sie hier wollen. Aber dazu später mehr.

Verwenden Sie als Grundkonzept einfach $filter auf dem resultierenden Array:

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" ] }
           }
        }
    }}
]);

Oder verwenden Sie $redact stattdessen:

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"
        }
    }}
]);

Beide erhalten das gleiche Ergebnis:

{  
  "_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 ]
    }
  ]
}

Unterm Strich ist das $lookup selbst kann "noch" nicht abfragen, um nur bestimmte Daten auszuwählen. Daher muss die gesamte "Filterung" nach dem $lookup erfolgen

Aber für diese Art von "Self Join" ist es besser, $lookup nicht zu verwenden überhaupt und den Overhead eines zusätzlichen Lesens und "Hash-Merge" vollständig zu vermeiden. Rufen Sie einfach die zugehörigen Elemente und $group ab stattdessen:

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 ] }
      }
    }
  }}
])

Was nur etwas anders herauskommt, weil ich bewusst die Fremdfelder entfernt habe. Fügen Sie sie selbst hinzu, wenn Sie wirklich möchten:

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

Das einzige wirkliche Problem hier ist also das "Filtern" von null Ergebnis aus dem Array, das erstellt wurde, als das aktuelle Dokument das parent war bei der Verarbeitung von Elementen zu $push .

Was Sie hier auch zu vermissen scheinen, ist, dass das gesuchte Ergebnis überhaupt keine Aggregation oder "Unterabfragen" benötigt. Die Struktur, die Sie festgestellt oder möglicherweise an anderer Stelle gefunden haben, ist so "gestaltet", dass Sie einen "Knoten" und alle seine "Kinder" in einer einzigen Abfrageanforderung erhalten können.

Das heißt, nur die "Abfrage" ist alles, was wirklich benötigt wird, und die Datensammlung (was alles ist, was passiert, da kein Inhalt wirklich "reduziert" wird) ist nur eine Funktion der Iteration des Cursor-Ergebnisses:

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);

Dies macht genau dasselbe:

{
  "_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
      ]
    }
  ]
}

Und dient als Beweis dafür, dass Sie hier wirklich nur die "einzige" Abfrage ausgeben müssen, um sowohl die Eltern als auch die Kinder auszuwählen. Die zurückgegebenen Daten sind genau die gleichen, und alles, was Sie auf dem Server oder dem Client tun, ist, sie in ein anderes gesammeltes Format zu „massieren“.

Dies ist einer dieser Fälle, in denen Sie daran denken, wie Sie Dinge in einer "relationalen" Datenbank gemacht haben, und nicht erkennen, dass Sie sie nicht mehr verwenden müssen, da sich die Art und Weise, wie die Daten gespeichert werden, "geändert" hat der gleiche Ansatz.

Genau das ist der Sinn des Dokumentationsbeispiels "Baumstrukturen mit untergeordneten Referenzen modellieren" in seiner Struktur, wo es einfach ist, Eltern und Kinder innerhalb einer Abfrage auszuwählen.