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

Agrupe e conte usando a estrutura de agregação


Parece que você começou nisso, mas se perdeu em alguns dos outros conceitos. Existem algumas verdades básicas ao trabalhar com arrays em documentos, mas vamos começar de onde você parou:
db.sample.aggregate([
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 }
    }}
])

Então, isso só vai usar o $group pipeline para reunir seus documentos nos diferentes valores do campo "status" e também produzir outro campo para "count", que obviamente "conta" as ocorrências da chave de agrupamento passando um valor de 1 para o $sum operador para cada documento encontrado. Isso coloca você em um ponto muito parecido com o que você descreve:
{ "_id" : "done", "count" : 2 }
{ "_id" : "canceled", "count" : 1 }

Esse é o primeiro estágio e fácil de entender, mas agora você precisa saber como obter valores de um array. Você pode ficar tentado quando entender a "notação de ponto" concept corretamente para fazer algo assim:
db.sample.aggregate([
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$devices.cost" }
    }}
])

Mas o que você descobrirá é que o "total" será de fato 0 para cada um desses resultados:
{ "_id" : "done", "count" : 2, "total" : 0 }
{ "_id" : "canceled", "count" : 1, "total" : 0 }

Por quê? Bem, as operações de agregação do MongoDB como essa não percorrem os elementos da matriz ao agrupar. Para fazer isso, a estrutura de agregação tem um conceito chamado $unwind . O nome é relativamente autoexplicativo. Uma matriz incorporada no MongoDB é como ter uma associação "um para muitos" entre fontes de dados vinculadas. Então, o que $unwind faz é exatamente esse tipo de resultado de "junção", onde os "documentos" resultantes são baseados no conteúdo da matriz e nas informações duplicadas para cada pai.

Portanto, para agir nos elementos do array, você precisa usar $unwind primeiro. Isso deve logicamente levar você a um código como este:
db.sample.aggregate([
    { "$unwind": "$devices" },
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$devices.cost" }
    }}
])

E então o resultado:
{ "_id" : "done", "count" : 4, "total" : 700 }
{ "_id" : "canceled", "count" : 2, "total" : 350 }

Mas isso não é muito certo é? Lembre-se do que você acabou de aprender com $unwind e como ele faz uma junção desnormalizada com as informações do pai? Então agora isso é duplicado para todos os documentos, pois ambos tinham dois membros da matriz. Portanto, embora o campo "total" esteja correto, a "contagem" é o dobro do que deveria ser em cada caso.

Um pouco mais de cuidado precisa ser tomado, então, em vez de fazer isso em um único $group etapa, é feito em dois:
db.sample.aggregate([
    { "$unwind": "$devices" },
    { "$group": {
        "_id": "$_id",
        "status": { "$first": "$status" },
        "total": { "$sum": "$devices.cost" }
    }},
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$total" }
    }}
])

Que agora obtém o resultado com os totais corretos:
{ "_id" : "canceled", "count" : 1, "total" : 350 }
{ "_id" : "done", "count" : 2, "total" : 700 }

Agora os números estão certos, mas ainda não é exatamente o que você está pedindo. Eu acho que você deveria parar por aí, pois o tipo de resultado que você está esperando não é realmente adequado para apenas um único resultado da agregação sozinha. Você está procurando que o total esteja "dentro" do resultado. Realmente não pertence lá, mas em dados pequenos está tudo bem:
db.sample.aggregate([
    { "$unwind": "$devices" },
    { "$group": {
        "_id": "$_id",
        "status": { "$first": "$status" },
        "total": { "$sum": "$devices.cost" }
    }},
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$total" }
    }},
    { "$group": {
        "_id": null,
        "data": { "$push": { "count": "$count", "total": "$total" } },
        "totalCost": { "$sum": "$total" }
    }}
])

E um formulário de resultado final:
{
    "_id" : null,
    "data" : [
            {
                    "count" : 1,
                    "total" : 350
            },
            {
                    "count" : 2,
                    "total" : 700
            }
    ],
    "totalCost" : 1050
}

Mas, "Não faça isso" . O MongoDB tem um limite de documento na resposta de 16 MB, que é uma limitação da especificação BSON. Em resultados pequenos, você pode fazer esse tipo de embalagem de conveniência, mas no esquema maior das coisas, você deseja os resultados na forma anterior e uma consulta separada ou vive com a iteração de todos os resultados para obter o total de todos os documentos.

Você parece estar usando uma versão do MongoDB inferior a 2.6 ou copiando a saída de um shell do RoboMongo que não suporta os recursos da versão mais recente. A partir do MongoDB 2.6, os resultados da agregação podem ser um "cursor" em vez de um único array BSON. Portanto, a resposta geral pode ser muito maior que 16 MB, mas somente quando você não estiver compactando em um único documento como resultados, mostrados no último exemplo.

Isso seria especialmente verdadeiro nos casos em que você estivesse "paginando" os resultados, com 100 a 1000 linhas de resultados, mas você só queria que um "total" retornasse em uma resposta da API quando você estivesse retornando apenas uma "página" de 25 resultados em um tempo.

De qualquer forma, isso deve fornecer um guia razoável sobre como obter o tipo de resultado que você espera do seu formulário de documento comum. Lembre-se de $unwind para processar matrizes e geralmente $group várias vezes para obter totais em diferentes níveis de agrupamento de seus agrupamentos de documentos e coleções.