Python的Django ORM把我坑惨了,原来select_related和prefetch_related的区别这么大

简介: 本文详解 Django ORM 中经典的 N+1 查询问题及解决方案:`select_related`(适用于 ForeignKey/OneToOne 单值关系,通过 JOIN 一次查询)与 `prefetch_related`(适用于 ManyToMany/反向一对多,分步查询 + Python 关联)。厘清二者本质区别、适用场景、误用后果及性能权衡,助你高效优化查询性能。(239字)

上个月帮同事看一个 Django 接口的性能问题。页面加载要七八秒,他定位到是某个列表页查询太慢。代码大概长这样:

orders = Order.objects.filter(status='paid')
for order in orders:
   print(order.user.username)      # 访问关联的 User
   for item in order.items.all():  # 访问关联的 OrderItem
       print(item.product.name)    # 再访问 OrderItem 的 Product

他说:“Django 的 ORM 不是自动处理关联查询吗?我直接用点号访问,多方便。”

是方便。但每次点号访问,只要关联对象还没被加载,Django 就会单独发一条 SQL 去数据库取。

一个订单列表页,假设有 200 个订单,每个订单 5 个商品,涉及 100 个用户。那么:

  • 取订单:1 条 SQL
  • 每个订单取用户:200 条 SQL
  • 每个订单取商品项:200 条 SQL
  • 每个商品项取商品:1000 条 SQL

加起来 1401 条 SQL。每条 SQL 哪怕只花 5 毫秒,也要 7 秒多。这就是典型的 N+1 查询问题。不是数据库慢,是查询次数太多。

解决办法就是 select_relatedprefetch_related。但这两个名字长得像、功能描述也像——“都是用来减少查询次数的”。很多人随便挑一个用,结果要么没效果,要么报错,要么内存暴涨。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (91).png

先搞清楚外键在数据库里长什么样

要理解两者的区别,得先回到数据库层面看关联关系是怎么存的。

以订单和用户为例:

class User(models.Model):
   username = models.CharField(max_length=100)

class Order(models.Model):
   user = models.ForeignKey(User, on_delete=models.CASCADE)
   status = models.CharField(max_length=20)
   created_at = models.DateTimeField(auto_now_add=True)

Order 表里有一个 user_id 字段,存的是用户表的主键。这是一个多对一关系:多个订单可以属于同一个用户,每个订单只有一个用户。

当 Django 执行 order.user 的时候,它看到 Order 实例的 user_id 是 5,但 user 对象还没加载,于是发一条 SQL:

SELECT * FROM user WHERE id = 5;

如果循环里对每个订单都这么做,就是 N 条额外 SQL。

select_related 解决的就是这个问题。它告诉 Django:“在取订单的时候,顺便把关联的用户也取出来。” Django 会用 SQL 的 JOIN 把两张表连起来,一次查询拿到所有数据:

SELECT order.*, user.*
FROM order
INNER JOIN user ON order.user_id = user.id
WHERE order.status = 'paid';

拿到结果后,Django 在内存里把每一行拆成 OrderUser 两个对象,并建立好引用。之后 order.user 直接返回已经加载好的对象,不再发 SQL。

一次 JOIN 查询代替了 N+1 条查询。


多对多和反向关系,JOIN 搞不定

现在看订单和商品项的关系:

class OrderItem(models.Model):
   order = models.ForeignKey(Order, on_delete=models.CASCADE)
   product = models.ForeignKey(Product, on_delete=models.CASCADE)
   quantity = models.IntegerField()

OrderItem 有外键指向 Order。反过来,一个订单可以有多个订单项。从 OrderOrderItem 是一个一对多的反向关系。

Django 默认给反向关系起名叫 orderitem_set,但如果 ForeignKey 里设置了 related_name='items',就可以用 order.items.all() 访问。

问题来了:select_related 能不能用在 order.items.all() 上?

不能。select_related 只能用于单值关系,也就是 ForeignKeyOneToOneField。因为 JOIN 之后,一行订单对应一行用户,一对一,不会产生重复行。

order.items.all() 是一对多,一个订单对应多个订单项。如果硬用 JOIN:

SELECT order.*, orderitem.*
FROM order
INNER JOIN orderitem ON orderitem.order_id = order.id;

一个订单有 5 个订单项,结果集里就会出现 5 行。Django 拿到这 5 行后,需要把它们合并成一个 Order 对象,再把 5 个 OrderItem 挂上去。这可行,但如果有多个一对多关系同时 JOIN,行数会乘法式膨胀,性能反而更差。

所以 Django 对多对多和反向一对多提供了另一个工具:prefetch_related

prefetch_related 走的是另一条路

prefetch_related 不用 JOIN。它的做法是分两步查询,然后在 Python 层面做匹配

还是用订单和订单项举例:

orders = Order.objects.prefetch_related('items').filter(status='paid')

Django 会执行两条 SQL:

第一条,取所有订单:

SELECT * FROM order WHERE status = 'paid';

假设拿到 200 个订单,ID 是 1 到 200。

第二条,取这些订单的所有订单项:

SELECT * FROM orderitem WHERE order_id IN (1, 2, 3, ..., 200);

一条 SQL 把所有相关订单项都取回来。

然后在 Python 里,Django 遍历这 200 个订单项,根据每项的 order_id,把它挂到对应的 Order 对象的 _prefetched_objects_cache 里。之后再访问 order.items.all(),直接返回缓存好的列表,不再发 SQL。

总共 2 条 SQL,而不是 201 条。

prefetch_related 还能继续往下预取。比如订单项里还有 product 外键:

orders = Order.objects.prefetch_related('items__product')

它会发三条 SQL:取订单、取订单项、取商品。然后在 Python 里把 OrderItemProduct 关联起来。比用 select_related 做多层 JOIN 更可控,也更省内存。

核心区别:一条 SQL 还是两条 SQL

现在可以做个清晰的对比了:

维度 select_related prefetch_related
查询方式 SQL JOIN 分步查询 + Python 匹配
SQL 条数 1 条 2 条(或更多)
适用关系 ForeignKey、OneToOneField ManyToManyField、反向 ForeignKey
能否用于多对多 不能
能否用于反向一对多 不能
内存占用 较低(单行不膨胀) 较高(要存所有关联对象)
是否支持自定义查询 有限 支持 Prefetch 对象
数据库压力 JOIN 可能较重 多条简单查询,通常更轻

一句话总结:select_related 用 JOIN 一次拿完,适合单值关系;prefetch_related 分两次拿,适合多值关系。

判断标准很简单:关联对象只有一个,用 select_related;关联对象有多个,用 prefetch_related。

order.user 是单个用户,用 select_related('user')order.items.all() 是多个订单项,用 prefetch_related('items')user.order_set.all() 是反向多个订单,用 prefetch_related('order_set')

用错了会怎样

prefetch_related 去预取一个 ForeignKey 行不行?行,但没必要。它会多发一条 SQL,虽然也能工作,但比 select_related 多一次数据库往返。

select_related 去预取多对多呢?直接报错:

FieldError: Cannot resolve keyword 'items' into field. Choices are: ...

Django 在解析 select_related 的参数时,只会查 ForeignKey 和 OneToOneField。遇到多对多或反向关系,它找不到对应的字段,直接抛异常。

还有一种常见错误:在 prefetch_related 里套 select_related 的语法:

Order.objects.prefetch_related('user__profile')  # 可以,会分步预取
Order.objects.select_related('items__product')   # 报错,items 不是单值关系

如果一条链路上既有单值又有多值,可以混用:

Order.objects.select_related('user').prefetch_related('items__product')

Django 会先 JOIN 用户,再分步预取订单项和商品。这是处理复杂关联的标准写法。

Prefetch 对象:更精细的控制

prefetch_related 默认会把关联对象的全部字段和全部记录都取回来。如果关联数据量很大,或者你只需要其中一部分,可以用 Prefetch 对象做精细控制。

from django.db.models import Prefetch

orders = Order.objects.prefetch_related(
   Prefetch(
       'items',
       queryset=OrderItem.objects.filter(quantity__gt=1).select_related('product'),
       to_attr='big_items'
   )
)

这里做了三件事:

  1. 只预取 quantity > 1 的订单项,不是全部。
  2. 预取时顺便用 select_relatedproduct 也加载了,避免后续再查。
  3. 结果存到 order.big_items 这个自定义属性里,而不是默认的 order.items

之后用 order.big_items 访问,得到的是一个列表,已经过滤好、关联好,且不再触发 SQL。

Prefetch 的价值在于:它让你在“分步查询”这个框架里,仍然能对第二步查询做定制。这是 select_related 做不到的——JOIN 的 SQL 由 Django 生成,你很难在中间插条件。

性能对比:不只是 SQL 条数

很多人以为 select_related 一定比 prefetch_related 快,因为 SQL 条数少。这不一定。

select_related 用 JOIN,如果关联表很大,或者关联链很长,JOIN 出来的结果集可能非常宽。比如 select_related('user', 'product', 'address', 'payment'),每行订单都会带上四张表的全部字段。字段越多,网络传输和内存解析的开销越大。

prefetch_related 分步查,每条 SQL 只取一张表的字段,结果集更干净。在关联数据量大、字段多的情况下,反而可能更快。

另一个因素是索引。JOIN 的 ON 条件如果没有索引,数据库要做全表扫描。而 prefetch_relatedIN 查询通常能用到主键索引,效率很高。

所以选择依据不是“哪个更快”,而是“哪个正确”。单值关系用 select_related,多值关系用 prefetch_related。这是由关系类型决定的,不是性能调优的结果。

一个真实场景

我之前维护一个博客系统,首页要展示最新的 20 篇文章,每篇文章显示作者名、分类名、标签列表、评论数。

最初的代码:

posts = Post.objects.all()[:20]
for post in posts:
   print(post.author.username)
   print(post.category.name)
   for tag in post.tags.all():
       print(tag.name)
   print(post.comments.count())

authorcategory 是外键,可以用 select_relatedtags 是多对多,comments 是反向一对多,需要用 prefetch_related

优化后:

posts = Post.objects.select_related('author', 'category').prefetch_related('tags', 'comments')[:20]

SQL 从 1 + 20 + 20 + 20 + 20 = 81 条降到 1 + 20 + 20 + 20 = 61 条?不是。

select_related 把 author 和 category 合并进第一条 SQL,所以取文章是 1 条。prefetch_related('tags') 是 1 条:SELECT * FROM tag WHERE post_id IN (...)prefetch_related('comments') 是 1 条:SELECT * FROM comment WHERE post_id IN (...)

总共 3 条 SQL。页面加载从 4 秒降到 200 毫秒。

注意 comments.count() 的问题。prefetch_related('comments') 会把所有评论对象加载到内存,如果只是想显示数量,用 annotate 更合适:

from django.db.models import Count

posts = Post.objects.select_related('author', 'category').prefetch_related('tags').annotate(comment_count=Count('comments'))[:20]

这样评论数由数据库直接算好,不用把评论对象全部取回来。prefetch_related 适合“需要访问关联对象本身”的场景,annotate 适合“只需要聚合值”的场景。

怎么发现 N+1

Django Debug Toolbar 是最直观的工具。装好之后,页面侧边栏会显示每个请求执行了多少条 SQL。如果看到几十上百条相似结构的查询,基本就是 N+1。

没有 toolbar 的话,可以用 connection.queries

from django.db import connection

# 执行查询
list(orders)

print(len(connection.queries))
for q in connection.queries:
   print(q['sql'])

connection.queries 只在 DEBUG=True 时记录,生产环境不适用。生产环境可以用 django-silk 或者 APM 工具做 SQL 监控。

还有一个办法:在测试里断言查询次数。

from django.test import TestCase
from django.assertNumQueries import assertNumQueries

class OrderTest(TestCase):
   def test_order_list_queries(self):
       with self.assertNumQueries(3):
           orders = Order.objects.select_related('user').prefetch_related('items')
           for order in orders:
               order.user.username
               list(order.items.all())

如果实际查询次数和预期不符,测试直接失败。这个方法能把 N+1 挡在上线之前。

什么时候不用预取

预取不是越多越好。有几个场景要谨慎:

第一,关联数据量极大。比如一篇热门文章有十万条评论,prefetch_related('comments') 会一次性把十万条评论全部加载到内存,直接 OOM。这时候应该分页,或者用 annotate 只取数量。

第二,预取了但没用到select_relatedprefetch_related 都有开销。如果某个关联对象在后续逻辑里根本没被访问,预取就是浪费。只预取确定会用到的关系。

第三,查询集只取一条记录Post.objects.get(id=1) 本身只发一条 SQL,即使有 N+1,N 也是 1。预取的意义不大。预取主要在列表页、循环访问的场景下发挥作用。

最后

select_relatedprefetch_related 的区别,本质上是单值关系多值关系的区别,是一条 JOIN分步查询的区别。

名字像,但适用场景不重叠:ForeignKey 和 OneToOneField 用 select_related,ManyToManyField 和反向 ForeignKey 用 prefetch_related。混用、用错,轻则没效果,重则报错或内存暴涨。

处理列表页的时候,习惯性地问自己:这个循环里访问了哪些关联对象?它们是单个还是多个?单个的进 select_related,多个的进 prefetch_related。再配上 assertNumQueries 写个测试,N+1 就再也坑不到你了。

目录
相关文章
|
12天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
|
12天前
|
人工智能
千问办公官网入口:阿里AI办公QwenWork产品页和免费网页端链接
千问办公官网含两大入口:一是网页端(qwenwork.cn),即开即用,支持浏览器直接访问;二是阿里云产品页 https://t.aliyun.com/U/JNKJuO 提供免费/付费版详情、功能介绍及使用指南。
|
19天前
|
网络协议 Linux iOS开发
【2026实测】Wireshark下载+安装+汉化+使用教程(图文版,巨详细)
Wireshark 是一款免费开源的网络协议分析工具,可实时捕获、解析并可视化数据包,助你诊断网络故障、分析通信协议(如HTTP、DNS、TCP等)。支持Windows/macOS/Linux,含中文界面,新手入门便捷。(239字)
|
11天前
|
IDE 开发工具
Qoder 上线 Sonus 模型,Computer Use 能力全面增强
Qoder国际版上线全新内置大模型Sonus(/ˈsoʊnəs/),全球领先,专精超长任务执行与电脑操作(Computer Use)。配合Qoder桌面端0.2.3版本,可自主完成编程、金融建模、科研及表格制作等复杂工作。现全面支持Qoder全系产品,效率提升3.2倍。
1414 8
Qoder 上线 Sonus 模型,Computer Use 能力全面增强
|
14天前
|
缓存 人工智能 自然语言处理
阿里云qwen3.8-flash大模型介绍:模型能力、模型价格、免费额度与最新活动
本文是阿里云百炼平台Qwen3.8-Flash大模型的选型接入指南,作为兼顾性能与响应速度的高性价比多模态模型,它支持百万级上下文窗口、全场景多模态输入与完整智能体能力矩阵,适配编程辅助、智能体协作等核心场景。文中同步梳理了最新下调的阶梯定价、夜间4折等优惠活动,搭配OpenAI兼容流式调用示例,帮助开发者低成本快速落地高并发AI应用。
阿里云qwen3.8-flash大模型介绍:模型能力、模型价格、免费额度与最新活动
|
13天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
1988 15
|
7天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
|
18天前
|
人工智能 运维 BI
阿里云千问办公QwenWork深度解析:基于Qwen3.8,六大核心能力重构企业全自动化工作流与计费选型指南
传统AI办公工具大多停留在对话问答、文档摘要、简单文案生成层面,只能完成单点碎片化任务,无法自主拆解复杂业务流程,很难串联多工具、多文档、外部业务系统完成端到端完整工作交付。很多企业在落地AI办公的时候,需要组合多款不同工具,来回切换界面,手动复制粘贴中间结果,智能化改造落地门槛居高不下。千问办公QwenWork是整合多款智能体产品能力打造的一体化企业办公智能体平台,底层基座依托Qwen3.8大模型,打通桌面端Agent、云端Agent、企业协同Agent三种运行形态,不再局限简单问答,接收业务目标之后自主拆解任务步骤,调用各类工具,处理文档、表格、浏览器自动化、数据查询,直接输出可交付的办公
1688 4
|
12天前
|
人工智能 安全 JavaScript
DeepSeek Harness开源Agent运行框架实战:4种安装方式、WebUI启动、插件管理与排坑全流程
随着AI Agent技术快速发展,单纯依靠大模型对话能力,很难完成复杂的自动化任务。模型需要具备读取本地文件、执行脚本、访问网页、操作文件系统、拆分复杂任务并分步执行的能力。DeepSeek Harness,简称DSH,是开源的AI Agent执行运行框架,遵循“Agent = 大模型 + Harness执行底座”的设计理念,为大模型提供一套安全可控的工具调用、任务编排、沙箱执行与插件扩展能力。它提供Web可视化界面与完整命令行工具,支持插件化扩展,能够让大模型自主拆解复杂需求,调用各类工具分步完成目标,无论是本地电脑调试,还是部署在云服务器上长期运行智能体任务都十分合适。本文为从0到1完整保
882 0
|
14天前
|
缓存 JSON API
阿里云千问Qwen3.8‑Max深度解析:核心能力、订阅计费规则、API接入配置与生产落地完整教程
Qwen3.8‑Max作为千问系列新一代MoE架构旗舰基座,总参数量达到2.4万亿,激活参数950亿,是面向复杂专业任务、长周期智能体、工程级代码开发、多模态深度解析的高阶大模型,原生支持文本、图像、视频多模态输入,最大上下文窗口达到百万Token,最大输出Token支持131072,内置深度思考推理链路,在编程、科研、法律金融专业分析、长视频文档解析、自主Agent任务等场景能力表现突出。很多开发者在项目前期直接接入该旗舰模型,却对模型能力边界、多种计费模式、订阅套餐权益、API参数配置、上下文缓存优化缺乏完整认知,出现成本失控、接口报错、长文本信息丢失、深度思考模式额外消耗大量Token等
939 3