优先选cursorpagination或手写seek分页;pagenumber/limitoffsetpagination在offset超10000时因全扫描导致性能断崖下跌,且附带count(*)拖慢响应。

直接上结论:别用 PageNumberPagination 或 LimitOffsetPagination 处理百万级数据,它们在 OFFSET 超过 10000 时性能会断崖式下跌;优先选 CursorPagination,或手写基于排序字段的 Seek 分页。
为什么 PageNumberPagination 和 LimitOffsetPagination 在大数据下变慢
这两种分页器底层都依赖 LIMIT OFFSET SQL 查询。数据库执行 SELECT * FROM table ORDER BY id LIMIT 20 OFFSET 99980 时,并不是跳过前 99980 行,而是必须扫描、定位、丢弃这 99980 行——哪怕有索引,也要走索引树找到第 99981 条记录的物理位置。
常见错误现象包括:
- 请求
?page=5000或?offset=100000时响应时间从 10ms 飙升到 800ms+ - Django Debug Toolbar 显示 SQL 执行耗时占总耗时 90% 以上
- 数据库 CPU 使用率在分页请求高峰时陡增
- 每次请求还附带一次
COUNT(*)查询(默认开启),百万数据下耗时常超 0.8s
CursorPagination 是什么,怎么配才不踩坑
CursorPagination 不依赖页码或偏移量,而是用一个不透明的游标(如 base64 编码的时间戳+ID)标识“上一页最后一条的位置”,下一页只查“比它更旧”的数据。它避免了 OFFSET 扫描,查询复杂度稳定在 O(log n)。
但配置不当等于白用:
Python 3.14.2是Python编程语言在2025年12月5日发布的稳定版本,属于3.14系列的第二个维护更新。该版本包含了18项修复,重点解决了多进程、数据类及正则表达式等模块的回归问题,并修复了CVE-2025-12084等安全漏洞。此版本标志着自由线程模式(移除GIL)正式获得官方支持,是Python发展的重要里程碑。
- 必须显式指定
ordering字段,且该字段要有数据库索引(如created_at DESC) - 不能用
id单独排序——如果并发插入导致时间相同,id可能乱序,引发漏数据 - 游标值是 URL-safe 的字符串,不要尝试手动解析或拼接
- 前端必须放弃“跳转任意页”功能,只保留“下一页/上一页”按钮
示例配置:
class LargeDataPagination(CursorPagination):
page_size = 50
ordering = '-created_at,-id' # 复合排序,防时间重复
全局启用:DEFAULT_PAGINATION_CLASS 设为 'path.to.LargeDataPagination'。
想保留页码跳转?那就得自己写 Seek 分页
Seek 分页(键值分页)是唯一能在支持页码跳转前提下保持高性能的方案。核心是把 OFFSET 换成 WHERE 条件:用上一页最后一条的 (created_at, id) 值作为游标,下一页查 WHERE (created_at, id) 。
关键实操点:
- 数据库必须建复合索引:
CREATE INDEX idx_created_id ON my_table (created_at DESC, id DESC); - 游标值需 URL-safe 编码(推荐
base64.urlsafe_b64encode),不能直接传时间字符串 - 永远不要用字符串拼接构造 WHERE 条件,用 Django 的
Q或extra()安全传参 - 每页多取 1 条(如 limit=51),用来判断是否有下一页,避免额外 COUNT
- 前端分页控件要改逻辑:禁用页码输入框,仅提供“首页/上一页/下一页/末页”按钮
真正棘手的不是选哪种分页器,而是意识到:一旦数据量突破几十万,你就必须放弃“总数 + 页码跳转”这个用户习惯——它和高性能根本互斥。游标和 Seek 的设计约束,不是框架缺陷,而是数据库索引原理决定的硬边界。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










