Django Admin 默认分页在百万数据下变慢,是因为其依赖 QuerySet.count() 触发全表扫描以计算总行数,即使只查看第1页;优化方案包括自定义 Paginator 禁用精确计数、启用游标分页、添加数据库索引及查询路由到只读副本。

为什么 django.contrib.admin 默认分页在百万数据下会变慢
因为 Django Admin 的默认分页依赖 QuerySet.count(),而 PostgreSQL/MySQL 在无索引或大表上执行 COUNT(*) 时需全表扫描——哪怕你只看第 1 页,它也得算出总行数。百万级表上这个操作常耗时数百毫秒到数秒,且并发一高就雪崩。
根本问题不在“显示”,而在“告诉用户总共有多少页”。实际业务中,用户极少翻到末页;但 Admin 界面强依赖 paginator.num_pages 渲染页码栏,触发了不必要的统计。
- 不改代码时,
Admin.list_per_page = 100仍卡,因为count()每次请求都执行 -
select_related/prefetch_related对 count 无效——它们只影响SELECT,不影响COUNT - 加数据库索引(如主键)能加速
COUNT(*),但 MySQL MyISAM 下快、InnoDB 下依然慢;PostgreSQL 有估算函数但 Admin 不用
绕过 count():自定义 Paginator 并禁用页码栏
最直接有效的做法是让分页器不查总数,同时隐藏掉不可靠的页码数字。Django 允许传入自定义 Paginator 类,只需重写 count 和 num_pages 属性,返回近似值或固定值。
示例:
from django.core.paginator import Paginator
<p>class EstimatedCountPaginator(Paginator):
@property
def count(self):</p><h1>返回一个稳定、快速的估算值(比如从 pg_class 或 SHOW TABLE STATUS 获取)</h1><pre class="brush:php;toolbar:false;"><pre class="brush:php;toolbar:false;"> # 或直接返回一个足够大的常量,如 10_000_000
return 10_000_000
@property
def num_pages(self):
return (self.count + self.per_page - 1) // self.per_page</code></pre></p>
- 在
ModelAdmin 中启用:<code>paginator = EstimatedCountPaginator - 配合
list_display_links = None和show_full_result_count = False,Admin 就不再显示“显示 X 到 Y 条,共 Z 条”那行文字 - 前端页码控件会消失,只剩“上一页 / 下一页”按钮——这反而是百万数据下的合理交互
真·秒级响应:用游标分页替代 OFFSET/LIMIT
OFFSET 分页在深度翻页(如第 10000 页)时性能断崖下跌,因为数据库要跳过前 N 行。游标分页(Cursor-based Pagination)用排序字段+值定位,复杂度恒定 O(log n)。
仅使用 Python 将 Claw Code 虚拟化框架与 OpenClaw 集成,提供 184 种工具和 200 多条命令,用于安全分析和代码质量...
立即学习“Python免费学习笔记(深入)”;
Django 本身不内置游标分页 Admin,但可借助 django-cursor-pagination 或手写逻辑:
- 要求列表必须有唯一、非空、有索引的排序字段(如
id或created_at) - URL 变成
?cursor=cD0xMjM0NTY=而非?p=1000,避免状态丢失和越界问题 - Admin 需重写
get_queryset()和changelist_view(),替换Paginator实例为游标分页器 - 注意:不能随意改排序字段,否则游标失效;
search_fields与游标分页互斥,需二选一
别忘了数据库层的硬优化
即使换了分页逻辑,查询本身仍可能慢——尤其带 list_filter 或 search_fields 时生成的 WHERE 条件没走索引。
- 给所有
list_filter字段建数据库索引:db_index=True或迁移中手动CREATE INDEX -
search_fields若含__icontains,MySQL 需前缀索引,PostgreSQL 推荐pg_trgm扩展 +GIN索引 - 禁用无意义的
list_select_related:它会让单条 SQL 变成 JOIN,反而拖慢游标分页的首行获取 - 考虑把 Admin 查询拆到只读副本库,用
using('replica')路由,避免拖累主库
真正卡住的往往不是“分页机制”,而是没索引的 WHERE 条件或低效的 ORDER BY。游标分页再快,遇上 ORDER BY RAND() 或未索引字段排序也白搭。

















