mongodb - unwind - pymongo aggregate



Aggregate $ lookup Die Gesamtgröße der Dokumente in der übereinstimmenden Pipeline überschreitet die maximale Dokumentgröße (1)

Wie bereits im Kommentar erwähnt, tritt der Fehler auf, weil bei der Ausführung von $lookup , bei der aus den Ergebnissen der fremden Sammlung standardmäßig ein Ziel- "Array" im übergeordneten Dokument erstellt wird, die Gesamtgröße der für dieses Array ausgewählten Dokumente das übergeordnete Element überschreitet das 16MB BSON Limit.

Der Zähler dafür soll mit einem $unwind der unmittelbar auf die $lookup Pipeline-Phase folgt. Dies ändert das Verhalten von $lookup , dass statt eines Arrays im übergeordneten Element die Ergebnisse eine "Kopie" jedes übergeordneten Elements für jedes abgeglichene Dokument sind.

Ähnlich wie bei der regulären Verwendung von $unwind , mit der Ausnahme, dass die Abwicklungsaktion nicht als "separate" Pipeline-Stufe verarbeitet wird, sondern der $lookup Pipeline-Operation selbst hinzugefügt wird. Im Idealfall folgen Sie dem $unwind mit einer $match Bedingung, wodurch auch ein matching Argument erstellt wird, das ebenfalls zum $lookup hinzugefügt wird. Sie können dies tatsächlich in der explain Ausgabe für die Pipeline sehen.

Das Thema wird (kurz) in einem Abschnitt zur Optimierung der Aggregationspipeline in der Kerndokumentation behandelt:

$ lookup + $ unwind Coalescence

Neu in Version 3.2.

Wenn ein $ unwind unmittelbar auf einen anderen $ lookup folgt und der $ unwind das Feld as des $ lookup bearbeitet, kann der Optimierer den $ unwind in die $ lookup-Phase zusammenführen. Dadurch wird vermieden, dass große Zwischendokumente erstellt werden.

Dies lässt sich am besten anhand einer Auflistung demonstrieren, die den Server belastet, indem "verwandte" Dokumente erstellt werden, die das 16-MB-BSON-Limit überschreiten. So kurz wie möglich gemacht, um das BSON-Limit zu unterbrechen 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 Anfangsdaten wird in der Auflistung versucht, ein Aggregat auszuführen, das lediglich aus $lookup diesem Versuch tritt der folgende Fehler auf:

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

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

Im Gegensatz dazu werden beim nächsten Versuch die Pipeline-Stufen $unwind und $match hinzugefügt

Die Erklärungsausgabe :

  {
    "$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 ist natürlich erfolgreich, denn da die Ergebnisse nicht mehr im übergeordneten Dokument abgelegt werden, kann das BSON-Limit nicht überschritten werden.

Dies geschieht wirklich nur als Ergebnis des Hinzufügens $unwind nur $unwind , aber das $match wird zum Beispiel hinzugefügt, um zu zeigen, dass dies auch in der $lookup Phase hinzugefügt wird und dass der Gesamteffekt darin besteht, die zurückgegebenen Ergebnisse auf effektive Weise zu "begrenzen" , da dies alles in dieser $lookup Operation erledigt ist und keine anderen Ergebnisse als die übereinstimmenden zurückgegeben werden.

Wenn Sie auf diese Weise konstruieren, können Sie nach "referenzierten Daten" suchen, die die BSON-Grenze überschreiten, und wenn Sie möchten $group die Ergebnisse wieder in ein Array-Format $group werden, nachdem sie effektiv durch die "versteckte Abfrage" gefiltert wurden, die tatsächlich vorliegt durchgeführt von $lookup .

MongoDB 3.6 und höher - zusätzlich für "LEFT JOIN"

Wie in allen obigen Inhalten erwähnt, ist das BSON-Limit ein "hartes" Limit, das Sie nicht überschreiten können, und dies ist im Allgemeinen der Grund, warum das $unwind als Zwischenschritt erforderlich ist. Es gibt jedoch die Einschränkung, dass "LEFT JOIN" aufgrund des Abwickelns von $unwind zu "INNER JOIN" wird, wenn der Inhalt nicht beibehalten werden kann. Außerdem würde sogar preserveNulAndEmptyArrays die "Koaleszenz" negieren und das intakte Array weiterhin belassen, was dasselbe BSON-Limit-Problem verursacht.

MongoDB 3.6 fügt $lookup eine neue Syntax hinzu, mit der ein "Sub-Pipeline" -Ausdruck anstelle der "lokalen" und "fremden" Schlüssel verwendet werden kann. Anstatt die "Koaleszenz" -Option zu verwenden, wie gezeigt, ist es möglich, Bedingungen in die Pipeline zu setzen, die das Array "intakt" zurückliefern, und möglicherweise ohne Übereinstimmungen, wie dies angezeigt 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 ist dies im Grunde das, was MongoDB mit der vorherigen Syntax "under the covers" macht, da 3.6 $expr "intern" verwendet, um die Anweisung zu $expr . Der Unterschied ist natürlich, dass es keine "unwinding" -Option gibt, wie der $lookup tatsächlich ausgeführt wird.

Wenn aufgrund des "pipeline" -Ausdrucks tatsächlich keine Dokumente erstellt werden, ist das Zielarray innerhalb des Masterdokuments in der Tat leer, genau wie ein "LEFT JOIN" dies tatsächlich tut und das normale Verhalten von $lookup ohne ein $lookup wäre andere Optionen.

Das Ausgabearray darf jedoch NICHT dazu führen, dass das Dokument, in dem es erstellt wird, das BSON-Limit überschreitet . Es liegt also wirklich an Ihnen, sicherzustellen, dass alle "übereinstimmenden" Inhalte unter diesen Bedingungen bleiben oder derselbe Fehler weiterhin auftritt, es sei denn, Sie verwenden tatsächlich $unwind , um den "INNER JOIN" zu bewirken.

Ich habe eine ziemlich einfache $lookup Aggregationsabfrage wie die folgende:

{'$lookup':
 {'from': 'edge',
  'localField': 'gid',
  'foreignField': 'to',
  'as': 'from'}}

Wenn ich dies bei einer Übereinstimmung mit genügend Dokumenten ausführe, wird die folgende Fehlermeldung angezeigt:

Command failed with error 4568: 'Total size of documents in edge
matching { $match: { $and: [ { from: { $eq: "geneDatabase:hugo" }
}, {} ] } } exceeds maximum document size' on server

Alle Versuche, die Anzahl der Dokumente zu begrenzen, schlagen fehl. allowDiskUse: true bewirkt nichts. Das Senden eines cursor bewirkt nichts. Das Hinzufügen eines $limit zur Aggregation schlägt ebenfalls fehl.

Wie konnte das sein?

Dann sehe ich den Fehler nochmal. Woher kommt das $match und $and und $eq ? Ist die Aggregations-Pipeline hinter den Kulissen, die den $lookup Aufruf für eine andere Aggregation erzeugt, eine, die von selbst ausgeführt wird und für die ich keine Beschränkungen festlegen oder Cursor verwenden kann?

Was geht hier vor sich?





aggregation-framework