MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Agregar $lookup O tamanho total dos documentos no pipeline de correspondência excede o tamanho máximo do documento


Como dito anteriormente no comentário, o erro ocorre porque ao executar o $lookup que por padrão produz uma "matriz" de destino no documento pai a partir dos resultados da coleção estrangeira, o tamanho total dos documentos selecionados para essa matriz faz com que o pai exceda o limite de 16 MB BSON.

O contador para isso é processar com um $unwind que segue imediatamente o $lookup estágio de tubulação. Isso realmente altera o comportamento de $lookup de tal forma que, em vez de produzir uma matriz no pai, os resultados são uma "cópia" de cada pai para cada documento correspondido.

Quase como o uso regular de $unwind , com a exceção de que, em vez de processar como um estágio de pipeline "separado", o unwinding ação é realmente adicionada ao $lookup própria operação do gasoduto. Idealmente, você também segue o $unwind com um $match condição, que também cria uma matching argumento a ser adicionado ao $lookup . Você pode realmente ver isso no explain saída para o pipeline.

Na verdade, o tópico é abordado (brevemente) em uma seção de Otimização de pipeline de agregação na documentação principal:

$lookup + $unwind Coalescence

Novo na versão 3.2.

Quando um $unwind segue imediatamente outro $lookup, e o $unwind opera no campo as do $lookup, o otimizador pode unir o $unwind no estágio $lookup. Isso evita a criação de grandes documentos intermediários.

Melhor demonstrado com uma listagem que coloca o servidor sob estresse criando documentos "relacionados" que excederiam o limite BSON de 16 MB. Feito o mais breve possível para quebrar e contornar o limite de BSON:
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();
  }

})();

Após inserir alguns dados iniciais, a listagem tentará executar uma agregação consistindo meramente de $lookup que irá falhar com o seguinte erro:

{ MongoError:tamanho total de documentos no pipeline de correspondência de borda { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } excede o tamanho máximo do documento

O que basicamente está dizendo que o limite de BSON foi excedido na recuperação.

Por outro lado, a próxima tentativa adiciona o $unwind e $match estágios do pipeline

A saída Explicar :
  {
    "$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"
      }
    }
  }

E esse resultado, claro, é bem-sucedido, porque como os resultados não estão mais sendo colocados no documento pai, o limite BSON não pode ser excedido.

Isso realmente acontece como resultado da adição de $unwind apenas, mas o $match é adicionado, por exemplo, para mostrar que isso é também adicionado ao $lookup stage e que o efeito geral é "limitar" os resultados retornados de forma efetiva, já que tudo é feito nesse $lookup operação e nenhum outro resultado diferente daqueles correspondentes são realmente retornados.

Ao construir dessa maneira, você pode consultar "dados referenciados" que excederiam o limite BSON e, em seguida, se você quiser $group os resultados de volta em um formato de matriz, uma vez que tenham sido efetivamente filtrados pela "consulta oculta" que está sendo realizada por $lookup .

MongoDB 3.6 e superior - Adicional para "LEFT JOIN"


Como todo o conteúdo acima observa, o Limite BSON é um "difícil" limite que você não pode violar e geralmente é por isso que o $unwind é necessário como um passo provisório. No entanto, existe a limitação de que o "LEFT JOIN" se torna um "INNER JOIN" em virtude do $unwind onde não pode preservar o conteúdo. Também até mesmo preserveNulAndEmptyArrays negaria a "coalescência" e ainda deixaria a matriz intacta, causando o mesmo problema de limite de BSON.

MongoDB 3.6 adiciona nova sintaxe ao $lookup que permite que uma expressão "sub-pipeline" seja usada no lugar das chaves "local" e "estrangeira". Então, em vez de usar a opção "coalescence" como demonstrado, desde que o array produzido também não ultrapasse o limite, é possível colocar condições nesse pipeline que retornam o array "intacto", e possivelmente sem correspondências como seria indicativo de um "LEFT JOIN".

A nova expressão seria então:
{ "$lookup": {
  "from": "edge",
  "let": { "gid": "$gid" },
  "pipeline": [
    { "$match": {
      "_id": { "$gte": 1, "$lte": 5 },
      "$expr": { "$eq": [ "$$gid", "$to" ] }
    }}          
  ],
  "as": "from"
}}

Na verdade, isso seria basicamente o que o MongoDB está fazendo "nos bastidores" com a sintaxe anterior desde 3.6 usa $expr "internamente" para construir o enunciado. A diferença, claro, é que não há "unwinding" opção presente em como o $lookup realmente é executado.

Se nenhum documento for realmente produzido como resultado do "pipeline" expressão, então a matriz de destino dentro do documento mestre estará de fato vazia, assim como um "LEFT JOIN" realmente faz e seria o comportamento normal de $lookup sem outras opções.

No entanto, a matriz de saída para NÃO DEVE fazer com que o documento em que está sendo criado exceda o limite BSON . Portanto, cabe a você garantir que qualquer conteúdo "correspondente" pelas condições permaneça abaixo desse limite ou o mesmo erro persistirá, a menos que você realmente use $unwind para efetuar o "INNER JOIN".