大表深分页慢的根源是数据库执行LIMIT OFFSET时需扫描丢弃前offset行,而非Django性能问题;应改用CursorPagination或手动Seek分页,并避免count()全表扫描。

大表分页慢,根本不是 Django 写得不好,而是 MySQL(或 PostgreSQL)执行 LIMIT offset, size 时必须扫描并丢弃前 offset 行——数据量越大,丢弃成本越高。百万级表里 OFFSET > 50000 就可能卡到秒级,count() 更是直接全表扫。
为什么 PageNumberPagination 在深分页下会崩
DRF 默认的 PageNumberPagination 底层靠 QuerySet[offset:offset+size] 实现,最终生成类似 SELECT * FROM article LIMIT 20 OFFSET 100000 的 SQL。MySQL 不会跳过索引节点,而是顺着索引树逐条定位第 100001 条记录的位置,中间所有匹配行都要读、判断、丢弃。
- 现象:前端点“第 5000 页”时接口响应从 20ms 暴涨到 1200ms,数据库 CPU 突增
-
EXPLAIN显示type = range但rows高达 10 万+,Extra含Using filesort - 哪怕
id有主键索引,OFFSET越大,越难利用索引的“跳跃”能力
用 CursorPagination 替代页码跳转
它不依赖数字页码,而是记住上一页最后一条记录的排序值(游标),下一页查“比它更小/更大”的数据,彻底避开 OFFSET。前提是排序字段必须有高效索引,且值能唯一锚定位置。
- 只适用于严格单调、非空、有索引的排序字段,比如
id或created_at+id组合 - Django REST Framework 中启用:
pagination_class = CursorPagination,并显式设ordering = '-id' - 前端不再传
page=3,而是传上一页响应里的cursor参数(Base64 编码的排序值) - 不能跳转任意页,但支持稳定、恒定耗时的“下一页”流式加载,适合日志、消息、动态时间线
手写 WHERE (col1, col2) < (val1, val2) 实现 Seek 分页
当 DRF 的 CursorPagination 不够灵活(比如要兼容 created_at DESC, id DESC 多字段排序),就得手动构造元组比较条件。MySQL 8.0+ 和 PostgreSQL 原生支持,SQLite 需拆成两个独立条件。
立即学习“Python免费学习笔记(深入)”;
- 第一请求(无游标):
Article.objects.order_by('-created_at', '-id')[:21] - 第二请求(带游标):
Article.objects.filter(created_at__lt=last_dt).filter(id__lt=last_id).order_by('-created_at', '-id')[:21]—— 注意:不能只用created_at__lt,同一秒多条记录时会漏或重 - 游标值必须 URL-safe 编码,避免
created_at里冒号、空格导致解析失败 - 索引必须覆盖排序字段:建
CREATE INDEX idx_article_created_id ON article (created_at DESC, id DESC);MySQL 5.7 及更早忽略DESC,此时统一用升序字段(如id)作主游标更稳
count() 是分页里最隐蔽的性能杀手
几乎所有分页器默认调 paginator.count 来算总页数,而 Django 的 count() 对大表就是 SELECT COUNT(*) 全表扫描。它比你实际要取的 20 条数据还慢。
- 真实场景中,用户几乎不关心“总共多少页”,只关心“还能不能往下翻”。去掉
count()能立竿见影降 300–800ms - PostgreSQL 可用
pg_class.reltuples快速估算(误差 ±10%),MySQL 无等效机制,别硬套 - 如果必须显示总数,考虑异步更新缓存计数,或改用近似统计(如采样估算),而不是每次请求都扫一遍
- DRF 自定义分页器里,重写
get_paginated_response,把'count'字段换成布尔值'has_next'即可
真正卡住的从来不是 Python 层逻辑,而是那条没被索引覆盖、又带了 OFFSET 的 SQL。优化顺序永远是:先看 EXPLAIN,再加索引,再换分页策略,最后才动代码。游标和 Seek 分页不是银弹,但它们把“慢”从 O(N) 变成了 O(1),这个拐点一旦跨过,后续所有扩展都轻松得多。


















