pagenumberpagination 不适用于百万级数据,因其依赖 count() 和 offset/limit,查深页时性能骤降;游标分页通过排序字段+上页末尾值实现索引范围扫描,需满足唯一排序、联合索引、游标防篡改三条件。

为什么不能用 PageNumberPagination 处理百万级数据?
因为 PageNumberPagination 依赖 count() 和 OFFSET/LIMIT,查第 10000 页时会扫描前 10000 × 每页条数行再丢弃——MySQL/PostgreSQL 都会明显变慢,甚至触发超时或锁表。游标分页绕过 OFFSET,靠排序字段+上一页末尾值定位下一页,查询始终走索引范围扫描。
CursorPagination 必须满足的三个条件
它不是开箱即用的“换掉分页类就行”,漏掉任意一条都会返回错误结果或跳数据:
- 排序字段必须有 唯一性保障:推荐组合
created_at+id(如'-created_at', '-id'),避免同一秒多条记录导致游标偏移 - 对应字段必须建联合索引:例如
db_index=True在模型里只对单字段有效,需在迁移中显式添加Index(fields=['-created_at', '-id'])或 SQL 手动建 - 客户端传的
cursor值不可篡改或重放:Django 默认用 base64 编码游标,但不校验签名,生产环境建议配合signed_cursor=True(Django 4.2+)或自行签名校验
如何自定义游标生成逻辑?
默认 CursorPagination 把排序字段值拼接后 base64 编码,但如果你的 created_at 是带微秒的 datetime,可能因精度问题导致前后端解析不一致;或者你想隐藏真实 ID —— 这时需要重写 get_cursor_querystring 和 decode_cursor:
class CustomCursorPagination(CursorPagination):
ordering = '-created_at,-id'
<pre class="brush:python;toolbar:false;">def encode_cursor(self, cursor):
# 例如把 id 转成短哈希,created_at 截断到秒级
ts = int(cursor[0].timestamp())
short_id = str(cursor[1])[-6:] # 简单截取,实际可用 hashids
return b64encode(f"{ts}:{short_id}".encode()).decode()
注意:重写后必须同步修改 decode_cursor,且确保编码后的字符串能无损还原出用于 WHERE 条件的原始值。
前端怎么安全地传递和存储游标?
游标本质是“下一页起始位置的快照”,不是页码,不能做算术运算(比如“上一页”不能靠减 1 实现):
- 响应体里必须返回
next和previous字段(Django REST Framework 自动提供),值为完整 URL,含cursor=xxx参数 - 前端只能原样保存并发起 GET 请求,禁止解析、拼接或尝试构造游标值
- 如果要做“回到顶部”,不要清空游标再请求第一页,而应直接跳转到无
cursor参数的初始 URL —— 因为游标分页没有绝对“第一页”的概念,只有“首次查询”
最常被忽略的是:游标链接有效期。数据库数据持续写入时,上一页末尾记录可能被新数据“挤”出索引范围,导致 previous 链接失效(返回空结果)。这不是 bug,是游标分页的设计特性。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











