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

user2987836
fonte
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": [
      {...},
      {...},
      {...},
    ]
  }
]
Anurag Pareek
fonte
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}]
  }
]
user3658510
fonte
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 } } }
] );
Vishal Ranapariya
fonte
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
Ankit Arya
fonte
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)
}
cnsnaveen
fonte
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);
homem louco
fonte
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
            });
          }
      });
    }
  });
});
Filip voska
fonte
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});
Harpal Singh
fonte
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) => {})
Rohit Parte
fonte
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
}
miqrc
fonte
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.

palasí
fonte
0

Desculpe, mas acho que você precisa de duas perguntas. Um para visualizações totais e outro para registros agrupados.

Você pode achar útil esta resposta

rubenfa
fonte
Obrigado ... Acho que sim ... Mas, não há opção com agregação .. :(
user2987836
1
Corri para uma situação semelhante. Não houve resposta a não ser fazer 2 perguntas. :( stackoverflow.com/questions/20113731/…
astroanu
0

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

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

Rajan Sharma
fonte
Eu acho que a pessoa que fez a pergunta quer agrupar, com base no assunto.
mjaggard