因为mysql执行offset需扫描并丢弃前n行,数据量越大扫描成本越高,导致cpu和i/o压力剧增;游标分页通过记录上一页最后排序值、查询“大于该值”的数据,避免全量扫描,性能恒定。

为什么 OFFSET 在百万级表里会越来越慢?
因为 MySQL 执行 SELECT * FROM table LIMIT 100000, 20 时,必须先扫描前 100000 行再丢弃,即使只取 20 条。行数越多,扫描成本越高,CPU 和 I/O 压力直线上升。
这不是 Django 的问题,是 MySQL 的执行机制决定的。Django 的 QuerySet 默认用 OFFSET 实现分页,所以一到深分页就卡顿。
- 真实场景中,
OFFSET > 50000就可能触发秒级延迟 -
ORDER BY id且id有索引时,才适合改用游标分页;若按created_at排序,得确保该字段有联合索引(如(created_at, id)) - 别依赖
count()获取总页数——它会触发全表扫描,大表下比LIMIT还慢
用游标分页替代 PageNumberPagination
核心思路:不跳过前 N 行,而是记住上一页最后一条记录的排序字段值,下一页查“比它更大”的数据。
比如按 id 升序分页,第一页取 id__gt=0 的前 20 条;第二页记下第 20 条的 id(设为 12345),下一页查 id__gt=12345 的前 20 条。
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
- Django REST Framework 中,改用
CursorPagination类,需显式指定ordering字段,且该字段必须有索引 - 前端不再传
page=3,而是传上一页返回的cursor参数(Base64 编码的排序值) - 不能跳转任意页,但支持无限向下翻——这对日志、消息流等场景更合理
- 注意时区和 NULL 值:
created_at若允许 NULL,需在查询中排除,否则游标值不可靠
如何给排序字段加高效索引?
游标分页快不快,80% 取决于索引是否覆盖查询条件 + 排序字段。
假设模型是 Article,常用 order_by('-created_at', '-id'),那么最有效的索引是:
CREATE INDEX idx_article_created_id ON article (created_at DESC, id DESC);
- MySQL 8.0+ 支持降序索引,老版本会忽略
DESC,实际建的是升序,此时应统一用升序字段(如id)做主游标 - 避免在
WHERE条件里混用非索引字段,比如status=1未建索引,会导致索引失效,回表严重 - 用
EXPLAIN验证:type应为range或index,Extra不含Using filesort或Using temporary
手动实现游标逻辑时容易漏掉什么?
直接写 filter(created_at__lt=last_created_at) 看似简单,但边界情况多。
- 同一秒内多条记录:仅靠
created_at无法唯一确定位置,必须加入id作为第二排序字段并写进 WHERE - 删除中间数据导致游标“断层”:比如第 100 条被删,第 101 条的游标值可能和第 99 条重复,需用
(created_at, id)元组比较,而非单字段 - 前端传错游标(如篡改 Base64):建议在视图里捕获
ValueError或解码失败,返回 400 而非 500 - 首次请求没有游标时,用
created_at__isnull=False或最小值兜底,避免None参与比较引发意外结果
复杂点不在代码长度,而在排序字段组合、索引匹配、空值/重复值处理这三者必须对齐。少一个,游标就可能漏数据或重复。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










