Django ORM 的 annotate、F、Q、Subquery 怎么用?
简化版
Django ORM 的「查询表达式」让你把计算下推到数据库,而不是把数据全查出来在 Python 里算——这是把「几万行 + 循环」变成「一条 SQL」的关键。四件套:① annotate():给每一行加一个计算出来的字段(Book.objects.annotate(n=Count("authors")),结果仍是 QuerySet,每个对象多一个属性);② aggregate():把整个查询集聚合成一个字典(Book.objects.aggregate(avg=Avg("price")) 返回 {"avg": 32.5},不是 QuerySet);③ F() 表达式:引用「数据库里的字段值」——F("views") + 1 生成的是 SET views = views + 1 这样的 SQL,在数据库层原子完成,避免了「读出来 +1 再写回」的竞态,还能用于字段间比较(filter(sold__gt=F("stock")));④ Q() 对象:构造复杂条件(filter(Q(a=1) | Q(b=2))、~Q(...) 取反),也是动态拼接查询条件的唯一正确方式。再往上是 Subquery/OuterRef/Exists:把「每一行都要查一次关联表」的需求写成关联子查询,一次 SQL 解决。最经典的坑是「多个 annotate 导致的重复计数」:对两个不同的多对多关系分别 Count() 时,SQL 会做两次 JOIN 产生笛卡尔积,结果被放大成乘积——必须用 Count("x", distinct=True) 或拆成子查询。调试的关键手段是 print(qs.query) 看生成的 SQL、qs.explain() 看执行计划。核心记忆:annotate 加列、aggregate 出汇总、F 引用字段值(原子更新)、Q 组合条件;多个 annotate 会重复计数,用 distinct 或 Subquery。
详细版
四类查询表达式:
| 工具 | 作用 | 返回 |
|---|---|---|
annotate() | 给每行加计算字段 | QuerySet(对象多一个属性) |
aggregate() | 整体聚合 | dict(终结操作) |
F() | 引用数据库字段的值 | 表达式(用于 update/filter/annotate) |
Q() | 组合查询条件(&/|/~) | 条件对象 |
Subquery/OuterRef | 关联子查询 | 表达式 |
Exists() | 存在性判断(比 __in 高效) | 表达式 |
from django.db.models import (Count, Sum, Avg, Max, F, Q, Value,
Subquery, OuterRef, Exists, Case, When, Window)
from django.db.models.functions import Coalesce, Rank
# ① ★annotate:给每行加一列★
books = Book.objects.annotate(author_count=Count("authors"))
for b in books:
print(b.title, b.author_count) # ★每个对象多了一个属性★
# ② ★aggregate:整体汇总(终结操作,返回 dict)★
stats = Book.objects.aggregate(
total=Count("id"), avg_price=Avg("price"), max_price=Max("price"))
# {'total': 120, 'avg_price': Decimal('32.5'), 'max_price': Decimal('99')}
# ③ ★F 表达式:在数据库层完成计算(原子)★
# ✗ 有竞态:读出来 → Python 里 +1 → 写回
book = Book.objects.get(pk=1)
book.views += 1 # ★两个请求会互相覆盖★
book.save()
# ✓ 原子更新
Book.objects.filter(pk=1).update(views=F("views") + 1) # SET views = views + 1
# F 还能做字段间比较、跨表引用
Book.objects.filter(sold__gt=F("stock")) # ★卖出 > 库存★
Book.objects.filter(price__gt=F("publisher__min_price")) # ★跨关系★
Book.objects.update(price=F("price") * Decimal("1.1")) # 批量涨价
# ④ ★Q 对象:复杂条件 + 动态拼接★
Book.objects.filter(Q(title__icontains="python") | Q(author__name="Guido"))
Book.objects.filter(~Q(status="draft")) # ★取反★
# ★动态构建(唯一正确方式)★
cond = Q()
if keyword: cond &= Q(title__icontains=keyword)
if year: cond &= Q(pub_year=year)
Book.objects.filter(cond)
# ⑤ ★Subquery + OuterRef:关联子查询★
latest = Comment.objects.filter(post=OuterRef("pk")).order_by("-created")
posts = Post.objects.annotate(
latest_comment=Subquery(latest.values("content")[:1]), # ★取每篇最新评论★
latest_at=Subquery(latest.values("created")[:1]))
# ⑥ ★Exists:存在性判断(比 __in 子查询高效)★
has_comment = Comment.objects.filter(post=OuterRef("pk"))
Post.objects.filter(Exists(has_comment)) # ★只判断有没有★
Post.objects.filter(~Exists(has_comment)) # 没有评论的
# ⑦ ★条件表达式 Case/When★
Order.objects.annotate(level=Case(
When(amount__gte=10000, then=Value("VIP")),
When(amount__gte=1000, then=Value("普通")),
default=Value("新手")))
# ⑧ ★窗口函数(2.0+)★
Sale.objects.annotate(rank=Window(expression=Rank(),
partition_by=[F("region")],
order_by=F("amount").desc()))
# ⑨ ★看 SQL / 执行计划★
print(qs.query) # ★生成的 SQL★
print(qs.explain()) # ★执行计划(2.0+)★
from django.db import connection
print(connection.queries[-1]) # ★DEBUG=True 时★
⚠️ 三个必须记住的点:①
annotate和aggregate是两个层次的东西:annotate是**「给每一行加一列」,返回的仍然是 QuerySet(可以继续filter、order_by);aggregate是「把整个结果集压成一个字典」,是终结操作**,之后不能再链式调用。一个常见需求「先分组再聚合」其实是values("字段").annotate(聚合)——values()在annotate()之前决定 GROUP BY 的分组键,顺序反了结果完全不同。②F()的核心价值是「在数据库里完成读-改-写」:book.views += 1; book.save()是「查出来 → Python 加 1 → 写回」,两个并发请求会互相覆盖(丢失更新);而update(views=F("views") + 1)生成SET views = views + 1,由数据库保证原子性。代价是F()更新后内存中的对象值是过期的(book.views还是旧值,需要refresh_from_db())。③ 多个annotate聚合会导致重复计数:对两个不同的多对多/一对多关系分别Count(),Django 会生成两次 JOIN,行数变成笛卡尔积,两个计数都被放大(比如 3 个作者 × 5 个标签,两个 Count 都变成 15)。解法是Count("x", distinct=True)(简单但仍有性能开销)或拆成Subquery(更高效)。
完整版教学
一、annotate 与 aggregate:加列 vs 汇总
★ 两者的本质区别:
★annotate★:给结果集的★每一行★增加一个计算列
Book.objects.annotate(n=Count("authors"))
→ SQL: SELECT book.*, COUNT(author.id) AS n
FROM book LEFT JOIN ... GROUP BY book.id
→ ★返回 QuerySet★,每个对象多一个 .n 属性
→ ★可以继续 filter / order_by / values★
★aggregate★:把★整个结果集★压成一个字典
Book.objects.aggregate(avg=Avg("price"))
→ SQL: SELECT AVG(price) FROM book
→ ★返回 dict,是终结操作★(立即执行,之后不能再链式)
★ 分组统计的正确写法(★高频考点★):
需求:每个出版社有多少本书
✓ Publisher.objects.annotate(n=Count("book")) # 按主表分组
✓ Book.objects.values("publisher").annotate(n=Count("id"))
→ ★values() 在 annotate() 之前 → 它决定 GROUP BY 的字段★
→ SQL: SELECT publisher_id, COUNT(id) FROM book GROUP BY publisher_id
★ 顺序反了完全不同:
Book.objects.annotate(n=Count("id")).values("publisher")
→ ★先按主键分组(每行一组)★,n 恒为 1,然后才取 publisher 列
→ ★这是最经典的错误之一★
★ annotate 之后可以继续过滤(HAVING):
Publisher.objects.annotate(n=Count("book")).filter(n__gte=10)
→ ★annotate 之后的 filter 会变成 HAVING★
→ 而 annotate ★之前★ 的 filter 是 WHERE(★先过滤再聚合★)
★ 顺序影响结果:
.filter(book__pub_year=2024).annotate(n=Count("book"))
→ ★只统计 2024 年的书★(WHERE 先生效)
.annotate(n=Count("book")).filter(n__gte=10)
→ ★统计全部书,再筛出 >= 10 本的出版社★(HAVING)
★ 常用聚合函数:
Count / Sum / Avg / Max / Min / StdDev / Variance
Count("x", distinct=True) ★去重计数★
Count("x", filter=Q(status="ok")) ★条件聚合(2.0+,比 Case/When 简洁)★
Sum("price", default=0) ★4.0+:空集时返回 0 而不是 None★
Coalesce(Sum("price"), Value(0)) 老版本的写法
★ 聚合结果为 None 的坑:
Book.objects.filter(pk=-1).aggregate(s=Sum("price"))
→ ★{'s': None}★(不是 0!)
✓ Sum("price", default=0) 或 Coalesce(Sum("price"), 0)
annotate 和 aggregate 是两个层次的操作:annotate 给每一行加一列(返回 QuerySet,可以继续链式调用),aggregate 把整个结果集压成一个字典(终结操作)。最经典的错误是「分组统计」时把 values() 和 annotate() 的顺序写反:values("publisher").annotate(n=Count("id")) 是「按 publisher 分组计数」(values() 在前决定 GROUP BY 的字段),而 annotate(...).values("publisher") 是「先按主键分组(每行一组,n 恒为 1)再取列」——结果完全不同。同样重要的是 filter 相对 annotate 的位置:annotate 之前的 filter 是 WHERE(先过滤再聚合),之后的 filter 是 HAVING(先聚合再筛选)。还有个易忘的细节:空结果集的 Sum 返回 None 而不是 0,要用 Sum("price", default=0)(4.0+)或 Coalesce。
二、F 表达式:把计算下推到数据库
★ 核心价值一:原子更新(★避免丢失更新★)
✗ 有竞态的写法:
p = Product.objects.get(pk=1) # ① SELECT stock → 100
p.stock -= 1 # ② Python 里算 → 99
p.save() # ③ UPDATE SET stock = 99
★ 两个并发请求:都读到 100,都写回 99 → ★少扣了一次★
✓ F 表达式:
Product.objects.filter(pk=1).update(stock=F("stock") - 1)
→ SQL: UPDATE product SET stock = stock - 1 WHERE id = 1
→ ★数据库层的原子操作,并发安全★
★ 配合条件防止扣成负数:
updated = Product.objects.filter(pk=1, stock__gte=1).update(
stock=F("stock") - 1)
if updated == 0: # ★update 返回受影响行数★
raise OutOfStock()
→ ★这是"乐观并发控制"的经典写法★
★ 核心价值二:字段间比较
Book.objects.filter(sold__gt=F("stock")) # 卖出 > 库存
Order.objects.filter(paid_at__lt=F("created_at")) # 数据异常检测
Book.objects.filter(price__gt=F("publisher__min")) # ★跨关系引用★
★ 核心价值三:批量计算,不把数据拉到 Python
✗ for b in Book.objects.all(): b.price *= 1.1; b.save() # ★N 次 UPDATE★
✓ Book.objects.update(price=F("price") * Decimal("1.1")) # ★1 次 UPDATE★
★ 三个使用陷阱:
① ★F 更新后内存对象是过期的★
p.stock = F("stock") - 1
p.save()
print(p.stock) # ★<CombinedExpression>,不是数字!★
✓ p.refresh_from_db() # 重新读取
② ★F 表达式不能用在 Python 逻辑里★
if p.stock > F("stock"): # ✗ 无意义
③ ★update() 不触发 save() 信号和 auto_now★
Book.objects.update(...) → ★不触发 pre_save/post_save★
→ ★auto_now 字段不会更新★
✓ 需要更新时间就显式加:update(price=F("price")*2, updated_at=timezone.now())
★ 相关:数据库函数(django.db.models.functions)
Coalesce(F("nickname"), F("username")) 取第一个非 NULL
Concat(F("first"), Value(" "), F("last")) 拼接
Lower / Upper / Length / Substr / Trim
Now() / TruncDate / TruncMonth / Extract ★时间处理(分组按天/月)★
Cast(F("x"), FloatField())
Greatest / Least
★ 例:按月统计
Order.objects.annotate(m=TruncMonth("created")).values("m") \
.annotate(n=Count("id")).order_by("m")
F() 表达式的核心价值是把计算下推到数据库,有三个用途。① 原子更新(避免丢失更新):p.stock -= 1; p.save() 是「读 → Python 计算 → 写回」,两个并发请求都读到 100 就都写回 99,少扣了一次;而 update(stock=F("stock") - 1) 生成 SET stock = stock - 1,由数据库保证原子性。配合 filter(stock__gte=1) 并检查 update() 的返回值(受影响行数),就是乐观并发控制的经典写法。② 字段间比较(filter(sold__gt=F("stock")),还能跨关系引用)。③ 批量计算(一次 UPDATE 代替 N 次)。三个陷阱要记住:F 更新后内存中的对象值是表达式对象而不是数字(要 refresh_from_db())、F 表达式不能用在 Python 逻辑里、以及 update() 不触发信号也不更新 auto_now 字段。配套的还有 django.db.models.functions 里的数据库函数(Coalesce、Concat、TruncMonth 用于按月分组等)。
三、Q 对象与动态查询
★ 基本用法:
Q(a=1) & Q(b=2) AND
Q(a=1) | Q(b=2) ★OR(filter 的多个参数天然是 AND,OR 只能用 Q)★
~Q(a=1) NOT
Q(a=1) ^ Q(b=2) XOR(4.1+)
Book.objects.filter(Q(title__icontains="py") | Q(desc__icontains="py"))
★ 位置规则:
★Q 对象必须放在关键字参数之前★
✓ filter(Q(a=1) | Q(b=2), status="ok")
✗ filter(status="ok", Q(a=1)) # ★SyntaxError★
★ ★动态构建查询(Q 的最大价值)★:
def search(keyword=None, year=None, tags=None, min_price=None):
cond = Q() # ★空 Q 等于"无条件"★
if keyword:
cond &= Q(title__icontains=keyword) | Q(author__name__icontains=keyword)
if year:
cond &= Q(pub_year=year)
if tags:
cond &= Q(tags__in=tags)
if min_price is not None:
cond &= Q(price__gte=min_price)
return Book.objects.filter(cond).distinct()
★ 对比字典拼接的局限:
filters = {}
if year: filters["pub_year"] = year
Book.objects.filter(**filters) # ✓ 能拼 AND
→ ★但拼不出 OR 和 NOT★ → 复杂条件必须用 Q
★ 多值 OR 的两种写法:
from functools import reduce
from operator import or_
cond = reduce(or_, [Q(name__icontains=k) for k in keywords])
# 或
cond = Q()
for k in keywords: cond |= Q(name__icontains=k)
★ ★filter 链式 vs 一次 filter(多对多时结果不同!)★
# 找"既有标签A又有标签B"的书
✗ Book.objects.filter(tags__name="A", tags__name="B")
→ ★同一行的 tags.name 不可能同时是 A 和 B → 结果为空★
✓ Book.objects.filter(tags__name="A").filter(tags__name="B")
→ ★两次 JOIN,各自匹配 → 正确★
★ 规则:对★多值关系(多对多、反向外键)★
- 一次 filter 里的多个条件 → ★作用于同一条关联记录★
- 链式 filter → ★每次 filter 独立 JOIN★
★ 这是 Django ORM 最容易踩的语义坑之一
★ distinct 的必要性:
跨多值关系的 filter 会因 JOIN 产生★重复行★
Book.objects.filter(tags__name__in=["A", "B"])
→ 一本书有两个匹配标签 → ★返回两次★
✓ 加 .distinct()
★ 但 distinct() 有性能代价,且和 order_by 组合时要小心
Q 对象是构造复杂条件和动态查询的唯一正确方式:filter() 的多个关键字参数天然是 AND,OR 和 NOT 只能靠 Q(注意 Q 对象必须放在关键字参数之前,否则语法错误)。它最大的价值是动态构建:用 cond = Q() 起手(空 Q 等于无条件),按需 &=/|= 累加——字典拼接只能拼 AND,拼不出 OR 和 NOT。这里必须讲一个 Django ORM 最容易踩的语义坑:对多值关系(多对多、反向外键),「一次 filter 里的多个条件」作用于同一条关联记录,而「链式 filter」每次独立 JOIN——所以「既有标签 A 又有标签 B」必须写成 .filter(tags__name="A").filter(tags__name="B"),写成一次 filter 的话结果永远为空。另外跨多值关系的 filter 会因 JOIN 产生重复行,通常需要 .distinct()。
四、Subquery、OuterRef 与 Exists
★ 场景:每一行都需要关联表的某个值 → 用关联子查询代替 N+1
✗ N+1 写法:
for post in Post.objects.all():
latest = post.comments.order_by("-created").first() # ★每行一次查询★
✓ Subquery + OuterRef:
latest = Comment.objects.filter(post=OuterRef("pk")).order_by("-created")
posts = Post.objects.annotate(
latest_content=Subquery(latest.values("content")[:1]),
latest_at=Subquery(latest.values("created")[:1]),
)
→ ★一条 SQL 搞定★
★ 三个要点:
① ★OuterRef("pk")★ 引用外层查询的字段(★延迟解析★)
② ★.values("字段")★ —— 子查询只能返回★一列★
③ ★[:1]★ —— 子查询只能返回★一行★(LIMIT 1)
→ 少了任何一个都会报错
★ ★Exists:只判断"有没有",比 __in 高效★
✗ Post.objects.filter(id__in=Comment.objects.values("post_id"))
→ ★子查询要物化整个 id 列表★(数据量大时很慢)
✓ has_c = Comment.objects.filter(post=OuterRef("pk"))
Post.objects.filter(Exists(has_c))
→ ★SQL 的 EXISTS:找到一条就短路返回★
★ 3.0+ 可以直接放进 filter;老版本要先 annotate 再 filter:
.annotate(has_c=Exists(has_c)).filter(has_c=True)
★ Subquery 的类型问题:
Subquery 有时推断不出字段类型 → 需要显式指定
Subquery(qs.values("price")[:1], output_field=DecimalField())
★ 做算术运算时尤其需要
★ 聚合子查询(★绕开重复计数★):
✗ Publisher.objects.annotate(
n_books=Count("book"), n_authors=Count("book__authors"))
→ ★两个 JOIN → 笛卡尔积 → 两个数都错★
✓ 用 Subquery:
books = Book.objects.filter(publisher=OuterRef("pk")).values("publisher")
n_books = books.annotate(c=Count("*")).values("c")
Publisher.objects.annotate(n_books=Subquery(n_books))
✓ 或者简单场景用 distinct:
Count("book", distinct=True), Count("book__authors", distinct=True)
★ 相关的性能对比(★经验值★):
┌────────────────────────┬──────────────────────────────┐
│ 写法 │ 特点 │
├────────────────────────┼──────────────────────────────┤
│ 循环里查(N+1) │ ★最慢★,N 次往返 │
│ prefetch_related │ 2 次查询,★内存里组装★ │
│ Subquery │ ★1 次查询★,但子查询可能较慢 │
│ Exists │ ★存在性判断最优★ │
│ __in + 子查询 │ 需要物化列表,大数据集慢 │
└────────────────────────┴──────────────────────────────┘
→ ★没有绝对最优,要看数据量和索引,用 explain() 验证★
Subquery + OuterRef 用来把「每一行都要查一次关联表」变成一条 SQL。三个要点缺一不可:OuterRef("pk") 引用外层查询的字段、.values("字段") 让子查询只返回一列、[:1] 让子查询只返回一行。Exists() 用于存在性判断,比 __in 子查询高效得多——__in 需要物化整个 id 列表,而 SQL 的 EXISTS 找到一条就短路返回。Subquery 还能绕开多个 annotate 的重复计数问题:与其用 Count(..., distinct=True),不如把每个聚合写成独立的子查询(更高效且语义清晰)。注意 Subquery 有时推断不出字段类型,做算术运算时要显式指定 output_field。最后要有个客观认识:这几种写法没有绝对最优(N+1、prefetch、Subquery、Exists 各有适用场景),要用 explain() 结合实际数据量和索引来验证。
五、聚合的经典陷阱
★ 陷阱一:多个 annotate 导致重复计数(★最经典★)
class Book: authors = M2M(Author); tags = M2M(Tag)
Book.objects.annotate(na=Count("authors"), nt=Count("tags"))
→ SQL 会 JOIN 两次:
book JOIN book_authors JOIN book_tags
→ 一本书有 3 个作者、5 个标签 → ★JOIN 后有 15 行★
→ ★na = 15、nt = 15(都被放大了!)★
✓ 解法一:distinct
Count("authors", distinct=True), Count("tags", distinct=True)
→ 正确,但 ★DISTINCT 在大表上有性能代价★
✓ 解法二:拆成 Subquery(★推荐★)
✓ 解法三:分两次查询,在 Python 里合并
★ 陷阱二:Sum 遇到 JOIN 也会被放大
Publisher.objects.annotate(total=Sum("book__price"), n=Count("book__tags"))
→ ★total 会因为 tags 的 JOIN 被重复累加★
→ ★比 Count 更隐蔽,因为数字看起来"像是对的"★
✓ 同样用 Subquery 或分开查
★ 陷阱三:values() 的位置决定分组
✓ Book.objects.values("publisher").annotate(n=Count("id"))
→ GROUP BY publisher_id
✗ Book.objects.annotate(n=Count("id")).values("publisher")
→ ★GROUP BY book.id(每行一组)★,n 恒为 1
★ 还有一个隐形分组:如果 Model 有 Meta.ordering
Book.objects.values("publisher").annotate(n=Count("id"))
→ ★Django 会把 ordering 字段也加进 GROUP BY!★
→ 分组变细,结果错误
✓ 加 .order_by() (★空的 order_by 清除默认排序★)
★ 陷阱四:annotate 之后再 filter 的语义
.filter(A).annotate(n=Count(...)) → WHERE 先过滤(★统计的是过滤后的★)
.annotate(n=Count(...)).filter(n__gt=5) → HAVING(★统计全部,再筛结果★)
★ 两者结果完全不同,要想清楚业务需求
★ 陷阱五:聚合空集返回 None
.aggregate(s=Sum("price")) → 空集时 {'s': None}
✓ Sum("price", default=0)(4.0+)/ Coalesce(Sum("price"), Value(0))
★ 陷阱六:distinct() 与 order_by 的冲突
.values("a").distinct().order_by("b")
→ ★order_by 的字段会被加进 SELECT(PostgreSQL 会报错)★
✓ 先 .order_by() 清空默认排序
★ 排查这类问题的方法:
① ★print(qs.query) 看有几个 JOIN★
② 手动跑一遍 SQL,对比 Python 里算的结果
③ ★用 explain() 看执行计划★
④ django-debug-toolbar 看查询数量和耗时
聚合有六个经典陷阱。最经典的是「多个 annotate 导致重复计数」:对两个多对多关系分别 Count(),SQL 会 JOIN 两次产生笛卡尔积——3 个作者 × 5 个标签会让两个计数都变成 15;Sum 遇到 JOIN 同样会被放大,而且更隐蔽(数字看起来「像是对的」)。解法是 distinct=True(简单但有性能代价)或拆成 Subquery(推荐)。第二个高频陷阱是 values() 的位置决定 GROUP BY,而且有个隐形杀手:如果 Model 定义了 Meta.ordering,Django 会把排序字段也加进 GROUP BY,导致分组变细、结果错误——解法是加一个空的 .order_by() 清除默认排序。其余几个:filter 在 annotate 前后分别对应 WHERE 和 HAVING、空集聚合返回 None、以及 distinct() 与 order_by 的冲突。排查手段是 print(qs.query) 数一下有几个 JOIN。
六、查看 SQL 与性能调优
★ 看生成的 SQL(★调试第一步★):
print(qs.query) # ★QuerySet 的 SQL(参数已内联,仅供阅读)★
print(qs.explain()) # ★执行计划(2.0+)★
print(qs.explain(analyze=True)) # PostgreSQL:实际执行统计
from django.db import connection, reset_queries
reset_queries()
list(qs)
for q in connection.queries: # ★需要 DEBUG=True★
print(q["time"], q["sql"])
★ 生产环境不能开 DEBUG(★connection.queries 会无限增长导致内存泄漏★)
✓ 用 django-debug-toolbar(开发)/ APM / 慢查询日志(生产)
★ 减少查询次数:
select_related("fk") ★一对一/外键 → JOIN,1 次查询★
prefetch_related("m2m") ★多对多/反向 → 2 次查询 + Python 组装★
Prefetch("comments", queryset=Comment.objects.filter(...)) ★定制预取★
★ 详见"select_related 与 prefetch_related"专题
★ 减少传输的数据量:
.only("id", "title") ★只查这几列(其他列延迟加载)★
.defer("content") ★排除大字段★
.values("id", "title") ★返回 dict,不构造模型对象★
.values_list("id", flat=True) ★返回扁平列表(★取 id 列表最快★)★
★ only/defer 的坑:访问未加载的字段会★触发额外查询★(比不用还慢)
★ 大数据量的处理:
.iterator(chunk_size=2000) ★不缓存结果,流式读取(★省内存★)★
★ 注意:iterator() 与 prefetch_related 在老版本不兼容(4.1+ 支持)
.exists() ★只判断有没有(不取数据)★
.count() ★SQL COUNT(比 len(qs) 好,除非已经取过数据)★
★ len(qs) 会把所有对象加载进内存;qs.count() 只发一条 COUNT
★ 常见的性能反模式:
✗ if qs: → ★会把整个 QuerySet 加载进内存★
✓ if qs.exists():
✗ len(qs) → 同上
✓ qs.count()
✗ qs[0] → ★如果只要一条,用 .first()★
✗ 循环里查询 → select_related / prefetch_related / Subquery
✗ 在 Python 里过滤 → ★把条件下推到数据库★
✗ 全表 count() → 大表上 COUNT(*) 很慢,考虑估算或缓存
★ 索引相关:
Model.objects.filter(a=1, b=2) → ★需要 (a, b) 复合索引★
.order_by("-created") → ★排序字段要有索引★
__icontains → ★前缀 % 无法用索引★(考虑全文搜索)
★ 用 explain() 确认是否走了索引
★ 一个调优的标准流程:
① django-debug-toolbar 找出查询多/慢的页面
② print(qs.query) 看 SQL 是否符合预期
③ explain() 看是否走索引、有没有全表扫描
④ 优化:加索引 / select_related / 改写查询 / 缓存
⑤ ★再测一遍确认★
调优的第一步永远是看生成的 SQL:print(qs.query)(仅供阅读,参数是内联的)、qs.explain() 看执行计划、connection.queries 看实际执行的查询(需要 DEBUG=True,而生产环境开 DEBUG 会让 connection.queries 无限增长造成内存泄漏)。优化分三个方向:减少查询次数(select_related/prefetch_related/Subquery)、减少传输的数据量(only/defer/values/values_list(flat=True),注意 only 的坑是访问未加载字段会触发额外查询、反而更慢)、处理大数据量(.iterator(chunk_size=) 流式读取省内存)。几个必须记住的反模式:if qs: 和 len(qs) 会把整个结果集加载进内存(应该用 .exists() 和 .count())、循环里查询、以及在 Python 里做本该由数据库做的过滤。
记忆钩子:「Django ORM 的查询表达式就是★把计算下推到数据库★。四件套:★annotate 给每行加一列(返回 QuerySet 可继续链式)、aggregate 把整个结果集压成一个 dict(终结操作)★;★F() 引用『数据库里的字段值』★——F(‘views’)+1 生成 SET views = views + 1,★在数据库层原子完成,避免『读出来+1再写回』的丢失更新★(配合 filter(stock__gte=1) 检查 update 返回的行数就是乐观并发控制),还能做字段间比较和跨关系引用;★Q() 是构造 OR/NOT 和动态查询的唯一方式★(Q 必须放在关键字参数之前,用 cond = Q() 起手再 &=/|= 累加);★Subquery+OuterRef 三要素缺一不可:OuterRef(‘pk’) 引用外层、.values(‘列’) 只返回一列、[:1] 只返回一行★,而 ★Exists() 比 __in 子查询高效★(EXISTS 找到一条就短路,__in 要物化整个列表)。★最经典的坑是多个 annotate 导致重复计数★:对两个多对多分别 Count 会 JOIN 两次产生笛卡尔积,3 个作者×5 个标签让两个计数都变成 15;★Sum 被放大更隐蔽★(数字看着像对的)——解法是 distinct=True 或★拆成 Subquery★。★第二个高频坑是 values() 的位置决定 GROUP BY★:values(‘publisher’).annotate(n=Count(‘id’)) 才是按出版社分组,反过来是按主键分组(n 恒为 1);而且★如果 Model 有 Meta.ordering,排序字段会被偷偷加进 GROUP BY★ → 要加空的 .order_by() 清除。还有:★annotate 前的 filter 是 WHERE、之后的 filter 是 HAVING★;★空集聚合返回 None 不是 0★(用 default=0);★多值关系上『一次 filter 的多个条件』作用于同一条关联记录,而『链式 filter』各自独立 JOIN★(『既有标签A又有标签B』必须链式写)。调试三板斧:★print(qs.query) 数 JOIN、explain() 看索引、debug-toolbar 看查询数★;反模式:★if qs: 和 len(qs) 会加载全部★(用 exists()/count())。」
七、常见误区与追问
- 误区:
values("publisher").annotate(n=Count("id"))和annotate(n=Count("id")).values("publisher")只是写法顺序不同。 结果完全不同。Django 用annotate()之前的values()来决定GROUP BY的字段:前者生成GROUP BY publisher_id,得到「每个出版社有多少本书」;后者因为annotate之前没有values(),会按主键分组(GROUP BY book.id,每行自成一组),所以n恒等于 1,然后才从结果里取publisher列——这是分组统计最常见的错误。还有一个更隐蔽的版本:如果 Model 定义了Meta.ordering,Django 会把排序字段也加进GROUP BY,导致分组粒度变细、统计结果偏多——解法是加一个空的.order_by()显式清除默认排序。 - 误区:
obj.count += 1; obj.save()和update(count=F("count") + 1)效果一样。 前者有丢失更新(lost update) 的竞态:它是「SELECT 读出当前值 → 在 Python 里加 1 → UPDATE 写回一个固定值」三步,两个并发请求都读到 100 就都会写回 101,实际只加了 1 次。而F()生成的是UPDATE ... SET count = count + 1,读改写在数据库内部原子完成,并发安全。这在计数器、库存、余额这类场景是正确性问题而不是性能问题。配套技巧是用filter(stock__gte=1)加条件并检查update()返回的受影响行数——返回 0 说明条件不满足(库存不足),这就是乐观并发控制。要注意两个副作用:F()更新后内存里的对象属性变成了表达式对象(需要refresh_from_db()),以及update()不触发save()信号、也不会更新auto_now字段。 - 误区:
Book.objects.filter(tags__name="A", tags__name="B")能找出同时有两个标签的书。 结果永远为空。因为对多值关系(多对多、反向外键),一次filter()里的多个条件会被应用到「同一条关联记录」上——SQL 里只 JOIN 一次,然后要求同一行的tag.name既等于 A 又等于 B,这不可能成立。正确写法是链式 filter:.filter(tags__name="A").filter(tags__name="B"),每次filter()会产生独立的 JOIN,分别匹配不同的关联行。反过来,如果你要的是「有 A 或 B 标签」,那么一次 filter 配tags__name__in=["A","B"]是对的,但要加.distinct()——因为一本书匹配两个标签时会因 JOIN 返回两次。这是 Django ORM 最容易踩的语义坑之一,跨多值关系时先想清楚「条件是作用于同一条关联记录还是不同记录」。 - 误区:在一个
annotate()里同时统计两个关联的数量很自然。annotate(n_authors=Count("authors"), n_tags=Count("tags"))会生成两次 JOIN,结果集变成笛卡尔积——一本书有 3 个作者和 5 个标签时,JOIN 后有 15 行,于是n_authors和n_tags都变成 15。更隐蔽的是Sum:Sum("book__price")遇到另一个 JOIN 会重复累加,数字看起来「像是对的」(只是偏大),很容易蒙混过关直到财务对账时才发现。两种解法:Count("authors", distinct=True)(简单,但DISTINCT在大表上有性能代价,而且对Sum无效——去重的是行不是值);改用Subquery分别聚合(推荐,每个统计一条独立子查询,语义清晰且高效)。判断方法很简单:print(qs.query)数一数有几个 JOIN。 - 误区:
qs.count()和len(qs)差不多,if qs:判断是否为空也很自然。 三者的代价差别很大。qs.count()发出一条SELECT COUNT(*),只传回一个数字;len(qs)会执行完整查询、把所有对象加载进内存并实例化,然后才数个数——一张几十万行的表上这是灾难。if qs:同样会触发完整的加载(因为要判断真值就得知道有没有元素,Django 的实现是加载整个结果集并缓存)。判断是否存在应该用.exists()(生成SELECT 1 ... LIMIT 1,找到一条就返回)。唯一的例外是「你反正要遍历这个 QuerySet」——此时结果已经被缓存,len(qs)不会产生额外查询,反而qs.count()会多发一条 SQL。所以规则是:只判断存在用exists()、只要数量用count()、已经要用数据了就直接len()。 - 追问:
Subquery和prefetch_related该怎么选? 看你需要「关联对象的全部」还是「一个聚合/单值」。prefetch_related适合「我要用到关联对象本身」:比如列表页要展示每篇文章的所有标签——它发 2 条查询(主表一条、关联表一条WHERE id IN (...)),然后在 Python 内存里组装,之后访问post.tags.all()不再查库。Subquery适合「每行只需要一个值」:每篇文章的最新评论内容、评论数、最高分——它把结果合并进一条 SQL,避免了传输大量关联对象。性能上没有绝对赢家:prefetch_related传输的数据多但 SQL 简单、易走索引;Subquery只有一条 SQL 但相关子查询在某些数据库/数据分布下可能较慢(尤其没有合适索引时)。存在性判断则一定用Exists()(比__in子查询高效,因为 EXISTS 找到一条就短路,而__in需要物化整个 id 列表)。实践建议:先写清楚语义,再用explain()在真实数据量下对比。 - 追问:
only()/defer()为什么可能让性能更差? 它们控制的是「SELECT 哪些列」:only("id","title")生成SELECT id, title,defer("content")则排除大字段。收益是减少网络传输和内存占用(尤其表里有 TEXT/JSON 大字段时)。但如果之后访问了没有加载的字段,Django 会为每个对象单独发一条查询去补——在循环里就变成了 N+1,比一开始全查还慢得多,而且这个查询是隐式发生的、很难察觉。所以用它们的前提是确切知道后续只会用到那几个字段(包括模板、序列化器、信号处理器里的访问)。更安全的替代是.values()/.values_list():它们返回字典或元组而不是模型实例,根本没有「延迟加载」的可能,也省掉了构造模型对象的开销——取 id 列表时values_list("id", flat=True)是最快的写法。代价是失去了模型的方法和属性。 - 追问:怎么系统地排查一个页面的 ORM 性能问题? 五步。① 定位:用 django-debug-toolbar(开发环境)看这个页面发了多少条 SQL、总耗时多少、有没有重复的查询——「同一条 SQL 出现 N 次」就是 N+1 的铁证;生产环境用 APM 或数据库的慢查询日志。② 看 SQL:
print(qs.query)确认生成的语句是否符合预期(数一数有几个 JOIN,检查 GROUP BY 的字段对不对)。③ 看执行计划:qs.explain()(PostgreSQL 可以explain(analyze=True)拿到真实耗时),确认有没有走索引、有没有全表扫描。④ 优化:N+1 用select_related/prefetch_related/Subquery;缺索引就加(注意复合索引的字段顺序要匹配查询条件,__icontains这类前缀模糊查询用不上普通索引);数据量大用.iterator()、分页或缓存;能下推到数据库的计算不要在 Python 里做。⑤ 复测:改完再跑一遍 debug-toolbar 对比查询数和耗时——没有数字对比的「优化」不算优化。
八、加强记忆
Django ORM 的查询表达式本质是「把计算下推到数据库」。 四件套:annotate 给每行加一列(返回 QuerySet,可继续链式)、aggregate 把整个结果集压成一个 dict(终结操作);F() 引用「数据库里的字段值」——F("views") + 1 生成 SET views = views + 1,在数据库层原子完成,避免「读出来 +1 再写回」的丢失更新(配合 filter(stock__gte=1) 并检查 update() 返回的行数就是乐观并发控制),还能做字段间比较和跨关系引用;Q() 是构造 OR/NOT 和动态查询的唯一方式(Q 必须放在关键字参数之前,用 cond = Q() 起手再 &=/|= 累加);Subquery + OuterRef 三要素缺一不可:OuterRef("pk") 引用外层、.values("列") 只返回一列、[:1] 只返回一行,而 Exists() 比 __in 子查询高效(EXISTS 找到一条就短路,__in 要物化整个列表)。最经典的坑是多个 annotate 导致重复计数:对两个多对多分别 Count 会 JOIN 两次产生笛卡尔积,3 个作者 × 5 个标签让两个计数都变成 15;Sum 被放大更隐蔽(数字看着像对的)——解法是 distinct=True 或拆成 Subquery。第二个高频坑是 values() 的位置决定 GROUP BY:values("publisher").annotate(n=Count("id")) 才是按出版社分组,反过来是按主键分组(n 恒为 1);而且如果 Model 有 Meta.ordering,排序字段会被偷偷加进 GROUP BY——要加空的 .order_by() 清除。其余高频点:annotate 之前的 filter 是 WHERE、之后的是 HAVING;空集聚合返回 None 而不是 0(用 default=0);多值关系上「一次 filter 的多个条件」作用于同一条关联记录,而「链式 filter」各自独立 JOIN(「既有标签 A 又有标签 B」必须链式写,「A 或 B」则要加 .distinct())。调试三板斧:print(qs.query) 数 JOIN、explain() 看索引、debug-toolbar 看查询数;性能反模式:if qs: 和 len(qs) 会加载全部数据(应该用 exists()/count()),以及 only() 用错会触发隐式的 N+1。