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

Aggregat $lookup Die Gesamtgröße der Dokumente in der übereinstimmenden Pipeline überschreitet die maximale Dokumentgröße

Wie bereits im Kommentar erwähnt, tritt der Fehler auf, weil beim Ausführen von $lookup die standardmäßig ein Ziel-"Array" innerhalb des übergeordneten Dokuments aus den Ergebnissen der Fremdsammlung erzeugt, führt die Gesamtgröße der für dieses Array ausgewählten Dokumente dazu, dass das übergeordnete Dokument die 16-MB-BSON-Grenze überschreitet.

Der Zähler dafür ist mit einem $unwind zu verarbeiten die unmittelbar auf $lookup folgt Pipeline-Phase. Dies ändert tatsächlich das Verhalten von $lookup in der Weise, dass anstatt ein Array im Elternteil zu erzeugen, die Ergebnisse stattdessen eine "Kopie" jedes Elternteils für jedes übereinstimmende Dokument sind.

So ziemlich wie die normale Verwendung von $unwind , mit der Ausnahme, dass das unwinding statt als "separate" Pipelinestufe verarbeitet wird Aktion wird tatsächlich zu $lookup hinzugefügt Pipeline-Betrieb selbst. Idealerweise folgen Sie auch dem $unwind mit einem $match Bedingung, die auch einen matching erzeugt Argument, das auch zu $lookup hinzugefügt werden soll . Sie können dies tatsächlich im explain sehen Ausgabe für die Pipeline.

Das Thema wird tatsächlich (kurz) in einem Abschnitt von Aggregation Pipeline Optimization in der Kerndokumentation behandelt:

$lookup + $unwind Koaleszenz

Neu in Version 3.2.

Wenn ein $unwind unmittelbar auf ein anderes $lookup folgt und das $unwind auf dem as-Feld des $lookup operiert, kann der Optimierer das $unwind in die $lookup-Stufe vereinigen. Dadurch wird die Erstellung großer Zwischendokumente vermieden.

Am besten demonstriert mit einem Listing, das den Server belastet, indem "verwandte" Dokumente erstellt werden, die die 16-MB-BSON-Grenze überschreiten würden. So kurz wie möglich ausgeführt, um das BSON-Limit zu brechen und zu umgehen:

const MongoClient = require('mongodb').MongoClient;

const uri = 'mongodb://localhost/test';

function data(data) {
  console.log(JSON.stringify(data, undefined, 2))
}

(async function() {

  let db;

  try {
    db = await MongoClient.connect(uri);

    console.log('Cleaning....');
    // Clean data
    await Promise.all(
      ["source","edge"].map(c => db.collection(c).remove() )
    );

    console.log('Inserting...')

    await db.collection('edge').insertMany(
      Array(1000).fill(1).map((e,i) => ({ _id: i+1, gid: 1 }))
    );
    await db.collection('source').insert({ _id: 1 })

    console.log('Fattening up....');
    await db.collection('edge').updateMany(
      {},
      { $set: { data: "x".repeat(100000) } }
    );

    // The full pipeline. Failing test uses only the $lookup stage
    let pipeline = [
      { $lookup: {
        from: 'edge',
        localField: '_id',
        foreignField: 'gid',
        as: 'results'
      }},
      { $unwind: '$results' },
      { $match: { 'results._id': { $gte: 1, $lte: 5 } } },
      { $project: { 'results.data': 0 } },
      { $group: { _id: '$_id', results: { $push: '$results' } } }
    ];

    // List and iterate each test case
    let tests = [
      'Failing.. Size exceeded...',
      'Working.. Applied $unwind...',
      'Explain output...'
    ];

    for (let [idx, test] of Object.entries(tests)) {
      console.log(test);

      try {
        let currpipe = (( +idx === 0 ) ? pipeline.slice(0,1) : pipeline),
            options = (( +idx === tests.length-1 ) ? { explain: true } : {});

        await new Promise((end,error) => {
          let cursor = db.collection('source').aggregate(currpipe,options);
          for ( let [key, value] of Object.entries({ error, end, data }) )
            cursor.on(key,value);
        });
      } catch(e) {
        console.error(e);
      }

    }

  } catch(e) {
    console.error(e);
  } finally {
    db.close();
  }

})();

Nach dem Einfügen einiger anfänglicher Daten versucht das Listing, ein Aggregat auszuführen, das lediglich aus $lookup besteht was mit folgendem Fehler fehlschlagen wird:

{ MongoError:Gesamtgröße der Dokumente in der Edge-Matching-Pipeline { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } überschreitet die maximale Dokumentgröße

Was Ihnen im Grunde sagt, dass das BSON-Limit beim Abrufen überschritten wurde.

Im Gegensatz dazu fügt der nächste Versuch den $unwind hinzu und $match Pipeline-Stufen

Die Explain-Ausgabe :

  {
    "$lookup": {
      "from": "edge",
      "as": "results",
      "localField": "_id",
      "foreignField": "gid",
      "unwinding": {                        // $unwind now is unwinding
        "preserveNullAndEmptyArrays": false
      },
      "matching": {                         // $match now is matching
        "$and": [                           // and actually executed against 
          {                                 // the foreign collection
            "_id": {
              "$gte": 1
            }
          },
          {
            "_id": {
              "$lte": 5
            }
          }
        ]
      }
    }
  },
  // $unwind and $match stages removed
  {
    "$project": {
      "results": {
        "data": false
      }
    }
  },
  {
    "$group": {
      "_id": "$_id",
      "results": {
        "$push": "$results"
      }
    }
  }

Und dieses Ergebnis gelingt natürlich, denn da die Ergebnisse nicht mehr in das übergeordnete Dokument eingefügt werden, kann die BSON-Grenze nicht überschritten werden.

Dies geschieht wirklich nur als Ergebnis des Hinzufügens von $unwind nur, sondern das $match wird zum Beispiel hinzugefügt, um zu zeigen, dass dies auch ist in $lookup hinzugefügt Phase und dass der Gesamteffekt darin besteht, die zurückgegebenen Ergebnisse effektiv zu "begrenzen", da dies alles in diesem $lookup erledigt wird Operation und es werden keine anderen Ergebnisse als die übereinstimmenden tatsächlich zurückgegeben.

Indem Sie auf diese Weise konstruieren, können Sie nach "referenzierten Daten" fragen, die das BSON-Limit überschreiten würden, und dann, wenn Sie $group möchten die Ergebnisse zurück in ein Array-Format, sobald sie effektiv durch die "versteckte Abfrage" gefiltert wurden, die tatsächlich von $lookup durchgeführt wird .

MongoDB 3.6 und höher – Zusätzlich für „LEFT JOIN“

Wie der gesamte obige Inhalt anmerkt, ist das BSON-Limit ein "hartes" Limit Grenze, die Sie nicht überschreiten können, und das ist im Allgemeinen der Grund für $unwind ist als Zwischenschritt notwendig. Es gibt jedoch die Einschränkung, dass der "LEFT JOIN" aufgrund des $unwind zu einem "INNER JOIN" wird wo es den Inhalt nicht bewahren kann. Auch preserveNulAndEmptyArrays würde die "Koaleszenz" negieren und dennoch das intakte Array belassen, was das gleiche BSON-Limit-Problem verursacht.

MongoDB 3.6 fügt $lookup eine neue Syntax hinzu Dies ermöglicht die Verwendung eines "Sub-Pipeline"-Ausdrucks anstelle der "lokalen" und "fremden" Schlüssel. Anstatt also die Option "Koaleszenz" wie gezeigt zu verwenden, ist es möglich, solange das erzeugte Array nicht auch die Grenze überschreitet, Bedingungen in diese Pipeline einzufügen, die das Array "intakt" und möglicherweise ohne Übereinstimmungen zurückgibt, wie dies ein Hinweis wäre eines "LEFT JOIN".

Der neue Ausdruck wäre dann:

{ "$lookup": {
  "from": "edge",
  "let": { "gid": "$gid" },
  "pipeline": [
    { "$match": {
      "_id": { "$gte": 1, "$lte": 5 },
      "$expr": { "$eq": [ "$$gid", "$to" ] }
    }}          
  ],
  "as": "from"
}}

Tatsächlich wäre dies im Grunde das, was MongoDB unter der Decke tut mit der vorherigen Syntax seit 3.6 verwendet $expr "intern", um die Aussage zu konstruieren. Der Unterschied besteht natürlich darin, dass es kein "unwinding" gibt Option vorhanden, wie der $lookup tatsächlich hingerichtet wird.

Wenn als Ergebnis der "pipeline" tatsächlich keine Dokumente erstellt werden Ausdruck, dann ist das Zielarray innerhalb des Masterdokuments tatsächlich leer, genau wie ein "LEFT JOIN" es tatsächlich tut und das normale Verhalten von $lookup wäre ohne weitere Optionen.

Das Ausgabe-Array darf jedoch NICHT dazu führen, dass das Dokument, in dem es erstellt wird, das BSON-Limit überschreitet . Es liegt also wirklich an Ihnen, dafür zu sorgen, dass alle "übereinstimmenden" Inhalte durch die Bedingungen unter dieser Grenze bleiben, oder derselbe Fehler bleibt bestehen, es sei denn, Sie verwenden tatsächlich $unwind um den "INNER JOIN" zu bewirken.