sql >> Databasteknik >  >> NoSQL >> MongoDB

Sammanlagd $lookup Den totala storleken på dokument i matchande pipeline överstiger den maximala dokumentstorleken

Som nämnts tidigare i kommentaren uppstår felet eftersom $lookup utförs som som standard producerar en mål "array" inom det överordnade dokumentet från resultaten av den främmande insamlingen, gör den totala storleken på dokument som valts för den arrayen att föräldern överskrider 16 MB BSON-gränsen.

Räknaren för detta är att bearbeta med en $unwind som omedelbart följer $lookup rörledningsstadiet. Detta ändrar faktiskt beteendet hos $lookup på så sätt att istället för att producera en array i den överordnade, blir resultaten istället en "kopia" av varje överordnad för varje matchat dokument.

Ungefär som vanlig användning av $unwind , med undantaget att istället för att behandla som ett "separat" pipelinesteg, unwinding åtgärden läggs faktiskt till i $lookup själva pipelinedriften. Helst följer du också $unwind med en $match condition, vilket också skapar en matching argument som också ska läggas till i $lookup . Du kan faktiskt se detta i explain output för pipeline.

Ämnet behandlas faktiskt (kortfattat) i ett avsnitt av Aggregation Pipeline Optimization i kärndokumentationen:

$lookup + $unwind Coalescence

Nyhet i version 3.2.

När en $unwind omedelbart följer en annan $lookup, och $unwind fungerar på as-fältet för $lookup, kan optimeraren sammansmälta $unwind till $lookup-stadiet. Detta undviker att skapa stora mellanliggande dokument.

Bäst demonstreras med en lista som sätter servern under stress genom att skapa "relaterade" dokument som skulle överskrida 16MB BSON-gränsen. Görs så kort som möjligt för att både bryta och komma runt BSON-gränsen:

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

})();

Efter att ha infogat några initiala data kommer listningen att försöka köra ett aggregat som bara består av $lookup som kommer att misslyckas med följande fel:

{ MongoError:Total storlek på dokument i kantmatchande pipeline { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } överskrider den maximala dokumentstorleken

Vilket i princip talar om för dig att BSON-gränsen överskreds vid hämtning.

Däremot lägger nästa försök till $unwind och $match pipeline etapper

Utgången Explain :

  {
    "$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"
      }
    }
  }

Och det resultatet lyckas förstås, för eftersom resultaten inte längre placeras i det överordnade dokumentet kan BSON-gränsen inte överskridas.

Detta händer egentligen bara som ett resultat av att $unwind lagts till bara, men $match läggs till till exempel för att visa att detta är också läggs till i $lookup skede och att den övergripande effekten är att "begränsa" resultaten som returneras på ett effektivt sätt, eftersom det hela görs i den $lookup operation och inga andra resultat än de som matchar returneras faktiskt.

Genom att konstruera på detta sätt kan du fråga efter "referensdata" som skulle överskrida BSON-gränsen och sedan om du vill ha $group resultaten tillbaka till ett arrayformat när de väl har filtrerats av den "dolda frågan" som faktiskt utförs av $lookup .

MongoDB 3.6 och ovan - Ytterligare för "LEFT JOIN"

Som allt innehåll ovan noterar är BSON-gränsen "hård" gräns som du inte kan bryta och det är i allmänhet därför som $unwind är nödvändigt som ett interimistiskt steg. Det finns dock begränsningen att "LEFT JOIN" blir en "INNER JOIN" i kraft av $unwind där den inte kan bevara innehållet. Även preserveNulAndEmptyArrays skulle upphäva "sammansmältningen" och fortfarande lämna den intakta arrayen, vilket orsakar samma BSON Limit-problem.

MongoDB 3.6 lägger till ny syntax i $lookup som tillåter ett "sub-pipeline"-uttryck att användas i stället för "lokala" och "främmande" nycklar. Så istället för att använda alternativet "koalescens" som visat, så länge som den producerade arrayen inte också bryter mot gränsen är det möjligt att sätta villkor i den pipeline som returnerar arrayen "intakt", och möjligen utan matchningar, vilket skulle vara indikativt av en "LEFT JOIN".

Det nya uttrycket skulle då vara:

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

I själva verket skulle detta i princip vara vad MongoDB gör "under täcket" med föregående syntax sedan 3.6 använder $expr "internt" för att konstruera uttalandet. Skillnaden är naturligtvis att det inte finns någon "unwinding" alternativ som finns i hur $lookup faktiskt avrättas.

Om inga dokument faktiskt produceras som ett resultat av "pipeline" uttryck, då kommer målarrayen i huvuddokumentet i själva verket vara tom, precis som en "LEFT JOIN" faktiskt gör och skulle vara det normala beteendet för $lookup utan några andra alternativ.

Men utdatamatrisen FÅR INTE göra att dokumentet där det skapas överskrider BSON-gränsen . Så det är verkligen upp till dig att se till att allt "matchande" innehåll enligt villkoren förblir under denna gräns, annars kommer samma fel att kvarstå, såvida du inte faktiskt använder $unwind för att utföra "INNER JOIN".



  1. Implementerar jag serialisera och deserialisera NodesJS + Passport + RedisStore?

  2. Node.js Redis Connection Pooling

  3. MongoDB-aggregationsjämförelse:group(), $group och MapReduce

  4. Hur man lagrar aggregerade katalogträdsökresultat i Redis