bigywater.tech
Flask · 104

API 分页

API 分页用 OFFSET 还是游标?翻到后面会慢多少?翻页时数据变了会怎样?db.paginate() 对非法参数怎么处理?这篇在真实的 20 万行数据上把耗时和一致性逐项量出来。

0.7 → 9.7 ms
OFFSET 分页从第 1 页翻到第 9999 页的耗时
0.60 ms
键集分页在同样位置的耗时
漏掉 4
翻页时删掉前面一行,OFFSET 的第 2 页漏掉的 id

OFFSET 越翻越慢

实测 · 本机执行输出
  第     1 页(OFFSET      0)    0.67 ms
  第  1000 页(OFFSET  19980)    1.54 ms
  第  5000 页(OFFSET  99980)    5.14 ms
  第  9999 页(OFFSET 199960)    9.71 ms

上一篇:请求参数校验;下一篇:API 版本控制。

键集分页

实测 · 本机执行输出
  从 id>     0 开始    0.57 ms
  从 id> 19980 开始    0.62 ms
  从 id> 99980 开始    0.63 ms
  从 id>199960 开始    0.60 ms
  EXPLAIN(OFFSET 199980): Limit  (cost=7728.33..7728.42 rows=1 width=82)
  EXPLAIN(id > 199980):   Limit  (cost=0.42..4.56 rows=20 width=82)
  (EXPLAIN 估算成本:OFFSET 约 7728,keyset 约 4.6——OFFSET 要先数过前面的 199980 行)
  顺便:select count(*) 取总数 7.3 ms(200000 行)

翻页时数据变化

实测 · 本机执行输出
  第 1 页 ids = [1, 2, 3] → 此时删掉 id=1 → OFFSET 的第 2 页 = [5, 6, 7] (漏掉了 4)| keyset 的下一页 = [4, 5, 6]

游标

实测 · 本机执行输出
  第一页返回的 next = eyJhZnRlciI6IDIwfQ== → 解码: {"after": 20}
  用它取第二页的前 3 个 id: [21, 22, 23]
  篡改的游标 '###'              → 400
  篡改的游标 'eyJhZnRlciI6IC'   → 400
  (游标只是编码不是加密——客户端能读也能伪造,服务端必须校验;不要把敏感值放进游标)

db.paginate()

实测 · 本机执行输出
  /paginate                       → 200 {'first': 1, 'has_next': True, 'n': 20, 'page': 1, 'pages': 10000, 'per_page': 20, 'total': 200000}
  /paginate?page=3&per_page=50    → 200 {'first': 101, 'has_next': True, 'n': 50, 'page': 3, 'pages': 4000, 'per_page': 50, 'total': 200000}
  /paginate?per_page=1000         → 200 {'first': 1, 'has_next': True, 'n': 100, 'page': 1, 'pages': 2000, 'per_page': 100, 'total': 200000}
  /paginate?page=0                → 404 <!doctype html>
<html lang=en>
<title>40
  /paginate?page=99999            → 404 <!doctype html>
<html lang=en>
<title>40
  /paginate?page=abc              → 404 <!doctype html>
<html lang=en>
<title>40
  /paginate?per_page=0            → 404 <!doctype html>
<html lang=en>
<title>40

分步演示

API 分页
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-SQLAlchemy 3.1.1、psycopg 3.3.6、自己起的 PostgreSQL(非默认端口)
    • 没有验证:按非唯一列(如时间戳)排序的键集分页(要加主键做并列排序)、反向翻页、Link 头与 HATEOAS 风格的响应格式、total 的近似计数(pg_class.reltuples)、缓存分页结果
    京ICP备2026067502号-1京公网安备11010502063487号