必须用 countDocuments() 获取真实总数,因其支持查询条件和索引;游标分页优于 skip(),避免全量扫描;$facet 可一次返回数据与总数,但性能取决于索引优化。

用 countDocuments() 获取总记录数,别用 estimatedDocumentCount()
分页时想显示“共 XX 条”,必须查真实总数,estimatedDocumentCount() 返回的是集合元数据里的近似值,不走查询条件,也不响应 $match、$text 等过滤逻辑,结果常严重不准。真实分页场景下必须用 countDocuments()。
它和 find() 共享同一套 filter 和 index,能保证总数与分页数据严格一致。但注意:它会触发全量扫描(除非有覆盖索引),大数据量时可能慢——这不是 bug,是设计使然。
- 带复杂查询条件(如嵌套字段、正则、数组操作)时,务必在对应字段建好复合索引,否则
countDocuments()会退化为 collection scan - 如果只是按 _id 分页且数据量极大(千万级以上),可考虑用
_id范围估算总数 + 游标分页,绕过 count 开销 - Node.js 驱动中,
countDocuments()是异步函数,不能和find().toArray()并发发请求——MongoDB 连接池默认复用,高并发下可能因 socket 复用导致超时或乱序,建议串行或显式控制并发
分页必须用 skip() + limit()?不,游标分页更稳
skip() 在偏移量大时性能急剧下降,因为 MongoDB 仍要从头扫描跳过的所有文档。比如 skip(100000),即使只取 20 条,也要定位并跳过前 10 万条,CPU 和 I/O 压力都高。
推荐用游标分页(cursor-based pagination):以排序字段(如 _id 或时间戳)为锚点,每次请求带上上一页最后一条的值,用 $gt / $lt 查询下一页。
- 必须有确定的排序字段,且该字段在查询范围内唯一或几乎唯一(避免漏数据或重复)
- 示例:按
createdAt降序分页,上一页最后一条是{"createdAt": "2024-05-01T10:00:00Z"},下一页查{ createdAt: { $lt: new Date("2024-05-01T10:00:00Z") } },再limit(20) - 不能用
skip()实现“跳转到第 N 页”,游标分页只支持线性翻页;真需要跳页,得结合缓存(如 Redis 存每页首条_id)或预计算 offset 映射
聚合管道里怎么同时返回数据和总数?用 $facet
一次请求拿数据 + 总数,最直接方式是聚合管道的 $facet。它允许在一个 pipeline 中并行执行多个子管道,分别输出数据列表和计数结果。
注意:虽然写起来简洁,但 $facet 内部仍是两路独立执行,总数部分依然会走 countDocuments 级别的扫描逻辑,性能不会比分开调用更好;但它减少了网络往返,适合对延迟敏感、且总数查询本身不重的场景。
- 示例 pipeline:
db.collection.aggregate([
{ $match: { status: "active" } },
{
$facet: {
data: [{ $sort: { _id: 1 } }, { $skip: 0 }, { $limit: 20 }],
total: [{ $count: "count" }]
}
}
])
-
$count只返回一个文档{ count: 12345 },不是数字本身,取值时要解包 - 如果
$match条件复杂或涉及多字段,确保$facet外层的$match被所有子管道继承;否则要在每个子管道里单独写,容易漏 - Driver 返回的是单个文档,结构为
{ data: [...], total: [{ count: 12345 }] },需手动提取total[0].count
为什么分页接口响应慢?先看执行计划 explain("executionStats")
分页慢,90% 情况不是代码写法问题,而是没走对索引。用 explain("executionStats") 查看真实执行路径,重点关注 executionStages.nReturned 和 executionStages.totalDocsExamined。
- 如果
totalDocsExamined远大于nReturned(比如查 20 条却扫了 10 万文档),说明缺失有效索引,或排序字段未包含在查询条件索引中 - 使用
skip()时,executionStages.stage若是SORT+SORT_KEY_GENERATOR,意味着内存排序已触发,数据量大时可能 OOM;应改用带排序字段的复合索引,让 sort 走索引 - 游标分页下,检查
indexBounds是否精确命中索引范围;若出现IXSCAN但indexBounds是[MinKey, MaxKey],说明索引没被有效利用
索引设计优先级:查询字段 = 排序字段 = 分页锚点字段,三者合一最稳妥。比如按 { status: 1, createdAt: -1, _id: 1 } 建索引,就能支撑 status="active" + createdAt 降序 + 游标分页的全链路。

















