seek分页是解决大页码性能崩坏最有效的手段,尤其当页码超过500或offset>10000时,可将查询从秒级降至毫秒级;其核心是用上一页最后记录的排序字段值作游标,避免offset扫描丢弃大量数据,需配合复合索引及非空约束,并舍弃页码跳转、总页数等需求。

直接结论:用 Seek 分页(键值分页)替代 OFFSET 是解决大页码性能崩坏最有效的手段,尤其当页码超过 500 或 OFFSET > 10000 时,Seek 能把查询从秒级降到毫秒级。
为什么 OFFSET 在大数据量下会变慢
数据库执行 LIMIT 20 OFFSET 100000 时,并不是“跳过前 10 万行直接取 20 行”,而是必须扫描并丢弃前 10 万行——哪怕有索引,也要走索引树找到第 100001 条记录的物理位置。PostgreSQL 的 Index-Only Scan 对 OFFSET 优化有限,MySQL 和 SQLite 基本无优化能力。
常见错误现象:
- 请求
?page=5000时响应时间突然飙升到 800ms+,而page=1只要 10ms - Django Debug Toolbar 显示 SQL 执行耗时占总耗时 90% 以上
- 数据库 CPU 使用率在分页请求高峰时陡增
如何手写一个基于 created_at + id 的 Seek 分页
核心思路:不依赖页码数字,而是用上一页最后一条记录的排序字段值作为游标(cursor),下一页查询“比它更新或同时间但 ID 更大”的数据。
假设你按 created_at DESC, id DESC 排序,且 created_at 有索引:
Python 3.14.2是Python编程语言在2025年12月5日发布的稳定版本,属于3.14系列的第二个维护更新。该版本包含了18项修复,重点解决了多进程、数据类及正则表达式等模块的回归问题,并修复了CVE-2025-12084等安全漏洞。此版本标志着自由线程模式(移除GIL)正式获得官方支持,是Python发展的重要里程碑。
- 第一页请求不带游标:
SELECT * FROM article ORDER BY created_at DESC, id DESC LIMIT 21(多查 1 条用于判断是否有下一页) - 第二页请求带游标:
SELECT * FROM article WHERE (created_at, id) - 注意括号内是元组比较,PostgreSQL/MySQL 8.0+ 支持;SQLite 需拆成两个条件
Python 实现要点:
- 不要拼接字符串生成 WHERE 条件,用 Django 的
Q或原生extra()安全传参 - 游标值需 URL-safe 编码(如 base64 或
quote_plus),避免时间戳中含冒号、空格 - 前端分页控件要改造成“上一页 / 下一页”按钮,不再显示页码输入框
DRF 中替换 LimitOffsetPagination 为 Seek 分页
Django REST Framework 没有内置 Seek 分页器,但可以快速自定义。关键不是继承 LimitOffsetPagination,而是绕过它,自己控制 queryset 构造:
- 在视图中解析游标参数:
cursor = request.query_params.get('cursor') - 若
cursor存在,解码后构造filter()条件,例如:Article.objects.filter(created_at__lt=dt).order_by('-created_at', '-id')[:21] - 若不存在,则走首次查询逻辑
- 返回结果时,把最后一条的
(created_at, id)组合成新游标,放进next链接
容易踩的坑:
- 没给
created_at加索引 → 查询仍慢,必须CREATE INDEX CONCURRENTLY ON article (created_at DESC, id DESC) - 游标字段允许 NULL → 元组比较会出错,确保排序字段非空或加
NOT NULL约束 - 并发写入导致同时间戳多条记录 → 单靠
created_at不够稳定,必须组合主键(如id)做二级排序
什么时候不该用 Seek 分页
Seek 分页不是银弹。以下场景仍建议保留 PageNumberPagination 或改用其他方案:
- 需要跳转任意页码(比如用户手动输入 “跳到第 328 页”)→ 这种需求本身就不适合大数据量,应限制页码范围或改用搜索
- 排序字段更新频繁(如
updated_at)→ 游标可能失效,导致漏数据或重复 - 需要精确总页数(
paginator.num_pages)→ Seek 分页无法高效获取总数,只能估算或放弃显示 - 小数据量(OFFSET 开销可忽略,强行改 Seek 反而增加复杂度
真正难处理的,是那些既要求“支持跳页”又“数据超百万”的混合需求——这时候得接受 trade-off:要么牺牲跳页能力,要么引入缓存层预计算页边界,而不是在 SQL 层硬扛。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










