你清楚在Django中如何用ORM优化数据库查询性能吗

访客 全栈框架 1

本文目录导读:

  1. 使用 select_related 解决外键/一对一关系的 N+1 问题
  2. 使用 prefetch_related 解决一对多/多对多关系的 N+1 问题
  3. 只取需要的字段 —— onlydefer
  4. 分页与限制结果集
  5. 合理建立数据库索引
  6. 善用 exists() / count() / update() 代替一次性查询
  7. 避免在循环中执行数据库查询 —— 批量操作
  8. 使用 select_for_update 处理并发(性能+数据一致性)
  9. 结合 connection.queries 分析真实 SQL
  10. 完整优化流程总结

在 Django 中,ORM 提供了非常方便的数据操作接口,但如果不注意性能策略,很容易产生 N+1 查询大量无用字段传输重复查询 等问题,下面系统性地介绍几种最常用且有效的优化方法。


使用 select_related 解决外键/一对一关系的 N+1 问题

适用场景:需要访问外键或多对一关系中的关联对象。

错误示例
循环中访问关联字段会导致多次查询。

books = Book.objects.all()
for book in books:
    print(book.author.name)   # 每次循环都查询 author 表

优化后

books = Book.objects.select_related('author').all()
for book in books:
    print(book.author.name)   # 只有一次 JOIN 查询

select_related 通过 SQL JOIN 一次性取回关联对象的数据,适用于 ForeignKeyOneToOneField


使用 prefetch_related 解决一对多/多对多关系的 N+1 问题

适用场景:需要访问反向关系、多对多关系或复杂筛选条件。

错误示例

authors = Author.objects.all()
for author in authors:
    books = author.book_set.all()   # 每个 author 都会触发一次查询

优化后

authors = Author.objects.prefetch_related('book_set').all()
for author in authors:
    books = author.book_set.all()   # 只有两次查询(一次查 author,一次查 book)

prefetch_related 在 Python 层面缓存结果,且可以链式使用:

Actor.objects.prefetch_related('movie_set', 'movie_set__director')

只取需要的字段 —— onlydefer

问题QuerySet 默认查询所有字段,可能传输大量不必要的数据。

only:只加载指定字段,其他字段在访问时触发额外查询(需注意)。

# 只需要 title 和 price
books = Book.objects.only('title', 'price')

defer:延迟加载指定字段,其他字段正常加载。

# 大部分字段都需加载,仅 content 很少用到
books = Book.objects.defer('content')

推荐:在列表/API 返回场景中,使用 values()values_list() 直接获取字典/元组,完全不构造模型实例,效率更高:

books = Book.objects.values('title', 'price')

分页与限制结果集

Django 的 QuerySet 是惰性的,但执行查询后还是会取回所有匹配的结果。

  • 使用 iterator() 处理大量数据,避免一次加载进内存:

    for book in Book.objects.iterator(chunk_size=500):
        ...
  • 使用分页器或手动 LIMIT

    books = Book.objects.all()[:20]        # 只取 20 条
    books = Book.objects.all()[10:30]     # offset + limit

合理建立数据库索引

ORM 查询的 WHERE / ORDER BY / JOIN 条件如果没有索引配合,即使 SQL 次数少也可能很慢。

Meta 中定义索引(Django 3.2+ 支持功能索引):

class Book(models.Model):= models.CharField(max_length=200)
    published_date = models.DateField()
    class Meta:
        indexes = [
            models.Index(fields=['published_date']),
            models.Index(fields=['title', 'published_date']),
        ]

也可在字段上设置 db_index=True


善用 exists() / count() / update() 代替一次性查询

  • 判断是否存在if Book.objects.filter(...).exists()if Book.objects.filter(...) 快得多。
  • 计数Book.objects.filter(...).count() 使用 SELECT COUNT(*) 而非取回全部对象。
  • 批量更新
    Book.objects.filter(published=True).update(price=0)
    # 而不是:
    for book in Book.objects.filter(published=True):
        book.price = 0
        book.save()

同理还有 F() 表达式做原子更新:

from django.db.models import F
Book.objects.update(price=F('price') * 1.1)

避免在循环中执行数据库查询 —— 批量操作

反例

for name in author_names:
    Author.objects.create(name=name)

正例

authors = [Author(name=name) for name in author_names]
Author.objects.bulk_create(authors)   # 一次 INSERT 多条

更新也支持 bulk_update

books = Book.objects.all()[:100]
for book in books:
    book.price = 10
Book.objects.bulk_update(books, ['price'])   # 一条 UPDATE 多条

使用 select_for_update 处理并发(性能+数据一致性)

如果要在事务中加锁,避免并发修改:

from django.db import transaction
with transaction.atomic():
    book = Book.objects.select_for_update().get(id=1)
    book.stock -= 1
    book.save()

结合 connection.queries 分析真实 SQL

Django 提供调试工具,可以在 shell 或日志中查看生成的 SQL 数量:

from django.db import connection
# 执行查询前重置
connection.queries_log.clear()
# 执行你的 ORM 操作
# ...
# 查看执行的 SQL 条数和内容
for query in connection.queries:
    print(query['sql'], query['time'])

此外推荐使用 django-debug-toolbar 在浏览器侧边栏直接查看。


完整优化流程总结

  1. 确认瓶颈 → 使用 connection.queries 或 django-debug-toolbar 分析 N+1 和重复查询。
  2. 消除 N+1 → 用 select_related / prefetch_related
  3. 减少数据传输only / defer / values
  4. 减少查询次数bulk_create / bulk_update / 批量删除。
  5. 数据库层优化 → 加索引,合理分页。
  6. Django 特定优化exists() / count() / F() / update() 避免加载完整对象。

掌握这些技巧后,大多数 Django 项目的数据库性能问题都能得到显著改善。

标签: N+1查询问题 预加载

抱歉,评论功能暂时关闭!