文章目录Django ORM 的 N1 问题——你每天都在踩但可能不知道导入语1 ~ 什么是 N1——用最简单代码复现1.1 模型定义1.2 触发 N1 的代码1.3 Django Debug Toolbar 实测2 ~ 根本原因——Django ORM 的懒加载2.1 什么是懒加载3 ~ 解决方案一select_related——SQL JOIN一锤子买卖3.1 语法3.2 适用场景——只适合外键和一对一3.3 真实效果4 ~ 解决方案二prefetch_related——额外查询 Python 层面组装4.1 什么场景用4.2 实例4.3 select_related vs prefetch_related 对比5 ~ 进阶——嵌套预加载和多层关联思考 总结结尾Django ORM 的 N1 问题——你每天都在踩但可能不知道文章简介N1 查询是 Django 项目中最隐蔽也最高频的性能杀手。表面看起来代码正常——获取 50 个用户然后显示每个用户的部门名称但实际上数据库被查询了 51 次而不是 2 次。本文从头拆解 N1 的成因——Django ORM 懒加载机制与关联对象访问的特性然后逐一分析select_relatedJOIN 方式预加载外键和prefetch_related额外查询预加载多对多的差异和适用场景。配有 Django Debug Toolbar 实测和真实事故——一个报表接口因为 N1 导致数据库连接池耗尽。 个人主页源码骑士❄专栏传送门《Android开发基础》《python基础课程》⭐️热衷从源码视角拆解技术底层原理将复杂架构讲得通俗易懂 源码骑士的简介5年Android Framework系统开发经验曾主导多项系统级性能优化专项技术栈覆盖Android系统全链路Binder/Handler/AMS/WMS/启动流程及Java后端全家桶Spring MyBatis Redis Oracle累计产出原创技术文章100篇文章以源码拆解为特色被读者评价为看一篇胜过啃一周文档导入语2021 年公司 CRM 系统的一个日报接口开始间歇性超时。运维反馈数据库连接池满了DBA 说没有慢查询。我打开 Django Debug Toolbar 一看——一个获取 100 个用户的接口执行了 101 条 SQL 查询。第一条获取用户列表后面 100 条是逐个查每个用户的部门名称。这就是 N1 问题。它最阴险的地方在于——你的代码看起来毫无问题。for user in users: print(user.department.name)就这一行背后是 100 次独立的数据库查询。这篇文章把 N1 的成因和两种解法select_related和prefetch_related讲清楚——不是背语法而是从 SQL 层面理解它们做了什么。1 ~ 什么是 N1——用最简单代码复现1.1 模型定义fromdjango.dbimportmodelsclassDepartment(models.Model):namemodels.CharField(max_length100)classEmployee(models.Model):namemodels.CharField(max_length100)departmentmodels.ForeignKey(Department,on_deletemodels.CASCADE)1.2 触发 N1 的代码# ❌ N1 问题——100 个员工 101 次查询employeesEmployee.objects.all()# ① 查一次SELECT * FROM employeeforempinemployees:print(emp.department.name)# ② 查 100 次每次访问 emp.department1.3 Django Debug Toolbar 实测安装django-debug-toolbar后访问这个页面查询次数: 101 第一次SELECT * FROM employee (1 条查询) 剩余 100 次SELECT * FROM department WHERE id 1 SELECT * FROM department WHERE id 2 ... SELECT * FROM department WHERE id 1002 ~ 根本原因——Django ORM 的懒加载2.1 什么是懒加载empEmployee.objects.get(id1)# 到目前为止只有一条 SQLSELECT * FROM employee WHERE id 1# emp.department 还没有被加载——它只是一个惰性占位符print(emp.department.name)# 此时才触发第二条 SQLSELECT * FROM department WHERE id emp.department_idDjango ORM 默认只加载当前对象关联的外键对象在被访问之前不会被查询。这本身不是 Bug——它是为了省去不必要的查询。但循环里逐个访问关联对象时就变成了 N1。Java 的 Hibernate 同样有懒加载——ManyToOne(fetch FetchType.LAZY)的行为和 Django 的 ForeignKey 懒加载原理一致。不同在于 Hibernate 的 N1 问题常用JOIN FETCH或BatchSize解决而 Django 有自己的一套工具。3 ~ 解决方案一select_related——SQL JOIN一锤子买卖3.1 语法# ✅ 1 次 JOIN 查询——2 条变成 1 条employeesEmployee.objects.select_related(department).all()forempinemployees:print(emp.department.name)# 不再触发额外查询生成的 SQLSELECTemployee.id,employee.name,employee.department_id,department.id,department.nameFROMemployeeINNERJOINdepartmentONemployee.department_iddepartment.id3.2 适用场景——只适合外键和一对一select_related适用这个只有 ForeignKey 和 OneToOneField 能用。它是通过 SQL JOIN 实现的——一次查询把主表和关联表的数据全拉回来。不支持 ManyToManyField 和反向关联。3.3 真实效果CRM 日报接口优化前后对比没有 select_related: 101 次查询接口耗时 820ms 加了 select_related: 1 次查询接口耗时 45ms4 ~ 解决方案二prefetch_related——额外查询 Python 层面组装4.1 什么场景用prefetch_related适用这些ManyToManyField、反向 ForeignKey、以及你不想用 JOIN 聚合外键的场景。它不是用 SQL JOIN而是——额外发一条查询然后在 Python 层面把主表和关联表的数据对应上。4.2 实例classArticle(models.Model):titlemodels.CharField(max_length200)tagsmodels.ManyToManyField(Tag)classTag(models.Model):namemodels.CharField(max_length50)# ❌ N1100 篇文章、每篇 3 个标签 301 次查询articlesArticle.objects.all()forarticleinarticles:print(article.tags.all())# 每篇文章都触发一次独立的 ManyToMany 查询# ✅ prefetch_related2 次查询搞定articlesArticle.objects.prefetch_related(tags).all()# 第 1 次SELECT * FROM article# 第 2 次SELECT * FROM article_tags WHERE article_id IN (1,2,3,...,100)# → 所有 100 篇文章的标签一次性查出 → Django 内部匹配到各自的文章4.3select_relatedvsprefetch_related对比select_relatedprefetch_related实现方式SQL JOIN额外查询 Python 组装支持关系类型ForeignKey、OneToOneManyToMany、反向外键、ForeignKey 也可以SQL 查询数1 条2 条或更多层嵌套什么时候用外键聚合简单主表行数不大多对多、反向外键、或不想用 JOIN 聚合时5 ~ 进阶——嵌套预加载和多层关联# Employee → Department → Company三层关联employeesEmployee.objects.select_related(department__company# 双下划线表示跨一层关系再预加载下一层).all()# 生成的 SQL# SELECT * FROM employee# INNER JOIN department ON ...# INNER JOIN company ON ...prefetch_related也支持嵌套# 预加载文章 → 标签 → 每个标签的分类articlesArticle.objects.prefetch_related(tags__category)思考 总结N1 问题的三个核心认知懒加载是必要的优化但在循环中逐个访问关联对象就会被放大为性能杀手。遇到for obj in queryset: obj.related_field直接加select_related或prefetch_related。select_related SQL JOINprefetch_related 额外查询 Python 组装。外键用前者多对多用后者嵌套用双下划线。装个 Django Debug Toolbar——它让你看到每次请求执行了多少条 SQL。N1 问题从来不是靠肉眼排查的而是靠数字暴光的。结尾N1 问题到这里拆解完毕。感谢阅读源码骑士 — 源码级拆解从底层看透技术关注跟博主一起从源码视角深耕底层原理❤️点赞让优质内容被更多人看见⭐收藏核心知识点存好随用随查评论分享你的经验或疑问一起交流一键四连别忘了给博主一键四连️寄语一条 SQL 变成 100 条——这就是 N1 的可怕之处。一条select_related把它变回一条。结语N1 是 Django 项目性能优化的头号切入点。select_related和prefetch_related就是你的两把工具。下篇讲一个请求从浏览器到数据库的完整旅程。一键四连