Агрегація MongoDB: Як отримати загальну кількість записів?


102

Я використовував агрегацію для отримання записів з 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),
));

Якщо я виконаю цей запит без обмеження, буде отримано 10 записів. Але я хочу зберегти ліміт як 2. Тому я хотів би отримати загальну кількість записів. Як я можу робити з агрегацією? Будь ласка, порадьте мене. Дякую


Як би виглядали результати, якби їх було лише 2?
WiredPrairie

Погляньте на $ facet Це може допомогти stackoverflow.com/questions/61812361/…
Сохам

Відповіді:


100

Це одне з найпоширеніших запитань для отримання пагінованого результату та загальної кількості результатів одночасно в одному запиті. Я не можу пояснити, що я відчував, коли нарешті досяг цього 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 ) ) ) )
))

Результат буде виглядати приблизно так:

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

8
Документація щодо цього: docs.mongodb.com/v3.2/reference/operator/aggregation/group/… ... зауважте, що при такому підході весь набір результатів, які не мають сторінок, повинен містити 16 МБ.
місто

7
Це чисте золото! Я їхав крізь пекло, намагаючись змусити цю роботу.
Генріке Міранда,

4
Дякую хлопце! Мені просто потрібно { $group: { _id: null, count: { $sum:1 }, result: { $push: '$$ROOT' }}}(вставити після {$group:{}}для підрахунку загальної знахідки.
Liberateur

1
Як застосувати обмеження до набору результатів? Результати тепер є вкладеним масивом
valen

@valen Ви можете побачити останній рядок коду "'results' => array ('$ slice' => array ('$ results', $ skip, $ length))" Тут ви можете застосувати обмеження та пропустити параметри
Anurag pareek

83

Починаючи з версії 3.4 (на мою думку), MongoDB тепер має нового оператора конвеєрного зведення під назвою ' грань ', який, власними словами:

Обробляє кілька конвеєрів агрегування на одному етапі на одному наборі вхідних документів. Кожен підпровід має своє власне поле у ​​вихідному документі, де його результати зберігаються як масив документів.

У цьому конкретному випадку це означає, що можна зробити щось подібне:

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

Результат буде (з попередніми 100 результатами):

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

13
Це чудово працює, станом на 3.4 це має бути прийнятою відповіддю
Адам Рейс

Щоб перетворити такий масивний результат у простий об'єкт із двох полів, мені потрібен інший $project?
SerG

1
тепер це має бути прийнятою відповіддю. працював як шарм.
Ароотін Агазарян

9
Це має бути прийнятою відповіддю сьогодні. Однак я виявив проблеми з продуктивністю при використанні підкачки з $ facet. Інша відповідь, яка проголосувала, також має проблеми з продуктивністю $ slice. Я вважаю, що краще конфігурувати $ skip і $ limit і зробити окремий виклик для підрахунку. Я перевірив це на досить великих наборах даних.
Jpepper

59

Використовуйте це, щоб знайти загальну кількість в отриманій колекції.

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

3
Дякую. Але я використовував "подання" у своєму кодуванні, щоб отримати кількість відповідних груп (тобто група 1 => 2 записи, група 3 => 5 записів тощо). Я хочу отримати кількість записів (тобто, загалом: 120 записів). Сподіваюся, ви зрозуміли ..
user2987836

34

Ви можете використовувати функцію toArray, а потім отримати її довжину для загальної кількості записів.

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

1
Хоча це може не працювати як "правильне" рішення, воно допомогло мені щось налагодити - воно працює, навіть якщо це не 100% рішення.
Йоганн Маркс,

3
Це не справжнє рішення.
Furkan Başaran

1
TypeError: Parent.aggregate(...).toArray is not a functionце помилка, яку я дав із цим рішенням.
Mohammad Hossein Shojaeinia,

Дякую. Це те, що я шукав.
skvp

Це отримає всі агреговані дані, а потім поверне довжину цього масиву. погана практика. замість цього ви можете додати {$ count: 'count'} в конвеєр агрегації
Аслам Шаїк,

19

Скористайтеся етапом конвеєру агрегування $ count, щоб отримати загальну кількість документів:

Запит:

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

Результат:

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

Це працює так само, як шарм, але наскільки це ефективно - це добре?
ana.arede

Чистий розчин. Дякую
skvp

13

Я зробив це так:

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

Сукупність поверне масив, тому просто циклічно його отримайте і отримайте остаточний індекс.

І інший спосіб зробити це:

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 Вам не потрібна varдекларація та mapдзвінок. Перших 3 рядків вашого першого прикладу достатньо.
Madbreaks,

7

Рішення, надане @Divergent, справді працює, але, на мій досвід, краще мати 2 запити:

  1. Спочатку для фільтрації, а потім групування за ідентифікатором, щоб отримати кількість відфільтрованих елементів. Тут не фільтруйте, це непотрібно.
  2. Другий запит, який фільтрує, сортує та розподіляє сторінки.

Рішення з натисканням $$ ROOT та використанням $ slice натрапляє на обмеження пам’яті документа на 16 МБ для великих колекцій. Крім того, для великих колекцій два запити разом, здається, працюють швидше, ніж той, що натискає $$ ROOT. Ви також можете запускати їх паралельно, тому вас обмежує лише повільніший із двох запитів (можливо, той, який сортується).

Я вирішив це рішення, використовуючи 2 запити та структуру агрегування (зауважте - я використовую node.js у цьому прикладі, але ідея однакова):

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
Зазвичай є гарною практикою включати пояснювальний текст разом із кодовою відповіддю.

3

Це може працювати в умовах кількох матчів

            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

Мені знадобився абсолютний загальний підрахунок після застосування агрегування. Це працювало для мене:

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

Результат:

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

2

Ось кілька способів отримати загальну кількість записів під час виконання агрегації MongoDB:


  • Використання $count:

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

    Для отримання 1000 записів це займає в середньому 2 мс і це найшвидший спосіб.


  • Використання .toArray():

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

    Для отримання 1000 записів це займає в середньому 18 мс.


  • Використання .itcount():

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

    Для отримання 1000 записів це займає в середньому 14 мс.


0

Вибачте, але я думаю, вам потрібні два запити. Один для загальної кількості переглядів, а інший для згрупованих записів.

Ця відповідь може бути вам корисною


Дякую .. я так думаю .. Але, немає варіанту з агрегацією .. :(
user2987836

1
я зіткнувся з подібною ситуацією. Не було відповіді, окрім як зробити 2 запити. :( stackoverflow.com/questions/20113731 / ...
astroanu

0

Якщо ви не хочете групуватися, скористайтеся наступним методом:

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


Я думаю, що той, хто задає запитання, дійсно хоче згрупуватися, виходячи з теми.
mjaggard
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.