Agrégation MongoDB: Comment obtenir le nombre total d'enregistrements?

101

J'ai utilisé l'agrégation pour récupérer les enregistrements 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),
));

Si j'exécute cette requête sans limite, 10 enregistrements seront récupérés. Mais je veux garder la limite à 2. Je voudrais donc obtenir le nombre total d'enregistrements. Comment puis-je faire avec l'agrégation? S'il vous plait conseillez-moi. Merci

user2987836
la source
À quoi ressembleraient les résultats s'il n'y en avait que 2?
WiredPrairie
Jetez un œil à $ facet Cela peut aider stackoverflow.com/questions/61812361/...
Soham

Réponses:

100

C'est l'une des questions les plus fréquemment posées pour obtenir le résultat paginé et le nombre total de résultats simultanément dans une seule requête. Je ne peux pas expliquer ce que j'ai ressenti quand je l'ai finalement atteint 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 ) ) ) )
))

Le résultat ressemblera à ceci:

[
  {
    "_id": null,
    "total": ...,
    "results": [
      {...},
      {...},
      {...},
    ]
  }
]
Anurag pareek
la source
8
Documentation à ce sujet: docs.mongodb.com/v3.2/reference/operator/aggregation/group/… ... notez qu'avec cette approche, l'ensemble des résultats non paginés doit tenir dans 16 Mo.
btown
7
C'est de l'or pur! J'allais à travers l'enfer en essayant de faire fonctionner ça.
Henrique Miranda
4
Merci mec! J'ai juste besoin { $group: { _id: null, count: { $sum:1 }, result: { $push: '$$ROOT' }}}(insérer après {$group:{}}pour le décompte total trouvé.
Liberateur
1
Comment appliquez-vous une limite à l'ensemble de résultats? Les résultats sont maintenant un tableau imbriqué
valen
@valen Vous pouvez voir la dernière ligne de code "'results' => array ('$ slice' => array ('$ results', $ skip, $ length))" Ici vous pouvez appliquer des limites et sauter des paramètres
Anurag pareek
82

Depuis la v.3.4 (je pense), MongoDB a maintenant un nouvel opérateur de pipeline d'agrégation appelé `` facet '' qui, dans leurs propres mots:

Traite plusieurs pipelines d'agrégation en une seule étape sur le même ensemble de documents d'entrée. Chaque sous-pipeline a son propre champ dans le document de sortie où ses résultats sont stockés sous forme de tableau de documents.

Dans ce cas particulier, cela signifie que l'on peut faire quelque chose comme ceci:

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

Le résultat sera (avec pour ex 100 résultats au total):

[
  {
    "paginatedResults":[{...},{...},{...}, ...],
    "totalCount":[{"count":100}]
  }
]
user3658510
la source
13
Cela fonctionne très bien, à partir de 3.4, cela devrait être la réponse acceptée
Adam Reis
Pour convertir un résultat si complet en un simple objet à deux champs, j'en ai besoin d'un autre $project?
SerG
1
cela doit maintenant être la réponse acceptée. a fonctionné comme du charme.
Arootin Aghazaryan
9
Cela devrait être la réponse acceptée aujourd'hui. Cependant, j'ai trouvé des problèmes de performances lors de l'utilisation de la pagination avec $ facet. L'autre réponse votée à la hausse a également des problèmes de performances avec $ slice. J'ai trouvé préférable de $ skip et $ limit dans le pipeline et de faire un appel séparé pour le comptage. J'ai testé cela sur des ensembles de données assez volumineux.
Jpepper
59

Utilisez ceci pour trouver le nombre total dans la collection résultante.

db.collection.aggregate( [
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $group: { _id: null, count: { $sum: 1 } } }
] );
Vishal Ranapariya
la source
3
Merci. Mais, j'ai utilisé des "vues" dans mon codage pour obtenir le décompte du nombre de groupes correspondant (c'est-à-dire, groupe 1 => 2 enregistrements, groupe 3 => 5 enregistrements et ainsi de suite). Je veux obtenir le nombre d'enregistrements (c'est-à-dire, total: 120 enregistrements). J'espère que vous avez compris ..
user2987836
34

Vous pouvez utiliser la fonction toArray, puis obtenir sa longueur pour le nombre total d'enregistrements.

db.CollectionName.aggregate([....]).toArray().length
Ankit Arya
la source
1
Bien que cela puisse ne pas fonctionner comme une solution «appropriée», cela m'a aidé à déboguer quelque chose - cela fonctionne, même si ce n'est pas une solution à 100%.
Johann Marx
3
Ce n'est pas une vraie solution.
Furkan Başaran
1
TypeError: Parent.aggregate(...).toArray is not a functionc'est l'erreur que j'ai donnée avec cette solution.
Mohammad Hossein Shojaeinia
Merci. C'est ce que je cherchais.
skvp le
Cela récupérera toutes les données agrégées puis retournera la longueur de ce tableau. pas une bonne pratique. à la place, vous pouvez ajouter {$ count: 'count'} dans le pipeline d'agrégation
Aslam Shaik
19

Utilisez l' étape de pipeline d'agrégation $ count pour obtenir le nombre total de documents:

Requete :

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

Résultat:

{
   "totalCount" : Number of records (some integer value)
}
cnsnaveen
la source
Cela fonctionne comme un charme, mais en termes de performances, est-ce bon?
ana.arede le
Solution propre. Merci
skvp
13

Je l'ai fait de cette façon:

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

L'agrégat renverra le tableau donc il suffit de le boucler et d'obtenir l'index final.

Et une autre façon de le faire est:

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);
homme fou
la source
fwiw vous n'avez besoin varni de la déclaration ni de l' mapappel. Les 3 premières lignes de votre premier exemple sont suffisantes.
Madbreaks
7

La solution fournie par @Divergent fonctionne, mais d'après mon expérience, il est préférable d'avoir 2 requêtes:

  1. D'abord pour le filtrage, puis le regroupement par ID pour obtenir le nombre d'éléments filtrés. Ne filtrez pas ici, c'est inutile.
  2. Deuxième requête qui filtre, trie et pagine.

La solution consistant à pousser $$ ROOT et à utiliser $ slice s'exécute dans une limitation de la mémoire de document de 16 Mo pour les grandes collections. En outre, pour les grandes collections, deux requêtes semblent s'exécuter plus rapidement que celle avec $$ ROOT poussant. Vous pouvez également les exécuter en parallèle, vous n'êtes donc limité que par la plus lente des deux requêtes (probablement celle qui trie).

J'ai réglé cette solution en utilisant 2 requêtes et un cadre d'agrégation (note - j'utilise node.js dans cet exemple, mais l'idée est la même):

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
la source
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
la source
1
Il est généralement recommandé d'inclure un texte explicatif avec une réponse codée.
3

Cela pourrait fonctionner pour plusieurs conditions de correspondance

            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
la source
2

J'avais besoin du décompte total absolu après avoir appliqué l'agrégation. Cela a fonctionné pour moi:

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

Résultat:

{
    "_id" : null,
    "count" : 57.0
}
miqrc
la source
2

Voici quelques moyens d'obtenir le nombre total d'enregistrements tout en effectuant l'agrégation MongoDB:


  • Utilisation $count:

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

    Pour obtenir 1000 enregistrements, cela prend en moyenne 2 ms et c'est le moyen le plus rapide.


  • Utilisation .toArray():

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

    Pour obtenir 1000 enregistrements, cela prend en moyenne 18 ms.


  • Utilisation .itcount():

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

    Pour obtenir 1000 enregistrements, cela prend en moyenne 14 ms.

palasн
la source
0

Désolé, mais je pense que vous avez besoin de deux requêtes. Un pour les vues totales et un autre pour les enregistrements groupés.

Vous pouvez trouver utile cette réponse

rubenfa
la source
Merci..Je pense que oui..Mais, il n'y a pas d'option avec l'agrégation .. :(
user2987836
1
J'ai couru dans une situation similaire. Il n'y avait pas de réponse mais de faire 2 requêtes. :( stackoverflow.com/questions/20113731/…
astroanu
0

Si vous ne souhaitez pas grouper, utilisez la méthode suivante:

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

Rajan Sharma
la source
Je pense que la personne qui pose la question veut se grouper en fonction du sujet.
mjaggard