关于keyset分页
你知道吗,使用 offset 进行分页非常烦人,却很容易可以避开。
offset 指示数据库跳过前 N 条数据进行查询。然而,数据库仍然需要从磁盘读取记录,排序…
这不是执行问题,而是 offset 设计方式:
…the rows are first sorted according to the <order by clause> and then limited by dropping the number of rows specified in the <result offset clause> from the beginning…
SQL:2016, Part 2, §4.15.3 Derived tables
译:…记录先按照 order by 条件排序,然后通过丢弃掉 offset 条件指定数量的行数…
这里的关键点是,offset 只有一个参数:丢掉的记录数,没有更多内容。数据库能做的只有获取和丢掉许多记录,换句话说,大的 offset 会带来数据库大量的工作,无论是 SQL 还是 NoSQL.
不仅如此:想象一下在读取两个页面的之间插入了一条新数据?
我们使用 offset➌ 来跳过前面获取的 entries❶, 你会同时得到副本以防在获取两个页面之间插入新数据➋。同时可能还有其他异常,这只是最常见的问题之一。
这甚至不是数据库问题,而是框架实现分页的方式问题:他只会说明要获取那一页或者要跳过多少行。只有这些信息,任何数据库都差不多。
同时请注意 offset 问题可能来自于不同的语法:
- offset 关键词
- 双参数 limit [offset,] limit(单参数 limit 没问题)
- 基于行数的 Lower-bound 筛选 (e.g. row_number(), rownum, …).
- 所有这些方法都有一个普遍的根本问题,它们只提供一个数字即需要丢掉的行数——而没有其他更多内容了。在此文中,我使用 offset 代指这些方法。
没有 OFFSET 的世界
想象一下没有这些问题。事实上,没有 offset 其实挺简单:只需要使用 where 条件选取你还没看过的记录。
为此,我们利用一个事实是我们以有序集合进行工作——你已经按照 order by 条件进行了排序。一旦给定排序,我们可以使用简单筛选器选取你已有的之后的数:
SELECT ...
FROM ...
WHERE ...
AND id < ?last_seen_id
ORDER BY id DESC
FETCH FIRST 10 ROWS ONLY
这是基本的方法。当在多字段排序时变得更加有趣,不过理念相同。该方法同样适用于 NoSQL 系统。
这个方法——称为 seek method 或者 keyset 分页(keyset pagination)——解决了以上图示显示的漂移问题,甚至比 offset 更快。如果你想知道使用 offset 或者 keyset 分页时,数据库内部发生了什么
可以查看以下的幻灯片:
(幻灯片略)
在 43 页,你会发现 keyset 翻页也有以下限制:很主要的一点是你不能直接导航到任意页面。当然这不会是问题如果使用的是滚屏加载。显示页码供点击不是好的导航接口—— IMHO.
如果你想要读取更多关于怎样在 SQL 中更好地实现 keyset分页, 请读本文。即便你没有涉及 SQL,此文也值得一读。
至于框架…
更喜欢选择 offset 而不是 keyset 主要是缺少工具支持。大部分提供分页的工具都是基于 offset, 而没有提供便捷的方式使用 keyset 分页。
请注意,keyset 分页影响了整个技术栈直到浏览器中中的的 JavaScript 发起 AJAX 请求用来加载滚屏数据,不再是发送一个简单的页码数字给服务器,你必须传送整个 keyset(通常是多列数据)给服务器。
支持 keyset 分页的框架有:
JavaScript
Node.js: bookshelf-cursor-pagination
Massive.js: Keyset Documentation
Python
Django Infinite Scroll Pagination
Java
jOOQ — Java Object Oriented Querying
PHP
.NET
MR.EntityFrameworkCore.KeysetPagination
Ruby
Perl