Agregação do MongoDB: como obter a contagem total de registros?


99

Eu usei agregação para buscar registros de mongodb.

$result = $collection->aggregate(array(
  array('$match' => $document),
  array('$group' => array('_id' => '$book_id', 'date' => array('$max' => '$book_viewed'),  'views' => array('$sum' => 1))),
  array('$sort' => $sort),
  array('$skip' => $skip),
  array('$limit' => $limit),
));

Se eu executar esta consulta sem limite, 10 registros serão obtidos. Mas quero manter o limite de 2. Portanto, gostaria de obter a contagem total de registros. Como posso fazer com agregação? Por favor, me aconselhe. obrigado


Como seriam os resultados se houvesse apenas 2?
WiredPrairie

Dê uma olhada em $ facet Isto pode ajudar stackoverflow.com/questions/61812361/…
Soham

Respostas:


100

Esta é uma das perguntas mais frequentes para obter o resultado paginado e o número total de resultados simultaneamente em uma única consulta. Não posso explicar como me senti quando finalmente consegui LOL.

$result = $collection->aggregate(array(
  array('$match' => $document),
  array('$group' => array('_id' => '$book_id', 'date' => array('$max' => '$book_viewed'),  'views' => array('$sum' => 1))),
  array('$sort' => $sort),

// get total, AND preserve the results
  array('$group' => array('_id' => null, 'total' => array( '$sum' => 1 ), 'results' => array( '$push' => '$$ROOT' ) ),
// apply limit and offset
  array('$project' => array( 'total' => 1, 'results' => array( '$slice' => array( '$results', $skip, $length ) ) ) )
))

O resultado será mais ou menos assim:

[
  {
    "_id": null,
    "total": ...,
    "results": [
      {...},
      {...},
      {...},
    ]
  }
]

8
Documentação sobre isso: docs.mongodb.com/v3.2/reference/operator/aggregation/group/… ... observe que, com esta abordagem, todo o conjunto de resultados não paginado deve caber em 16 MB.
btown de

7
Isso é ouro puro! Eu estava passando por um inferno tentando fazer isso funcionar.
Henrique Miranda

4
Obrigado cara! Juste preciso { $group: { _id: null, count: { $sum:1 }, result: { $push: '$$ROOT' }}}(inserir depois {$group:{}}para contagem total achado.
Liberateur

1
Como você aplica o limite ao conjunto de resultados? Os resultados agora são uma matriz aninhada
valen

@valen Você pode ver a última linha do código "'resultados' => array ('$ slice' => array ('$ results', $ skip, $ length))" Aqui você pode aplicar limites e pular parâmetros
Anurag pareek

80

Desde a v.3.4 (eu acho) MongoDB tem agora um novo operador de pipeline de agregação chamado ' facet ' que em suas próprias palavras:

Processa vários pipelines de agregação em um único estágio no mesmo conjunto de documentos de entrada. Cada sub pipeline tem seu próprio campo no documento de saída, onde seus resultados são armazenados como uma matriz de documentos.

Neste caso específico, isso significa que se pode fazer algo assim:

$result = $collection->aggregate([
  { ...execute queries, group, sort... },
  { ...execute queries, group, sort... },
  { ...execute queries, group, sort... },
  $facet: {
    paginatedResults: [{ $skip: skipPage }, { $limit: perPage }],
    totalCount: [
      {
        $count: 'count'
      }
    ]
  }
]);

O resultado será (com por ex 100 resultados totais):

[
  {
    "paginatedResults":[{...},{...},{...}, ...],
    "totalCount":[{"count":100}]
  }
]

13
Isso funciona muito bem, a partir de 3.4 esta deve ser a resposta aceita
Adam Reis

Para converter um resultado tão arrayful em um objeto de dois campos simples, preciso de outro $project?
SerG de

1
esta agora deve ser a resposta aceita. funcionou como charme.
Arootin Aghazaryan

8
Esta deve ser a resposta aceita hoje. No entanto, encontrei problemas de desempenho ao usar paginação com $ facet. A outra resposta votada também tem problemas de desempenho com $ slice. Achei melhor $ skip e $ limit no pipeline e fazer uma chamada separada para contar. Eu testei isso com conjuntos de dados bastante grandes.
Jpepper de

57

Use para encontrar a contagem total na coleção resultante.

db.collection.aggregate( [
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $group: { _id: null, count: { $sum: 1 } } }
] );

3
Obrigado. Mas, usei "visualizações" em minha codificação para obter a contagem da contagem de grupo correspondente (ou seja, grupo 1 => 2 registros, grupo 3 => 5 registros e assim por diante). Desejo obter a contagem de registros (ou seja, total: 120 registros). Espero que você tenha entendido ..
user2987836

33

Você pode usar a função toArray e obter seu comprimento para a contagem total de registros.

db.CollectionName.aggregate([....]).toArray().length

1
Embora isso possa não funcionar como uma solução "adequada", me ajudou a depurar algo - funciona, mesmo que não seja 100% uma solução.
Johann Marx

3
Esta não é a solução real.
Furkan Başaran

1
TypeError: Parent.aggregate(...).toArray is not a functioneste é o erro que dei com esta solução.
Mohammad Hossein Shojaeinia,

Obrigado. Isso é o que eu estava procurando.
skvp

Isso buscará todos os dados agregados e, em seguida, retornará o comprimento dessa matriz. não é uma boa prática. em vez disso, você pode adicionar {$ count: 'count'} no pipeline de agregação
Aslam Shaik

18

Use o estágio de pipeline de agregação $ count para obter a contagem total de documentos:

Inquerir :

db.collection.aggregate(
  [
    {
      $match: {
        ...
      }
    },
    {
      $group: {
        ...
      }
    },
    {
      $count: "totalCount"
    }
  ]
)

Resultado:

{
   "totalCount" : Number of records (some integer value)
}

Isso funciona como um encanto, mas em termos de desempenho, é bom?
ana.arede

Solução limpa. Obrigado
skvp

13

Eu fiz assim:

db.collection.aggregate([
     { $match : { score : { $gt : 70, $lte : 90 } } },
     { $group: { _id: null, count: { $sum: 1 } } }
] ).map(function(record, index){
        print(index);
 });

O agregado retornará a matriz, então apenas faça um loop e obtenha o índice final.

E outra maneira de fazer isso é:

var count = 0 ;
db.collection.aggregate([
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $group: { _id: null, count: { $sum: 1 } } }
] ).map(function(record, index){
        count++
 }); 
print(count);

fwiw você não precisa da vardeclaração nem da mapchamada. As primeiras 3 linhas do seu primeiro exemplo são suficientes.
Madbreaks

7

A solução fornecida por @Divergent funciona, mas na minha experiência é melhor ter 2 consultas:

  1. Primeiro para filtrar e depois agrupar por ID para obter o número de elementos filtrados. Não filtre aqui, é desnecessário.
  2. Segunda consulta que filtra, classifica e pagina.

A solução com o envio de $$ ROOT e o uso de $ slice atinge a limitação de memória do documento de 16 MB para grandes coleções. Além disso, para grandes coleções, duas consultas juntas parecem executar mais rápido do que aquela com envio de $$ ROOT. Você também pode executá-los em paralelo, portanto, você está limitado apenas pela mais lenta das duas consultas (provavelmente aquela que classifica).

Resolvi esta solução usando 2 consultas e estrutura de agregação (nota - eu uso node.js neste exemplo, mas a ideia é a mesma):

var aggregation = [
  {
    // If you can match fields at the begining, match as many as early as possible.
    $match: {...}
  },
  {
    // Projection.
    $project: {...}
  },
  {
    // Some things you can match only after projection or grouping, so do it now.
    $match: {...}
  }
];


// Copy filtering elements from the pipeline - this is the same for both counting number of fileter elements and for pagination queries.
var aggregationPaginated = aggregation.slice(0);

// Count filtered elements.
aggregation.push(
  {
    $group: {
      _id: null,
      count: { $sum: 1 }
    }
  }
);

// Sort in pagination query.
aggregationPaginated.push(
  {
    $sort: sorting
  }
);

// Paginate.
aggregationPaginated.push(
  {
    $limit: skip + length
  },
  {
    $skip: skip
  }
);

// I use mongoose.

// Get total count.
model.count(function(errCount, totalCount) {
  // Count filtered.
  model.aggregate(aggregation)
  .allowDiskUse(true)
  .exec(
  function(errFind, documents) {
    if (errFind) {
      // Errors.
      res.status(503);
      return res.json({
        'success': false,
        'response': 'err_counting'
      });
    }
    else {
      // Number of filtered elements.
      var numFiltered = documents[0].count;

      // Filter, sort and pagiante.
      model.request.aggregate(aggregationPaginated)
      .allowDiskUse(true)
      .exec(
        function(errFindP, documentsP) {
          if (errFindP) {
            // Errors.
            res.status(503);
            return res.json({
              'success': false,
              'response': 'err_pagination'
            });
          }
          else {
            return res.json({
              'success': true,
              'recordsTotal': totalCount,
              'recordsFiltered': numFiltered,
              'response': documentsP
            });
          }
      });
    }
  });
});

5
//const total_count = await User.find(query).countDocuments();
//const users = await User.find(query).skip(+offset).limit(+limit).sort({[sort]: order}).select('-password');
const result = await User.aggregate([
  {$match : query},
  {$sort: {[sort]:order}},
  {$project: {password: 0, avatarData: 0, tokens: 0}},
  {$facet:{
      users: [{ $skip: +offset }, { $limit: +limit}],
      totalCount: [
        {
          $count: 'count'
        }
      ]
    }}
  ]);
console.log(JSON.stringify(result));
console.log(result[0]);
return res.status(200).json({users: result[0].users, total_count: result[0].totalCount[0].count});

1
Normalmente, é uma boa prática incluir um texto explicativo junto com uma resposta em código.

3

Isso pode funcionar para várias condições de correspondência

            const query = [
                {
                    $facet: {
                    cancelled: [
                        { $match: { orderStatus: 'Cancelled' } },
                        { $count: 'cancelled' }
                    ],
                    pending: [
                        { $match: { orderStatus: 'Pending' } },
                        { $count: 'pending' }
                    ],
                    total: [
                        { $match: { isActive: true } },
                        { $count: 'total' }
                    ]
                    }
                },
                {
                    $project: {
                    cancelled: { $arrayElemAt: ['$cancelled.cancelled', 0] },
                    pending: { $arrayElemAt: ['$pending.pending', 0] },
                    total: { $arrayElemAt: ['$total.total', 0] }
                    }
                }
                ]
                Order.aggregate(query, (error, findRes) => {})

2

Eu precisava da contagem total absoluta após aplicar a agregação. Isso funcionou para mim:

db.mycollection.aggregate([
    {
        $group: { 
            _id: { field1: "$field1", field2: "$field2" },
        }
    },
    { 
        $group: { 
            _id: null, count: { $sum: 1 } 
        } 
    }
])

Resultado:

{
    "_id" : null,
    "count" : 57.0
}

2

Aqui estão algumas maneiras de obter a contagem total de registros ao fazer a agregação do MongoDB:


  • Usando $count:

    db.collection.aggregate([
       // Other stages here
       { $count: "Total" }
    ])

    Para obter 1000 registros, leva em média 2 ms e é o caminho mais rápido.


  • Usando .toArray():

    db.collection.aggregate([...]).toArray().length

    Para obter 1000 registros, leva em média 18 ms.


  • Usando .itcount():

    db.collection.aggregate([...]).itcount()

    Para obter 1000 registros, leva em média 14 ms.



0

Se você não quiser agrupar, use o seguinte método:

db.collection.aggregate( [ { $match : { score : { $gt : 70, $lte : 90 } } }, { $count: 'count' } ] );


Eu acho que a pessoa que fez a pergunta quer agrupar, com base no assunto.
mjaggard
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.