使用limit时,如何获取MongoDB中文档的总数?

81

我对使用MongoDB优化“分页”解决方案很感兴趣。我的问题很简单。通常,我使用limit()功能限制返回的文档数量。这迫使我发出一个没有limit()函数的多余查询,以便我也可以捕获查询中文档的总数,以便我可以将其传递给客户端,让他们知道他们需要发出额外的请求来检索其余文档。

是否有一种方法可以将此压缩为1个查询?同时获取文档的总数,但只检索使用limit()的子集?是否有一种不同于我处理该问题的方式?


我曾经遇到过这种情况,并撰写了一篇文章,供其他人在此处使用:https://beingnin.medium.com/implement-server-side-pagination-in-mongodb-with-total-count-cfbb11b5c956 - Beingnin
16个回答

97

Mongodb 3.4 推出了聚合操作符 $facet

它可以在同一组输入文档上,在单个阶段中处理多个聚合管道。

使用 $facet$group 操作符,您可以通过$limit找到文档并获取总数。

您可以在 mongodb 3.4 中使用以下聚合操作:

db.collection.aggregate([
  { "$facet": {
    "totalData": [
      { "$match": { }},
      { "$skip": 10 },
      { "$limit": 10 }
    ],
    "totalCount": [
      { "$group": {
        "_id": null,
        "count": { "$sum": 1 }
      }}
    ]
  }}
])

即使您可以在MongoDB 3.6中使用已经介绍的$count聚合函数。

您可以在MongoDB 3.6 中使用以下聚合函数。

db.collection.aggregate([
  { "$facet": {
    "totalData": [
      { "$match": { }},
      { "$skip": 10 },
      { "$limit": 10 }
    ],
    "totalCount": [
      { "$count": "count" }
    ]
  }}
])

请查看以下实现:https://beingnin.medium.com/implement-server-side-pagination-in-mongodb-with-total-count-cfbb11b5c956 - Beingnin
7
如果您想在 $match 操作发生后获得数据的总计数,则将 $match 操作放在 $facet 操作之前可以实现此目的。 - Hedley Smith
3
这个答案解释了同样的事情,但更清晰易懂。 - teuber789
这个答案能够正确返回总计数,但是它没有返回totalData的总数。 - Rigin Oommen
1
关于性能:$facet 阶段及其子流水线无法利用索引,即使其子流水线使用 $match 或 $facet 是管道中的第一个阶段也是如此。$facet 阶段在执行过程中始终会执行 COLLSCAN。 - Prisacari Dmitrii

23

,没有其他方法。需要两次查询 - 一次查询总数,一次带有限制。否则您需要使用不同的数据库,例如Apache Solr,它可以按照您的需求正常工作。在Solr中,每个查询都有限制返回totalCount。


9
现在我们有了mongoDB 3.4版本,我不确定“否”是否仍然是正确的答案。请参阅 https://dev59.com/vGIj5IYBdhLWcg3wWj-d#39784851 - Felipe
1
有多种方法可以解决这个问题,我也一直在寻找解决方案。您可以创建一个聚合操作来返回总计数以及根据条件返回完整文档。您还可以基于条件执行一个findAll操作,存储该数组的长度,然后根据您的限制/偏移值切片。 这两个选项都只需要一个对数据库的调用。聚合操作的开销取决于其复杂性,与返回数组上运行的切片相同。您对此有何想法? - Sam Gruse
这个答案怎么样?https://dev59.com/9WEh5IYBdhLWcg3w_Xr5#56693959 对我来说似乎有效。与限制100个文档的聚合相比,平均运行速度甚至稍微快一些(约2-3毫秒)... - sznrbrt
可以使用子管道(facet sub pipelines)通过一个查询完成,然而,这种解决方案的缺点是$facet阶段非常慢,即使其中使用了匹配(match),它也无法使用索引,这种差异在1000万个文档中可以注意到。因此,最好还是使用多个单独的查询,而不仅仅是一个。 - rasfuranku

19

MongoDB允许您在使用limit()skip()时仍然使用cursor.count()

假设您有一个包含10个项目的db.collection

您可以执行以下操作:

async function getQuery() {
  let query = await db.collection.find({}).skip(5).limit(5); // returns last 5 items in db
  let countTotal = await query.count() // returns 10-- will not take `skip` or `limit` into consideration
  let countWithConstraints = await query.count(true) // returns 5 -- will take into consideration `skip` and `limit`
  return { query, countTotal } 
}

聚合函数怎么样? - Mahmoud Heretani
4
对我来说最好的选择,我讨厌聚合^^。我觉得这种方法更简单易懂。 - TOPKAT
3
.skip(5).limit(5) 不会返回数据库中的最后5个项目,而是返回第二组5个项目。count()将始终返回10,无论有多少项目,只要至少有10个项目。 - Walter Tross
为什么countTotal和CountWithConstraints需要等待一个Promise? - Ricky-U
2
Mongo 4.4版本和mongo node客户端4版本不显示总项目数。 - Amit Kumar

17

以下是使用 MongoDB 3.4+(配合 Mongoose)和 $facets 完成此操作的步骤。该示例基于匹配后的文档返回 $count

const facetedPipeline = [{
    "$match": { "dateCreated": { $gte: new Date('2021-01-01') } },
    "$project": { 'exclude.some.field': 0 },
  },
  {
    "$facet": {
      "data": [
        { "$skip": 10 },
        { "$limit": 10 }
      ],
      "pagination": [
        { "$count": "total" }
      ]
    }
  }
];

const results = await Model.aggregate(facetedPipeline);

这种模式对于从 REST API 返回分页信息很有用。

参考:MongoDB $facet


请注意,在管道中首先进行匹配,然后再进行分面操作,这样您就能够命中索引。您无法从 $facet 命中索引。 - Willem van der Veen

12

时代变了,我相信您可以通过使用聚合、$sort$group$project来实现OP所要求的内容。对于我的系统,我还需要从我的users集合中获取一些用户信息。希望这也能回答任何关于那方面的问题。下面是一个聚合管道。最后三个对象(sort、group和project)负责获取总计数,然后提供分页功能。

db.posts.aggregate([
  { $match: { public: true },
  { $lookup: {
    from: 'users',
    localField: 'userId',
    foreignField: 'userId',
    as: 'userInfo'
  } },
  { $project: {
    postId: 1,
    title: 1,
    description: 1
    updated: 1,
    userInfo: {
      $let: {
        vars: {
          firstUser: {
            $arrayElemAt: ['$userInfo', 0]
          }
        },
        in: {
          username: '$$firstUser.username'
        }
      }
    }
  } },
  { $sort: { updated: -1 } },
  { $group: {
    _id: null,
    postCount: { $sum: 1 },
    posts: {
      $push: '$$ROOT'
    }
  } },
  { $project: {
    _id: 0,
    postCount: 1,
    posts: {
      $slice: [
        '$posts',
        currentPage ? (currentPage - 1) * RESULTS_PER_PAGE : 0,
        RESULTS_PER_PAGE
      ]
    }
  } }
])

这个查询会返回什么响应?它会返回计数和结果吗? - Kumar
1
@Kumar 是的,在 $group 中使用 $sum 计算计数,数组结果来自 $push。您可以在 $project 中看到我包括了帖子计数(postCount),然后仅使用 $slice 从结果数组中取出一个部分。最终响应返回总帖子数以及其中一部分供分页使用。 - TestWell

12

Mongodb 3.4中有一种方法:$facet

您可以使用它来实现以下操作:

db.collection.aggregate([
  {
    $facet: {
      data: [{ $match: {} }],
      total: { $count: 'total' }
    }
  }
])

那么您将能够同时运行两个聚合操作。


9
只是一个小更新,总数应该是一个数组,例如:total: [{ $count: 'total' }]。 - Sunil Pachlangia
无法与$sort阶段一起工作,得到意外的输出。只有在使用$facet时才存在问题。 - Alok Deshwal

9

默认情况下,count()方法会忽略游标的skip()和limit()效果。(参考MongoDB文档

由于count方法排除了limit和skip的影响,您可以使用cursor.count()来获取总数。

 const cursor = await database.collection(collectionName).find(query).skip(offset).limit(limit)
 return {
    data: await cursor.toArray(),
    count: await cursor.count() // this will give count of all the documents before .skip() and limit()
 };

自2023年起已被弃用 - undefined

5
这完全取决于你需要的分页体验,你是否需要进行两次查询。您需要列出每一页甚至一系列页面吗?实际上,有人会去第1051页吗?关于分页模式有很多UX - 避免分页的痛苦涵盖了各种分页类型及其场景,并且许多不需要计数查询就可以知道是否有下一页。例如,如果您在一页上显示10个项目并限制为13个,则会知道是否还有另一页。

4

MongoDB引入了一种新的方法,仅获取与给定查询匹配的文档计数,具体如下:

const result = await db.collection('foo').count({name: 'bar'});
console.log('result:', result) // prints the matching doc count

分页使用的配方:

const query = {name: 'bar'};
const skip = (pageNo - 1) * pageSize; // assuming pageNo starts from 1
const limit = pageSize;

const [listResult, countResult] = await Promise.all([
  db.collection('foo')
    .find(query)
    .skip(skip)
    .limit(limit),

  db.collection('foo').count(query)
])

return {
  totalCount: countResult,
  list: listResult
}

了解更多关于db.collection.count的详细信息,请访问此页面


1

在使用聚合进行分页时,建议提供一个警告。如果API经常被用户用于获取数据,则最好使用两个查询。当更多的用户在线访问系统时,这比在生产服务器上使用聚合获取数据至少快50倍。聚合和$facet更适合仪表板、报告和调度作业等不经常调用的场景。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接