【问题标题】:Get a count of total documents with MongoDB when using limit使用限制时使用 MongoDB 获取总文档数
【发布时间】:2014-02-15 20:33:31
【问题描述】:

我有兴趣优化我正在使用 MongoDB 开发的“分页”解决方案。我的问题很简单。我通常使用limit() 功能限制返回的文档数量。这迫使我在没有 limit() 函数的情况下发出冗余查询,以便我还可以捕获查询中的文档总数,以便我可以将其传递给客户端,让他们知道他们必须发出额外的请求(s) 检索其余文档。

有没有办法将其压缩为 1 个查询?获取文档总数,但同时只使用limit()检索一个子集?有没有与我正在处理的问题不同的方式来思考这个问题?

【问题讨论】:

标签: mongodb pagination mongodb-query aggregation-framework casbah


【解决方案1】:

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" }
    ]
  }}
])

【讨论】:

  • 如果您希望在 $match 发生后获取数据的总计数,请将 $match 放在 $facet 实现此目的之前。
  • This answer 解释了同样的事情,但更清晰。
  • 这个答案正确返回总计数,但未能返回总数据
【解决方案2】:

不行,没有别的办法。两个查询 - 一个用于计数 - 一个有限制。或者您必须使用不同的数据库。例如 Apache Solr 就像你想要的那样工作。那里的每个查询都是有限的并且返回 totalCount。

【讨论】:

  • 现在我们有了 mongoDb 3.4,我不确定“否”是否是完全正确的答案。见stackoverflow.com/a/39784851/3654061
  • 有多种方法可以做到这一点,因为我自己一直在寻找解决方案。您可以创建聚合操作以根据条件返回总计数和完整文档。你也可以根据条件做一次 findAll 。存储该数组的长度。然后根据您的限制/偏移值切出值。这两个选项都只是对数据库的一次调用。聚合的费用取决于它的复杂程度,与在返回数组上运行的切片相同。对此有何想法?
  • 这个答案怎么样? stackoverflow.com/a/56693959 对我来说似乎有效。与限制为 100 个文档的聚合相比,对我来说,在 avg 上的运行速度甚至稍微快一些(~2-3ms)......
【解决方案3】:

MongoDB 允许您使用cursor.count(),即使您传递了limit()skip()

假设您有一个db.collection,其中包含 10 个项目。

你可以这样做:

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 } 
}

【讨论】:

  • 聚合怎么样?
  • 对我来说最好的,我讨厌聚合^^。我发现这种方式更简单易读。
  • .skip(5).limit(5) 不返回数据库中的最后 5 个项目。它返回第二组 5 个项目。 count() 将始终返回 10 无论有多少项目,只要至少有 10 个。
  • 为什么 countTotal 和 CountWithConstraints 等待承诺?
  • Mongo 4.4 版和 mongo 节点客户端 4 版不显示总项目数。
【解决方案4】:

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

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 是的,计数是在 $group 期间使用 $sum 计算的,数组结果来自 $push。您可以在 $project 中看到我包含了帖子计数 (postCount),然后使用 $slice 从结果数组中仅获取一个部分。最终响应返回总帖子的数量以及其中的一部分用于分页。
【解决方案5】:

Mongodb 3.4 中有一个方法:$facet

你可以的

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

那么你将能够同时运行两个聚合

【讨论】:

  • 只是一点点更新总数应该是一个数组,如 total: [{ $count: 'total' }]
  • 不使用 $sort 阶段,得到意外的输出。只有使用 $facet 时才会出现问题。
【解决方案6】:

以下是使用$facets 使用MongoDB 3.4+(使用Mongoose)执行此操作的方法。此示例根据匹配后的文档返回$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

【讨论】:

    【解决方案7】:

    默认情况下,count() 方法会忽略 cursor.skip() 和 cursor.limit() (MongoDB docs)

    由于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()
     };
    

    【讨论】:

      【解决方案8】:

      这完全取决于您是否需要执行两个查询所需的分页体验。

      您是否需要列出每一页甚至一系列页面?有没有人甚至转到第 1051 页 - 从概念上讲,这实际上是什么意思?

      有很多关于分页模式的 UX - Avoid the pains of pagination 涵盖了各种类型的分页及其场景,许多不需要计数查询就可以知道是否有下一页。例如,如果您在一个页面上显示 10 个项目并且您限制为 13 个 - 您将知道是否还有另一个页面..

      【讨论】:

        【解决方案9】:

        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 的更多详细信息,请访问this page

        【讨论】:

          【解决方案10】:

          可以使用count() 在没有limit() 的影响的情况下获得总结果大小,如下所示: Limiting results in MongoDB but still getting the full count?

          根据文档,您甚至可以控制在调用count() 时是否考虑限制/分页: https://docs.mongodb.com/manual/reference/method/cursor.count/#cursor.count

          编辑:与其他地方写的相反 - 文档明确指出 “该操作不执行查询,而是计算查询返回的结果”。根据我的理解,这意味着只执行一个查询。

          例子:

          > db.createCollection("test")
          { "ok" : 1 }
          
          > db.test.insert([{name: "first"}, {name: "second"}, {name: "third"}, 
          {name: "forth"}, {name: "fifth"}])
          BulkWriteResult({
              "writeErrors" : [ ],
              "writeConcernErrors" : [ ],
              "nInserted" : 5,
              "nUpserted" : 0,
              "nMatched" : 0,
              "nModified" : 0,
              "nRemoved" : 0,
              "upserted" : [ ]
          })
          
          > db.test.find()
          { "_id" : ObjectId("58ff00918f5e60ff211521c5"), "name" : "first" }
          { "_id" : ObjectId("58ff00918f5e60ff211521c6"), "name" : "second" }
          { "_id" : ObjectId("58ff00918f5e60ff211521c7"), "name" : "third" }
          { "_id" : ObjectId("58ff00918f5e60ff211521c8"), "name" : "forth" }
          { "_id" : ObjectId("58ff00918f5e60ff211521c9"), "name" : "fifth" }
          
          > db.test.count()
          5
          
          > var result = db.test.find().limit(3)
          > result
          { "_id" : ObjectId("58ff00918f5e60ff211521c5"), "name" : "first" }
          { "_id" : ObjectId("58ff00918f5e60ff211521c6"), "name" : "second" }
          { "_id" : ObjectId("58ff00918f5e60ff211521c7"), "name" : "third" }
          
          > result.count()
          5 (total result size of the query without limit)
          
          > result.count(1)
          3 (result size with limit(3) taken into account)
          

          【讨论】:

          • 如果您投反对票,请添加一个理由,以便我有机会理解 - 这也可能会改善未来的答案!
          • 我不确定是否投反对票,但仅供参考:count() 仅适用于 find(),因此对 aggregate 查询没有帮助
          【解决方案11】:

          尝试如下:

          cursor.count(false, function(err, total){ console.log("total", total) })

          core.db.users.find(query, {}, {skip:0, limit:1}, function(err, cursor){
              if(err)
                  return callback(err);
          
              cursor.toArray(function(err, items){
                  if(err)
                      return callback(err);
          
                  cursor.count(false, function(err, total){
                      if(err)
                          return callback(err);
          
                      console.log("cursor", total)
          
                      callback(null, {items: items, total:total})
                  })
              })
           })
          

          【讨论】:

            【解决方案12】:

            考虑在分页使用聚合时提供警告。如果用户经常使用 API 来获取数据,最好为此使用两个查询。当更多用户在线访问系统时,这比在生产服务器上使用聚合获取数据至少快 50 倍。聚合和 $facet 更适合调用频率较低的 Dashboard 、报告和 cron 作业。

            【讨论】:

              【解决方案13】:

              我们可以使用 2 个查询来做到这一点。

                  const limit = parseInt(req.query.limit || 50, 10);
                  let page = parseInt(req.query.page || 0, 10);
                  if (page > 0) { page = page - 1}
              
                  let doc = await req.db.collection('bookings').find().sort( { _id: -1 }).skip(page).limit(limit).toArray();
                  let count = await req.db.collection('bookings').find().count();
                  res.json({data: [...doc], count: count});
              

              【讨论】:

                【解决方案14】:

                您可以在一个查询中完成此操作。首先你运行一个计数并在其中运行 limit() 函数。

                在 Node.js 和 Express.js 中,您必须像这样使用它才能将“count”函数与 toArray 的“result”一起使用。

                var curFind = db.collection('tasks').find({query});
                

                然后你可以像这样在它之后运行两个函数(一个嵌套在另一个中)

                curFind.count(function (e, count) {
                
                // Use count here
                
                    curFind.skip(0).limit(10).toArray(function(err, result) {
                
                    // Use result here and count here
                
                    });
                
                });
                

                【讨论】:

                • 这是不正确的方法。您只是在所有文档中查找,而不是在每个请求中的前 10 个文档中查找。对于每个请求,每次您只是在整个文档中查找。不在前 10 个。
                • 感谢您的评论。当时这是我们提出的解决方案。就效率而言,它可能并不完美。建议一个即兴发挥的解决方案。
                猜你喜欢
                • 2022-09-24
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多