bigywater.tech
Django · 100

N+1 查询问题实战

N+1 不只出现在你自己写的视图里,admin 列表页、__str__、@property 也会悄悄制造它。这篇用一个三层关系(分类 → 作者 → 文章)的 admin,把 N+1 从发现到修复走一遍。

55 → 5
50 行文章列表的查询数:未优化 → list_select_related
__str__
模型的 __str__ 里访问外键,在每一行显示时都会触发查询
自动 select_related
admin 对 list_display 里的外键列会自动 select_related,但不会跟到 __str__ 里更深一层

admin 列表页

实测 · 本机执行输出
文章列表 list_display=(title, author)      → status, 查询数 = (200, 55)
作者列表 list_display=(name, category)      → status, 查询数 = (200, 5)

这里的关键是「admin 已经帮你做了一层」——所以有时候你以为没问题,换一个模型的 __str__ 就出现了 N+1。

藏在 __str__ 与 property 里

实测 · 本机执行输出
5 篇文章各 str(p) 的查询数: 10 (每篇:取 author + 取 category = 2 次)
property label(author.category.name)5 篇: 11  (含 1 次取文章列表)

最好的习惯:模型的 __str__ 只用自己的字段,不要访问外键;需要展示关联信息时用专门的 list_display 方法并配合 list_select_related。

修复

实测 · 本机执行输出
文章列表 list_select_related=("author__category") → 查询数 = 5
作者列表 list_select_related=("category")          → 查询数 = 5

list_select_related 接受字段名元组(可以带 __ 做多层)。也可以在 get_queryset 里自己 select_related / prefetch_related。

数据量的影响

实测 · 本机执行输出
    10 篇文章:未优化 15 次查询;优化后 5 次
    50 篇文章:未优化 55 次查询;优化后 5 次
   200 篇文章:未优化 105 次查询;优化后 5 次

用不同的行数各跑一遍,就能确认查询数是不是与行数成正比——这是判断 N+1 最直接的办法。

分步演示:N+1 实战

admin 里的 N+1
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite;admin 页面由 django.test.Client 以超级用户请求,查询数由 CaptureQueriesContext 统计(含会话、用户、计数等固定开销)
    • 没有验证:admin 详情页 / inline 里的 N+1、DRF 序列化器里的 N+1、django-debug-toolbar 的重复查询检测(第 101 篇)、更早版本的 admin 是否也自动 select_related(6.1.1 的行为已读 ChangeList.apply_select_related 源码确认)
    京ICP备2026067502号-1京公网安备11010502063487号