在考虑RESTful API的分页时,需要注意的事项
追加说明:关于性能比较,有人指出”是否正确应用了索引呢?”是的确如此。非常抱歉。
由于 Django RESTful Framework 中的分页实践很好地概述了在创建 RESTful API 时的分页过程,因此希望将其泛化以适用于除 Django 之外的其他实现。
响应的表达方式
在响应中指示下一页的方法有以下几种方式。
-
- JSON や XML のレスポンスに含める
Link ヘッダに URL で指定する
RFC 8288
GitHub REST API v3 Traversing with Pagination
HTML なら link タグが使えたりもする (HTML Living Standard – 14 July 2020 4.6.6.24: Sequential link types)
Content-Range ヘッダを使う
无论支持哪种都可以,如果不考虑数据量和复杂性,我认为可以同时支持两种。不过,就目前而言,我觉得标题不像是为分页设计的,所以我觉得最好是将其包含在内容中。补充:最符合标准的应该是类似于 Link 头部,但不返回 URL 感觉有点冗长。个人而言,我想要包含在响应中。进一步补充:可以使用相对路径从请求的URI中,所以只需重新构建查询部分并返回以 “?” 开头的字符串应该是更好的。
当它表现为相对引用形式时([RFC3986]第4.2节),最终值是通过与有效请求URI进行解析计算得出的([RFC3986]第5节)。
不过从 RESTful API 的话题离开一下,据说 Google 搜索从几年前开始就不再支持 rel=prev/next。如果有考虑到搜索的话,最终要在网页的正文中进行反映。
春季大扫除!在我们评估了索引信号后,我们决定停止使用rel=prev/next。研究显示用户喜欢单页面内容,尽可能地进行单页面设计,但对于Google搜索来说,多部分内容也是可以的。了解并为*你的*用户做出最好的选择!#春天即将来临 pic.twitter.com/hCODPoKgKp— 谷歌网络管理员 (@googlewmc) 2019年3月21日
请求的表达方式
限制/偏移类型
如何在链接中添加参数,如 ?limit=100&offset=500。
由于SQL的LIMIT子句保持不变,因此实现简单,客户端的自由度也很高。
页码类型
比如会变成 ?page=4 这样。虽然涉及到了一些简单的算术运算,但由于查询的模式数量受限,所以更容易进行缓存。如果是事先准备好的情况,比使用 LIMIT/OFFSET 更简单。
光标类型
指定要开始的数据ID的方法。当数据量变大时,由于不必保留排序的数据,这种方法会带来效果。例如,可以表示为?cursor=…&limit=100。
对于 LIMIT/OFFSET 类型的反对意见在 “Building Cursors for the Disqus API” 这篇文章中有详细介绍。例如,当我们想要获取不确定数量的最新内容,比如时间线上的内容时,使用 OFFSET 并不能轻松地进行特定的定位。而如果数据被分割保存在不同的分区中,我们就不需要考虑分区的开头是全体数据的第几项了。AWS的 Dynamo DB 也是通过响应中的 LastEvaluatedKey 和请求中的 ExclusiveStartKey 来实现这个功能的。
与其他方式相比,客户端返回处理变得更加困难,无法跳转到某些页面。取而代之的是,滚动到页面底部时会自动伸展的页面导航更适用。此外,即使在时间过去后再次访问,只要不与目标数据发生冲突,内容也不会因新文章而改变。用户可以在查询 URL 上粘贴并等待一段时间后,仍能显示相同的数据。
总结
-
- 次のページのリクエストに必要な情報を表現する方法は、 今のところ特に決まりはない Link ヘッダが良さそう。
-
- データの特性によって、リクエストの表現方法を選定するべき
DB のインデックスが許す限りは、クライアントが自由に決めれる LIMIT/OFFSET 型でも問題はない
事前に計算したりキャッシュする場合は、ページ番号で引くようにする方法が考えられる
カーソル型は件数が増えてもそれほど遅くはならないが、ユーザの操作が制限されてしまう
新着順の URL で時間が経っても同じデータを表示できる
DynamoDB や Cassandra などの NoSQL と相性が良さそう
创建索引name_id在profiles表的name和id列上,即可将执行时间缩短至约0.200秒。
光标形式的性能.
我在SQL中实际比较了LIMIT/OFFSET类型。
CREATE TABLE profiles (
id CHAR(36) PRIMARY KEY,
name VARCHAR(80)
);
数据共有100万条。其中名字数据通过疑似个人信息数据生成服务生成了100人的数据,并进行了10000次INSERT,总计生成了100万条数据。因此,仅凭名字无法确定顺序,需要通过(name, id)进行排序。顺便一提,id是使用UUID生成的。
虽然原本应该更改查询几次并清除缓存,但结果已经足够显著,所以只进行简单的操作。
LIMIT/OFFSET 型 (SQLite3) 的中文释义是”将结果限制和分页显示”。
命令:
time sqlite3 test.db "SELECT * FROM profiles ORDER BY name, id LIMIT 500000,10"
结果:
2.08s user 2.40s system 99% cpu 4.497 total
光标类型(SQLite3)
请用中文进行本地化版本。
命令:
time sqlite3 test.db "SELECT p.* FROM profiles p, (SELECT name, id FROM profiles WHERE id='fffe95b6-4032-458a-bda6-cb49dc0f9261') AS v WHERE p.name>v.name OR (p.name=v.name AND p.id>v.id) ORDER BY p.name, p.id LIMIT 10"
The result is:
结果:
0.25s user 0.02s system 99% cpu 0.264 total
LIMIT/OFFSET 型 (MariaDB) – 限制/偏移类型 (MariaDB)
用中文进行改写:
指令:
time mysql pagination_test -u root -e "SELECT * FROM profiles ORDER BY name, id LIMIT 500000,10"
结果:
0.00s user 0.00s system 0% cpu 20.562 total
游标形式的 (MariaDB)
指令:
指令:
time mysql pagination_test -u root -e "SELECT p.* FROM profiles AS p WHERE (p.name, p.id)>(SELECT name, id FROM profiles WHERE id='fffe95b6-4032-458a-bda6-cb49dc0f9261') ORDER BY p.name, p.id LIMIT 10"
结果:
0.01s user 0.00s system 1% cpu 0.573 total