MongoDB Aggregation: come ottenere il conteggio totale dei record?


102

Ho usato l'aggregazione per recuperare i record da 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 eseguo questa query senza limiti, verranno recuperati 10 record. Ma voglio mantenere il limite come 2. Quindi vorrei ottenere il conteggio totale dei record. Come posso fare con l'aggregazione? Per favore, consigliami. Grazie


Come sarebbero i risultati se ce ne fossero solo 2?
WiredPrairie

Dai un'occhiata a $ facet Questo può aiutare stackoverflow.com/questions/61812361/…
Soham

Risposte:


101

Questa è una delle domande più frequenti per ottenere simultaneamente il risultato impaginato e il numero totale di risultati in una singola query. Non riesco a spiegare come mi sono sentito quando finalmente l'ho raggiunto 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 ) ) ) )
))

Il risultato sarà simile a questo:

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

8
Documentazione su questo: docs.mongodb.com/v3.2/reference/operator/aggregation/group/… ... notare che con questo approccio, l'intero set di risultati non impaginato deve rientrare in 16 MB.
btown

7
Questo è oro puro! Stavo andando all'inferno cercando di far funzionare questo.
Henrique Miranda

4
Grazie ragazzo ! Ne ho bisogno { $group: { _id: null, count: { $sum:1 }, result: { $push: '$$ROOT' }}}(inserire dopo {$group:{}}per il conteggio totale trovato.
Liberateur

1
Come si applica il limite al set di risultati? I risultati ora sono un array annidato
valen

1
La mia vita è completa ora, posso morire felice
Jack

83

Dalla v.3.4 (credo) MongoDB ha ora un nuovo operatore di pipeline di aggregazione chiamato ' facet ' che nelle loro stesse parole:

Elabora più pipeline di aggregazione all'interno di una singola fase sullo stesso set di documenti di input. Ogni pipeline secondaria ha il proprio campo nel documento di output in cui i risultati vengono archiviati come una matrice di documenti.

In questo caso particolare, ciò significa che si può fare qualcosa del genere:

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

Il risultato sarà (con per ex 100 risultati totali):

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

13
Funziona benissimo, a partire da 3.4 questa dovrebbe essere la risposta accettata
Adam Reis,

Per convertire un risultato così schierato in un semplice oggetto a due campi ho bisogno di un altro $project?
SerG

1
questa deve ora essere la risposta accettata. ha funzionato come un fascino.
Arootin Aghazaryan

9
Questa dovrebbe essere la risposta accettata oggi. Tuttavia, ho riscontrato problemi di prestazioni durante l'utilizzo del paging con $ facet. L'altra risposta votata ha anche problemi di prestazioni con $ slice. Ho trovato meglio $ skip e $ limit nella pipeline e fare una chiamata separata per il conteggio. L'ho testato su set di dati abbastanza grandi.
Jpepper

59

Usalo per trovare il conteggio totale nella raccolta risultante.

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

3
Grazie. Tuttavia, ho utilizzato "visualizzazioni" nella mia codifica per ottenere il conteggio del conteggio del gruppo corrispondente (ad esempio, gruppo 1 => 2 record, gruppo 3 => 5 record e così via). Voglio ottenere il conteggio dei record (cioè, totale: 120 record). Spero tu abbia capito ..
user2987836

34

È possibile utilizzare la funzione toArray e quindi ottenere la sua lunghezza per il conteggio totale dei record.

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

1
Anche se questo potrebbe non funzionare come una soluzione "corretta", mi ha aiutato a eseguire il debug di qualcosa: funziona, anche se non è una soluzione al 100%.
Johann Marx

3
Questa non è una vera soluzione.
Furkan Başaran

1
TypeError: Parent.aggregate(...).toArray is not a functionquesto è l'errore che ho dato con questa soluzione.
Mohammad Hossein Shojaeinia

Grazie. Questo è quello che stavo cercando.
skvp

Questo recupererà tutti i dati aggregati quindi restituirà la lunghezza di quell'array. non è una buona pratica. invece puoi aggiungere {$ count: 'count'} nella pipeline di aggregazione
Aslam Shaik,

19

Utilizza la fase della pipeline di aggregazione $ count per ottenere il conteggio totale dei documenti:

Query:

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

Risultato:

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

Funziona proprio come un fascino, ma dal punto di vista delle prestazioni è buono?
ana.arede

Soluzione pulita. Grazie
skvp

13

L'ho fatto in questo modo:

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

L'aggregato restituirà l'array, quindi basta eseguire il loop e ottenere l'indice finale.

E un altro modo per farlo è:

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 non hai bisogno della vardichiarazione né della mapchiamata. Le prime 3 righe del tuo primo esempio sono sufficienti.
Madbreaks

7

La soluzione fornita da @Divergent funziona, ma nella mia esperienza è meglio avere 2 domande:

  1. Prima per filtrare e poi raggruppare per ID per ottenere il numero di elementi filtrati. Non filtrare qui, non è necessario.
  2. Seconda query che filtra, ordina e impagina.

La soluzione con il push di $$ ROOT e l'utilizzo di $ slice comporta la limitazione della memoria del documento di 16 MB per raccolte di grandi dimensioni. Inoltre, per raccolte di grandi dimensioni, due query insieme sembrano essere eseguite più velocemente di quella con $$ ROOT push. Puoi anche eseguirli in parallelo, quindi sei limitato solo dalla più lenta delle due query (probabilmente quella che ordina).

Ho scelto questa soluzione utilizzando 2 query e un framework di aggregazione (nota: utilizzo node.js in questo esempio, ma l'idea è la stessa):

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
Di solito è buona norma includere un testo esplicativo insieme a una risposta in codice.

3

Questo potrebbe funzionare per più condizioni di corrispondenza

            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

Avevo bisogno del conteggio totale assoluto dopo aver applicato l'aggregazione. Questo ha funzionato per me:

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

Risultato:

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

2

Di seguito sono riportati alcuni modi per ottenere il conteggio totale dei record durante l'esecuzione dell'aggregazione MongoDB:


  • Utilizzando $count:

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

    Per ottenere 1000 record questo richiede in media 2 ms ed è il modo più veloce.


  • Utilizzando .toArray():

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

    Per ottenere 1000 record questo richiede in media 18 ms.


  • Utilizzando .itcount():

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

    Per ottenere 1000 record questo richiede in media 14 ms.



0

Se non vuoi raggruppare, usa il seguente metodo:

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


Penso che la persona che pone la domanda voglia fare un gruppo, in base all'argomento.
mjaggard
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.