bigywater.tech
Django · 28

select_related() 与 prefetch_related() 查询对比

select_related 和 prefetch_related 都能消灭 N+1,差别在哪?各自发出什么 SQL?什么时候只能用哪一个?这篇用同一份数据把两者的查询次数、耗时、SQL 都摆出来。

2001 → 1
外键场景下的查询次数:不优化 → select_related
2001 → 2
多对多场景下的查询次数:不优化 → prefetch_related
再 filter
在已预取的属性上再 .filter() 会重新发查询,预取缓存只服务 .all()

外键:两种方式

实测 · 本机执行输出
不优化(每本书访问 author 查一次)                          查询  2001 次      213.5 ms
select_related("author")                       查询     1 次        9.9 ms
prefetch_related("author")                     查询     2 次        8.4 ms

耗时是 5 轮取最快的一次。两种方式都把 N+1 变成常数次查询,耗时接近;这里的数据量下差别很小,不代表在别的数据分布下也一样。

多对多

实测 · 本机执行输出
不优化                                            查询  2001 次      236.9 ms
prefetch_related("tags")                       查询     2 次       58.2 ms
  select_related("tags") → FieldError: Invalid field name(s) given in select_related: 'tags'. Choices are: author

多对多(和反向外键)是「一对多」的关系,JOIN 会让主表的行重复,所以只能用 prefetch_related(分两步查再在 Python 里组装)。

实际发出的 SQL

实测 · 本机执行输出
select_related:
   SELECT "lab_book"."id", "lab_book"."title", "lab_book"."author_id", "lab_author"."id", "lab_author"."name" FROM "lab_book" INNER JOIN "lab_author" ON ("lab_book"."author_id" = "lab_author"."id") LIMIT 2
prefetch_related:
   SELECT "lab_book"."id", "lab_book"."title", "lab_book"."author_id" FROM "lab_book" LIMIT 2
   SELECT ("lab_book_tags"."book_id") AS "_prefetch_related_val_book_id", "lab_tag"."id", "lab_tag"."name" FROM "lab_tag" INNER JOIN "lab_book_tags" ON ("lab_tag"."id" = "lab_book_tags"."tag_id") WHERE "lab_book_tags"."book_id" IN (1, 2)

select_related 一条 JOIN;prefetch_related 一条 IN (…)。IN 列表的长度等于主表这一批的行数,行数很大时要分批(SQLite 有变量个数限制,本文没有测到边界)。

反向外键与 Prefetch 对象

实测 · 本机执行输出
100 个作者各查一次书                                   查询   101 次       18.4 ms
prefetch_related("books")                      查询     2 次        8.4 ms
Prefetch(queryset=filter, to_attr) 的查询数: 2  SQL 2: SELECT "lab_book"."id", "lab_book"."title", "lab_book"."author_id" FROM "lab_book" WHERE ("lab_book"."id" <= 200 AND "lab_book"."author_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1...
  前 3 个作者带回的书数量: [2, 2, 2]
过滤后再访问原属性 a.books.all() 会不会多一次查询?
  在预取的属性上再 .filter(...) 的查询数(100 个作者): 102

用 Prefetch(queryset=..., to_attr=...) 预取带条件的子集;之后读 to_attr 指定的属性。在预取的关系上再写 .filter() 会重新查询,是 N+1 复发的常见原因。

分步演示:两种预取

select_related vs prefetch_related
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite;数据:2000 本书、100 个作者、20 个标签、每本书 3 个标签;耗时为 5 轮中最快的一轮
    • 没有验证:PostgreSQL / MySQL 上的耗时与计划、大结果集下 IN 列表的分批、嵌套预取(a__b__c)、select_related 多层 JOIN 的成本、prefetch_related_objects()
    京ICP备2026067502号-1京公网安备11010502063487号