90%的Django ORM性能问题源于未合理使用select_related、prefetch_related或缺失复合索引;select_related适用于正向外键/一对一的单次JOIN查询,prefetch_related在大数据量时可能因两次查询反而更慢,索引需按查询条件组合创建并验证执行计划。

直接说结论:90% 的 Django ORM 性能问题,不是代码写得不够“Pythonic”,而是没用对 select_related、prefetch_related,或者根本没建对索引——尤其是复合索引。
为什么 select_related 有时比 prefetch_related 快 10 倍
因为它是单次 JOIN 查询,数据库在一次扫描中就把主表 + 关联表字段全取回来。但前提是关联方向必须是“正向外键”或“一对一”,且字段在 SQL 层就能完成拼接。
- 适用场景:
Book.objects.select_related('author')(Book 有author_id外键);Order.objects.select_related('customer__address')(支持链式) - 不适用场景:反向关系(如
Author.objects.select_related('book_set')报错)、多对多(Tag和Article)、含聚合或annotate的子查询 - 容易踩的坑:过度链式,比如
select_related('a__b__c__d__e')可能导致 JOIN 表过多、结果集爆炸;PostgreSQL 对 JOIN 数量有限制,MySQL 容易触发临时表 - 验证方式:打开
django-debug-toolbar,看 SQL 是否变成一条带多个JOIN的语句,而不是多条独立SELECT
prefetch_related 在什么情况下反而更慢
它本质是“两次查询 + Python 内存匹配”,所以当关联数据量极大(比如一个 Commune 对应 5 万条 CommuneMeteo 记录),或网络延迟高时,两次查询的开销可能超过多次小查询。
- 典型错误写法:
Commune.objects.prefetch_related('communemeteo_set')直接拉全量气象数据,内存暴涨、序列化卡顿 - 正确做法:配合
Prefetch过滤 + 限制字段:Prefetch('communemeteo_set', queryset=CommuneMeteo.objects.filter(date__range=(s, e)).only('temp_min', 'temp_max')) - 注意:
prefetch_related不会减少数据库扫描行数,只减少查询次数;如果底层WHERE条件没走索引,它只是把慢查询从 N+1 变成 2 次慢查询
索引失效的三个隐蔽原因
Django 模型里加了 db_index=True ≠ 查询就走索引。真正起作用的是数据库执行计划是否命中你预设的路径。
SkillSub Pro - Python 题解与代码注释双功能技能功能概述SkillSub Pro - Python 题解与代码注释双功能技能是一项面向实际任务的技能,主要用于SkillSub Pro 是一个 Python 题解生成与代码注释的 双功能合体技能 ,专为学生、算法学习者和开发者设计;✅ 一个技能,两种用途 :;核心要点📝 题解模式 :输入题目/题号,自动生成完整 Python 题解(含详细注释、解题思路、复杂度分析);💬 注释模式 :输入 Python 代码,自动添加详细中。它将相关步骤、
立即学习“Python免费学习笔记(深入)”;
-
date__range+commune_id=xxx这类组合条件,单列索引(哪怕两个都加了db_index=True)完全无效,必须建复合索引:models.Index(fields=['commune_id', 'date']) - 使用
order_by('-date')但索引是['date']升序?某些数据库(如 MySQL 8.0 之前)无法复用该索引做逆序扫描 - 在
filter()中用了函数包装字段,比如date__year=2025或Lower('name')__contains='abc',会导致索引失效,改用数据库原生函数或生成计算字段加索引
百万级聚合别硬扛 ORM,先看 SQL 能不能干
像积温 GDD(Sum((F('temp_min') + F('temp_max')) / 2 - 10))这类跨行计算,Django Subquery + OuterRef 在百万数据上极易触发全表扫描,不如交给数据库原生聚合。
- 优先尝试:
CommuneMeteo.objects.filter(...).aggregate(gdd=Sum(...))—— 单次聚合,不涉及外层循环 - 外层要为每个
Commune算 GDD?用原生 SQL 子查询或extra(tables=..., where=...)更可控,避免 ORM 生成冗余GROUP BY和嵌套SELECT - 实在跑不动:导出关键字段到 Pandas,在 Python 层分块计算,再回写;比在数据库里拖着百万行反复 JOIN 更稳
最常被忽略的一点:优化永远从 EXPLAIN 开始,不是从改代码开始。没看过数据库执行计划就加 select_related 或建索引,大概率是在给性能埋雷。


















