MongoDB-Aggregation: Wie erhalte ich die Gesamtzahl der Datensätze?


97

Ich habe die Aggregation zum Abrufen von Datensätzen aus Mongodb verwendet.

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

Wenn ich diese Abfrage unbegrenzt ausführe, werden 10 Datensätze abgerufen. Aber ich möchte das Limit auf 2 halten. Also möchte ich die Gesamtzahl der Datensätze erhalten. Wie kann ich mit Aggregation umgehen? Bitte berate mich. Vielen Dank


Wie würden die Ergebnisse aussehen, wenn es nur 2 gäbe?
WiredPrairie

Antworten:


99

Dies ist eine der am häufigsten gestellten Fragen, um das paginierte Ergebnis und die Gesamtzahl der Ergebnisse gleichzeitig in einer einzelnen Abfrage zu erhalten. Ich kann nicht erklären, wie ich mich gefühlt habe, als ich es endlich erreicht habe. 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 ) ) ) )
))

Das Ergebnis sieht ungefähr so ​​aus:

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

8
Dokumentation dazu: docs.mongodb.com/v3.2/reference/operator/aggregation/group/… ... Beachten Sie, dass bei diesem Ansatz die gesamte nicht paginierte Ergebnismenge in 16 MB passen muss.
Btown

7
Das ist reines Gold! Ich ging durch die Hölle und versuchte, diese Arbeit zu machen.
Henrique Miranda

4
Danke Kerl! Ich brauche { $group: { _id: null, count: { $sum:1 }, result: { $push: '$$ROOT' }}}(füge nach ein, {$group:{}}um die Gesamtzahl zu finden.
Liberateur

1
Wie können Sie die Ergebnismenge begrenzen? Ergebnisse ist jetzt ein verschachteltes Array
valen

@valen Sie können die letzte Codezeile sehen "" results '=> array (' $ Slice '=> array (' $ results ', $ skip, $ length)) "Hier können Sie Limit- und Skip-Parameter anwenden
Anurag pareek

79

Seit v.3.4 (glaube ich) hat MongoDB jetzt einen neuen Aggregationspipeline-Operator namens ' facet ', der in eigenen Worten:

Verarbeitet mehrere Aggregations-Pipelines in einer einzigen Phase auf demselben Satz von Eingabedokumenten. Jede Subpipeline verfügt über ein eigenes Feld im Ausgabedokument, in dem die Ergebnisse als Array von Dokumenten gespeichert werden.

In diesem speziellen Fall bedeutet dies, dass man so etwas tun kann:

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

Das Ergebnis ist (mit beispielsweise 100 Gesamtergebnissen):

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

13
Dies funktioniert hervorragend, ab 3.4 sollte dies die akzeptierte Antwort sein
Adam Reis

Um so ein Array-Ergebnis in ein einfaches Zwei-Feld-Objekt umzuwandeln, brauche ich ein anderes $project?
SerG

1
Dies muss nun die akzeptierte Antwort sein. arbeitete wie Charme.
Arootin Aghazaryan

8
Dies sollte heute die akzeptierte Antwort sein. Bei der Verwendung von Paging mit $ facet wurden jedoch Leistungsprobleme festgestellt. Die andere Antwort hat ebenfalls Leistungsprobleme mit $ Slice. Ich fand es besser, $ überspringen und $ begrenzen in der Pipeline und einen separaten Aufruf zur Zählung zu tätigen. Ich habe dies gegen ziemlich große Datenmengen getestet.
Jpepper

57

Verwenden Sie diese Option, um die Gesamtzahl in der resultierenden Sammlung zu ermitteln.

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

3
Vielen Dank. Ich habe jedoch "Ansichten" in meiner Codierung verwendet, um die Anzahl der entsprechenden Gruppen zu ermitteln (dh Gruppe 1 => 2 Datensätze, Gruppe 3 => 5 Datensätze usw.). Ich möchte die Anzahl der Datensätze erhalten (dh insgesamt: 120 Datensätze). Hoffe du hast verstanden ..
user2987836

32

Sie können die toArray-Funktion verwenden und dann ihre Länge für die Gesamtzahl der Datensätze abrufen.

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

1
Obwohl dies möglicherweise nicht als "richtige" Lösung funktioniert, hat es mir beim Debuggen geholfen - es funktioniert, auch wenn es keine 100% ige Lösung ist.
Johann Marx

3
Dies ist keine echte Lösung.
Furkan Başaran

1
TypeError: Parent.aggregate(...).toArray is not a functionDies ist der Fehler, den ich bei dieser Lösung gegeben habe.
Mohammad Hossein Shojaeinia

Vielen Dank. Das habe ich gesucht.
skvp

Dadurch werden alle aggregierten Daten abgerufen und die Länge dieses Arrays zurückgegeben. keine gute Praxis. Stattdessen können Sie {$ count: 'count'} in die Aggregationspipeline einfügen
Aslam Shaik

18

Verwenden Sie die Pipeline-Phase $ count Aggregation , um die Gesamtanzahl der Dokumente abzurufen:

Abfrage:

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

Ergebnis:

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

Das funktioniert wie ein Zauber, aber was die Leistung betrifft, ist es gut?
Ana.arede

Saubere Lösung. Danke
skvp

13

Ich habe es so gemacht:

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

Das Aggregat gibt das Array zurück, also schleifen Sie es einfach und erhalten Sie den endgültigen Index.

Und eine andere Möglichkeit ist:

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 Sie brauchen weder die varErklärung noch den mapAnruf. Die ersten 3 Zeilen Ihres ersten Beispiels sind ausreichend.
Madbreaks

6

Die von @Divergent bereitgestellte Lösung funktioniert, aber meiner Erfahrung nach ist es besser, zwei Abfragen zu haben:

  1. Zuerst zum Filtern und dann zum Gruppieren nach ID, um die Anzahl der gefilterten Elemente zu erhalten. Filtern Sie hier nicht, es ist unnötig.
  2. Zweite Abfrage, die filtert, sortiert und paginiert.

Die Lösung durch Drücken von $$ ROOT und Verwenden von $ Slice führt zu einer Dokumentenspeicherbeschränkung von 16 MB für große Sammlungen. Bei großen Sammlungen scheinen zwei Abfragen zusammen schneller zu laufen als die mit $$ ROOT Pushing. Sie können sie auch parallel ausführen, sodass Sie nur durch die langsamere der beiden Abfragen (wahrscheinlich die sortierte) eingeschränkt sind.

Ich habe mich mit dieser Lösung unter Verwendung von 2 Abfragen und einem Aggregationsframework abgefunden (Hinweis - in diesem Beispiel verwende ich node.js, aber die Idee ist dieselbe):

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
Es wird normalerweise empfohlen, erklärenden Text zusammen mit einer Code-Antwort beizufügen.

3

Dies kann für mehrere Spielbedingungen funktionieren

            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

Ich brauchte die absolute Gesamtzahl nach dem Anwenden der Aggregation. Das hat bei mir funktioniert:

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

Ergebnis:

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

2

Hier sind einige Möglichkeiten, um die Gesamtzahl der Datensätze während der MongoDB-Aggregation zu ermitteln:


  • Verwenden von $count:

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

    Für 1000 Datensätze dauert dies durchschnittlich 2 ms und ist der schnellste Weg.


  • Verwenden von .toArray():

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

    Für 1000 Datensätze dauert dies durchschnittlich 18 ms.


  • Verwenden von .itcount():

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

    Für 1000 Datensätze dauert dies durchschnittlich 14 ms.



0

Wenn Sie nicht gruppieren möchten, verwenden Sie die folgende Methode:

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


Ich denke, die Person, die die Frage stellt, möchte sich nach dem Thema gruppieren.
Mjaggard
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.