MongoDB Aggregation: как получить общее количество записей?


105

Я использовал агрегацию для получения записей из 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/…
Сохам,

Ответы:


104

Это один из наиболее часто задаваемых вопросов для получения результатов с разбивкой на страницы и общего количества результатов одновременно в одном запросе. Я не могу объяснить, что я чувствовал, когда наконец достиг этого 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 МБ.
btown

8
Это чистое золото! Я прошел через ад, пытаясь заставить эту работу работать.
Энрике Миранда

4
Спасибо, парень! Мне нужно { $group: { _id: null, count: { $sum:1 }, result: { $push: '$$ROOT' }}}(вставить после, {$group:{}}чтобы подсчитать общее количество
Liberateur,

1
Как применить ограничение к набору результатов? Результаты теперь представляют собой вложенный массив
valen

2
Моя жизнь завершена, я могу умереть счастливым
Джек

91

Начиная с версии 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 в конвейере и сделать отдельный вызов для count. Я проверил это на довольно больших наборах данных.
Jpepper

59

Используйте это, чтобы найти общее количество в результирующей коллекции.

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

3
Спасибо. Но я использовал «представления» в моем кодировании, чтобы получить количество соответствующих групп (т. Е. Группа 1 => 2 записи, группа 3 => 5 записей и т. Д.). Я хочу получить количество записей (то есть всего: 120 записей). Надеюсь, вы поняли ..
user2987836 03

37

Вы можете использовать функцию 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'} в конвейер агрегации
Аслам Шайк

21

Используйте этап конвейера агрегирования $ 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звонок. Достаточно первых трех строк вашего первого примера.
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 03

1
я столкнулся с похожей ситуацией. Не было ответа, кроме как выполнить 2 запроса. :( stackoverflow.com/questions/20113731/…
astroanu 03

0

Если вы не хотите группироваться, используйте следующий метод:

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


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