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

Obtenha a contagem filtrada de elementos na matriz de $ lookup junto com todo o documento

Anotação para quem procura - Contagem Estrangeira


Um pouco melhor do que foi respondido originalmente é usar a forma mais recente de $pesquisa do MongoDB 3.6. Isso pode realmente fazer a "contagem" na expressão "sub-pipeline" em vez de retornar um "array" para filtragem e contagem subsequentes ou até mesmo usar $unwind
db.emailGroup.aggregate([
  { "$lookup": {
    "from": "link",
    "let": { "id": "$_id" },
    "pipeline": [
      { "$match": {
        "originalLink": "",
        "$expr": { "$eq": [ "$$id", "$_id" ] }
      }},
      { "$count": "count" }
    ],
    "as": "linkCount"    
  }},
  { "$addFields": {
    "linkCount": { "$sum": "$linkCount.count" }
  }}
])

Não o que a pergunta original estava pedindo, mas parte da resposta abaixo na forma mais otimizada, como é claro, o resultado de $lookup é reduzido para a "contagem de correspondências" somente em vez de "todos os documentos correspondentes".

Original


A maneira correta de fazer isso seria adicionar o "linkCount" para o $group palco, bem como um $first em quaisquer campos adicionais do documento pai para obter o formulário "singular", como era o estado "antes" do $unwind foi processado na matriz que foi o resultado de $lookup :

Todos os detalhes
db.emailGroup.aggregate([
  { "$lookup": {
    "from": "link",
    "localField": "_id",
    "foreignField": "emailGroupId",
    "as": "link"    
  }},
  { "$unwind": "$link" },
  { "$match": { "link.originalLink": "" } },
  { "$group": {
    "_id": "$_id",
    "partId": { "$first": "$partId" },
    "link": { "$push": "$link" },
    "linkCount": {
      "$sum": {
        "$size": {
          "$ifNull": [ "$link.linkHistory", [] ]
        } 
      }   
    }
  }}
])

Produz:
{
    "_id" : ObjectId("594a6c47f51e075db713ccb6"),
    "partId" : "f56c7c71eb14a20e6129a667872f9c4f",
    "link" : [ 
        {
            "_id" : ObjectId("594b96d6f51e075db67c44c9"),
            "originalLink" : "",
            "emailGroupId" : ObjectId("594a6c47f51e075db713ccb6"),
            "linkHistory" : [ 
                {
                    "_id" : ObjectId("594b96f5f51e075db713ccdf")
                }, 
                {
                    "_id" : ObjectId("594b971bf51e075db67c44ca")
                }
            ]
        }
    ],
    "linkCount" : 2
}

Agrupar por partId
db.emailGroup.aggregate([
  { "$lookup": {
    "from": "link",
    "localField": "_id",
    "foreignField": "emailGroupId",
    "as": "link"    
  }},
  { "$unwind": "$link" },
  { "$match": { "link.originalLink": "" } },
  { "$group": {
    "_id": "$partId",
    "linkCount": {
      "$sum": {
        "$size": {
          "$ifNull": [ "$link.linkHistory", [] ]
        } 
      }   
    }
  }}
])

Produz
{
    "_id" : "f56c7c71eb14a20e6129a667872f9c4f",
    "linkCount" : 2
}

A razão pela qual você faz isso com um $unwind e, em seguida, um $match é por causa de como o MongoDB realmente lida com o pipeline quando emitido nessa ordem. Isso é o que acontece com o $lookup como é demonstrado no "explain" saída da operação:
    {
        "$lookup" : {
            "from" : "link",
            "as" : "link",
            "localField" : "_id",
            "foreignField" : "emailGroupId",
            "unwinding" : {
                "preserveNullAndEmptyArrays" : false
            },
            "matching" : {
                "originalLink" : {
                    "$eq" : ""
                }
            }
        }
    }, 
    {
        "$group" : {

Estou deixando a parte com $group nessa saída para demonstrar que os outros dois estágios do pipeline "desaparecem". Isso ocorre porque eles foram "agrupados" no $pesquisa estágio de tubulação como mostrado. Na verdade, é assim que o MongoDB lida com a possibilidade de que o limite BSON possa ser excedido pelo resultado de "juntar" resultados de $lookup em uma matriz do documento pai.

Você pode escrever alternadamente a operação assim:

Todos os detalhes
db.emailGroup.aggregate([
  { "$lookup": {
    "from": "link",
    "localField": "_id",
    "foreignField": "emailGroupId",
    "as": "link"    
  }},
  { "$addFields": {
    "link": {
      "$filter": {
        "input": "$link",
        "as": "l",
        "cond": { "$eq": [ "$$l.originalLink", "" ] }    
      }
    },
    "linkCount": {
      "$sum": {
        "$map": {
          "input": {
            "$filter": {
              "input": "$link",
              "as": "l",
              "cond": { "$eq": [ "$$l.originalLink", "" ] }
            }
          },
          "as": "l",
          "in": { "$size": { "$ifNull": [ "$$l.linkHistory", [] ] } }
        }     
      }
    }    
  }}
])

Agrupar por partId
db.emailGroup.aggregate([
  { "$lookup": {
    "from": "link",
    "localField": "_id",
    "foreignField": "emailGroupId",
    "as": "link"    
  }},
  { "$addFields": {
    "link": {
      "$filter": {
        "input": "$link",
        "as": "l",
        "cond": { "$eq": [ "$$l.originalLink", "" ] }    
      }
    },
    "linkCount": {
      "$sum": {
        "$map": {
          "input": {
            "$filter": {
              "input": "$link",
              "as": "l",
              "cond": { "$eq": [ "$$l.originalLink", "" ] }
            }
          },
          "as": "l",
          "in": { "$size": { "$ifNull": [ "$$l.linkHistory", [] ] } }
        }     
      }
    }    
  }},
  { "$unwind": "$link" },
  { "$group": {
    "_id": "$partId",
    "linkCount": { "$sum": "$linkCount" } 
  }}
])

Que tem a mesma saída, mas "difere" da primeira consulta, pois o $filtro aqui é aplicado "depois" de TODOS resultados do $lookup são retornados para a nova matriz do documento pai.

Portanto, em termos de desempenho, é realmente mais eficaz fazê-lo da primeira maneira, além de ser portátil para possíveis grandes conjuntos de resultados "antes da filtragem" que, de outra forma, quebrariam o limite BSON de 16 MB.

Como uma nota lateral para aqueles que estão interessados, em versões futuras do MongoDB (presumivelmente 3.6 ou superior) você pode usar $replaceRoot em vez de um $addFields com o uso do novo $mergeObjects operador de dutos. A vantagem disso é como um "bloco", podemos declarar o "filtrado" conteúdo como uma variável via $let , o que significa que você não precisa escrever o mesmo $filter "duas vezes":
db.emailGroup.aggregate([
  { "$lookup": {
    "from": "link",
    "localField": "_id",
    "foreignField": "emailGroupId",
    "as": "link"    
  }},
  { "$replaceRoot": {
    "newRoot": {
      "$mergeObjects": [
        "$$ROOT",
        { "$let": {
          "vars": {
            "filtered": {
              "$filter": {
                "input": "$link",
                "as": "l",
                "cond": { "$eq": [ "$$l.originalLink", "" ] }    
              }
            }
          },
          "in": {
            "link": "$$filtered",
            "linkCount": {
              "$sum": {
                "$map": {
                  "input": "$$filtered.linkHistory",
                  "as": "lh",
                  "in": { "$size": { "$ifNull": [ "$$lh", [] ] } } 
                }   
              } 
            }  
          }
        }}
      ]
    }
  }}
])

No entanto, a melhor maneira de fazer isso "filtrado" $lookup operações está "ainda" neste momento usando o $unwind então $match padrão, até que você possa fornecer argumentos de consulta para $ pesquisa diretamente.