Agregacja MongoDB: Jak uzyskać całkowitą liczbę rekordów?


105

Użyłem agregacji do pobierania rekordów z 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),
));

Jeśli wykonam to zapytanie bez ograniczeń, zostanie pobranych 10 rekordów. Ale chcę zachować limit na 2. Więc chciałbym uzyskać łączną liczbę rekordów. Jak mogę zrobić z agregacją? Proszę, poradź mi. Dzięki


Jak wyglądałyby wyniki, gdyby były tylko 2?
WiredPrairie

Spójrz na $ facet To może pomóc stackoverflow.com/questions/61812361/…
Soham

Odpowiedzi:


102

Jest to jedno z najczęściej zadawanych pytań, aby uzyskać wynik podzielony na strony i całkowitą liczbę wyników jednocześnie w jednym zapytaniu. Nie potrafię wyjaśnić, jak się czułem, kiedy w końcu to osiągnąłem 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 ) ) ) )
))

Wynik będzie wyglądał mniej więcej tak:

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

8
Dokumentacja na ten temat: docs.mongodb.com/v3.2/reference/operator/aggregation/group/ ... ... zwróć uwagę, że przy takim podejściu cały zestaw wyników nie podzielonych na strony musi mieścić się w 16 MB.
btown

8
To jest czyste złoto! Szedłem przez piekło, próbując sprawić, by to zadziałało.
Henrique Miranda

4
Dzięki! Po prostu potrzebuję { $group: { _id: null, count: { $sum:1 }, result: { $push: '$$ROOT' }}}(wstaw po {$group:{}}zliczeniu całkowitego znalezienia.
Liberateur

1
Jak zastosować ograniczenie do zestawu wyników? Wyniki to teraz tablica zagnieżdżona
valen

2
Moje życie jest teraz kompletne, mogę umrzeć szczęśliwy
Jack

89

Od wersji 3.4 (tak mi się wydaje) MongoDB ma teraz nowego operatora potoku agregacji o nazwie „ facet ”, który własnymi słowami:

Przetwarza wiele potoków agregacji w ramach jednego etapu na tym samym zestawie dokumentów wejściowych. Każdy pod-potok ma własne pole w dokumencie wyjściowym, w którym jego wyniki są przechowywane jako tablica dokumentów.

W tym konkretnym przypadku oznacza to, że można zrobić coś takiego:

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

Wynik będzie (z wynikiem ex 100):

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

13
To działa świetnie, od 3.4 to powinna być akceptowana odpowiedź
Adam Reis

Aby przekonwertować tak bogaty wynik w prosty obiekt z dwoma polami, potrzebuję innego $project?
SerG

1
teraz musi to być zaakceptowana odpowiedź. działał jak urok.
Arootin Aghazaryan

9
Taka powinna być dzisiaj akceptowana odpowiedź. Jednak napotkałem problemy z wydajnością podczas korzystania ze stronicowania z $ facet. Druga głosowana odpowiedź również ma problemy z wydajnością w przypadku plasterka $. Okazało się, że lepiej jest pominąć $ skip i $ limit w potoku i wykonać oddzielne wezwanie do liczenia. Przetestowałem to na dość dużych zestawach danych.
Jpepper

59

Użyj tego, aby znaleźć całkowitą liczbę w wynikowym zbiorze.

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

3
Dzięki. Ale użyłem „widoków” w moim kodowaniu, aby uzyskać liczbę odpowiednich grup (tj. Grupa 1 => 2 rekordy, grupa 3 => 5 rekordów itd.). Chcę uzyskać liczbę rekordów (tj. Łącznie: 120 rekordów). Mam nadzieję, że zrozumiałeś ...
user2987836

36

Możesz użyć funkcji toArray, a następnie uzyskać jej długość dla łącznej liczby rekordów.

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

1
Chociaż może to nie działać jako „właściwe” rozwiązanie, pomogło mi to debugować - działa, nawet jeśli nie jest w 100% rozwiązaniem.
Johann Marx

3
To nie jest prawdziwe rozwiązanie.
Furkan Başaran

1
TypeError: Parent.aggregate(...).toArray is not a functionto jest błąd, który podałem przy tym rozwiązaniu.
Mohammad Hossein Shojaeinia

Dzięki. To jest to, czego szukałem.
skvp

Spowoduje to pobranie wszystkich zagregowanych danych, a następnie zwróci długość tej tablicy. nie jest dobrą praktyką. zamiast tego możesz dodać {$ count: 'count'} do potoku agregacji
Aslam Shaik

21

Użyj etapu potoku agregacji $ count, aby uzyskać łączną liczbę dokumentów:

Zapytanie:

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

Wynik:

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

To działa jak urok, ale czy pod względem wydajności jest dobre?
ana.arede,

Czyste rozwiązanie. Dzięki
skvp

13

Zrobiłem to w ten sposób:

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

Agregat zwróci tablicę, więc po prostu zapętl ją i uzyskaj ostateczny indeks.

Innym sposobem na to jest:

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 nie potrzebujesz vardeklaracji ani mapwezwania. Wystarczą pierwsze 3 wiersze pierwszego przykładu.
Madbreaks

7

Rozwiązanie dostarczone przez @Divergent działa, ale z mojego doświadczenia wynika, że ​​lepiej mieć 2 zapytania:

  1. Najpierw do filtrowania, a następnie grupowania według identyfikatora w celu uzyskania liczby przefiltrowanych elementów. Nie filtruj tutaj, jest to niepotrzebne.
  2. Drugie zapytanie, które filtruje, sortuje i paginuje.

Rozwiązanie z wypychaniem $$ ROOT i użyciem $ slice powoduje ograniczenie pamięci dokumentów do 16 MB dla dużych zbiorów. Ponadto w przypadku dużych kolekcji dwa zapytania razem wydają się działać szybciej niż to z wypychaniem $$ ROOT. Możesz je również uruchamiać równolegle, więc ogranicza Cię tylko wolniejsze z dwóch zapytań (prawdopodobnie to, które sortuje).

Rozstrzygnąłem się z tym rozwiązaniem za pomocą 2 zapytań i frameworka agregacji (uwaga - w tym przykładzie używam node.js, ale pomysł jest ten sam):

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
Zwykle dobrą praktyką jest dołączenie tekstu wyjaśniającego wraz z odpowiedzią kodową.

3

Może to działać w przypadku wielu warunków dopasowania

            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

Po zastosowaniu agregacji potrzebowałem całkowitej liczby całkowitej. To zadziałało dla mnie:

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

Wynik:

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

2

Oto kilka sposobów na uzyskanie całkowitej liczby rekordów podczas agregacji MongoDB:


  • Używając $count:

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

    Uzyskanie 1000 rekordów zajmuje średnio 2 ms i jest najszybszym sposobem.


  • Używając .toArray():

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

    Uzyskanie 1000 rekordów zajmuje średnio 18 ms.


  • Używając .itcount():

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

    Uzyskanie 1000 rekordów zajmuje średnio 14 ms.



0

Jeśli nie chcesz grupować, użyj następującej metody:

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


Myślę, że osoba zadająca pytanie chce grupować się na podstawie tematu.
mjaggard
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.