在考虑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
广告
将在 10 秒后关闭
bannerAds