skip 到底慢在哪?范围分页怎么写才不会丢数据、不会重复?这篇用 50 万个文档对比两种分页的检查量、耗时和一致性。
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 20skip 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 个键,与翻到第几页无关。
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 mscountDocuments 要数文档(50 万个 49 ms),estimatedDocumentCount 读元数据(0.1 ms)但不能带过滤条件。