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


À 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": [
      {...},
      {...},
      {...},
    ]
  }
]

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}]
  }
]

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 } } }
] );

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

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)
}

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);

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
            });
          }
      });
    }
  });
});

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
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) => {})

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
}

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.



0

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

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


Je pense que la personne qui pose la question veut se grouper en fonction du sujet.
mjaggard
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.