bigywater.tech
MongoDB · 45

skip 大偏移的代价与范围分页

skip 到底慢在哪?范围分页怎么写才不会丢数据、不会重复?这篇用 50 万个文档对比两种分页的检查量、耗时和一致性。

74 ms
skip 400,000 取 20 条的耗时;范围分页恒定 0.2 ms
100,020 对 20
skip 100,000 时检查的键数与读取的文档数
10 条
翻页期间插入 10 条新数据后,skip 方式第 2 页与第 1 页重复的条数(范围方式 0 条)

skip 的代价

实测 · 本机执行输出
      skip        耗时        检查的键       读取的文档    返回
         0    0.3 ms          20          20    20
     1,000    0.4 ms       1,020          20    20
    10,000    2.2 ms      10,020          20    20
   100,000     18 ms     100,020          20    20
   400,000     74 ms     400,020          20    20

skip N 要扫过 N 个索引键:skip 400,000 检查 400,020 个键、74 ms;读取的文档只有 20 个,代价在索引键上。

范围分页

实测 · 本机执行输出
          上一页最后一条的 _id        耗时        检查的键       读取的文档    返回
                     0    0.4 ms          20          20    20
                 1,000    0.4 ms          20          20    20
                10,000    0.3 ms          20          20    20
               100,000    0.2 ms          20          20    20
               400,000    0.2 ms          20          20    20

记住上一页最后一条的排序键,下一页从它后面开始,每页只检查 20 个键,与翻到第几页无关。

explain

实测 · 本机执行输出
  test> db.t.find().sort({ts: 1, _id: 1}).skip(100000).limit(20)
  {
    plan: 'LIMIT > FETCH > SKIP > IXSCAN',
    nReturned: 20,
    keysExamined: 100020,
    docsExamined: 20,
    ms: 24
  }
  test> 只要索引里的字段(覆盖查询){_id: 1, ts: 1} 同样 skip 100000:
  { keys: 100020, docs: 0 }

计划里 SKIP 在 FETCH 之下,被跳过的文档不读;覆盖查询时文档数为 0,但被跳过的键仍要扫。

平局打破

实测 · 本机执行输出
  按 ts 排序取一页 15 条:最后一条 _id=1014, ts=101;这个 ts 值共有 10 个文档
  只用 ts > 上一页最后的 ts 作为条件:下一页第一条 _id=1020;这两页之间丢掉了 5 个 ts 相同的文档
  用 (ts, _id) 作为条件:下一页第一条 _id=1015(紧接着上一页的最后一条,不丢)

排序键不唯一时,条件必须带 _id 之类唯一字段,只用 ts > 上一页最后的 ts 会丢掉同一个 ts 的其余文档。

一致性

实测 · 本机执行输出
  第 1 页(最新 20 条): 1000 … 981
  skip 方式的第 2 页:990 … 971;与第 1 页重复的条数 10
  范围方式的第 2 页:980 … 961;与第 1 页重复的条数 0

翻页期间有新数据写入,skip 方式整体后移,第 2 页重复了第 1 页的 10 条;范围方式不受影响。

总数

实测 · 本机执行输出
  countDocuments({}) 49 ms;estimatedDocumentCount() 0.1 ms;countDocuments({user: 5})(无索引的过滤条件)55 ms

countDocuments 要数文档(50 万个 49 ms),estimatedDocumentCount 读元数据(0.1 ms)但不能带过滤条件。

分步演示

skip 分页与范围分页
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。50 万个文档(ts 每 10 个文档相同);耗时是 pymongo 的 5 次中位数,检查量来自 explain
    • 没有验证:skip 配合 sort 但没有索引时(要全部排序);分片集群上的 skip(每个分片都要取 skip + limit);游标式分页(batchSize、getMore)与分页的区别;降序分页与复合排序方向混合;多排序键的范围条件写法在更复杂场景下的计划
    京ICP备2026067502号-1京公网安备11010502063487号