0

0

Django ORM高效实现左连接:prefetch_related深度解析

DDD

DDD

发布时间:2025-10-07 13:00:02

|

290人浏览过

|

来源于php中文网

原创

Django ORM高效实现左连接:prefetch_related深度解析

本文深入探讨了在Django中如何高效地执行模型间的左连接查询,特别是当需要获取所有父级记录及其关联的子级记录(即使子级不存在)时。文章分析了select_related和原生SQL的局限性,并重点介绍了prefetch_related作为最佳实践,它通过两次数据库查询和Python层面的数据关联,有效避免了数据重复传输,优化了查询性能,并保持了ORM的优势。

理解Django模型关联与左连接需求

在数据库应用中,经常需要查询主表的所有记录,并附带查询其关联的从表记录,即使从表中没有匹配的记录也要包含主表信息。这在sql中通常通过left join实现。例如,我们可能需要列出所有州(state),并显示它们所属的城市(city),即使某些州目前还没有任何城市。

考虑以下Django模型定义:

from django.db import models

class State(models.Model):
  name = models.CharField(max_length=25)
  abbreviation = models.CharField(max_length=2)

  def __str__(self):
    return f"{self.name} ({self.abbreviation})"


class City(models.Model):
  name = models.CharField(max_length=25)
  population = models.IntegerField()
  state = models.ForeignKey(State, related_name="cities", on_delete=models.CASCADE)

  def __str__(self):
    return f"{self.name} ({self.state.abbreviation})"

我们的目标是获取所有State对象,并为每个State对象加载其所有关联的City对象,包括那些没有City的State。期望的逻辑结果类似于SQL LEFT JOIN的扁平化输出,但更倾向于在Python中以结构化的方式访问数据。

常见尝试与局限性分析

在Django ORM中实现此类查询时,开发者常会尝试select_related或原生SQL,但它们各自存在一些局限性。

select_related的问题

select_related用于在查询时预先加载外键关系,通常通过INNER JOIN实现,以减少后续访问关联对象时的数据库查询次数(N+1问题)。然而,这并不适用于所有左连接场景。

# 示例:使用select_related查询City及其关联的State
cities_states = City.objects.all().select_related('state').order_by('state_id')

for city in cities_states:
    print(f"City: {city.name}, State: {city.state.name}")

局限性: select_related默认执行的是内连接(INNER JOIN)。这意味着如果一个State没有任何关联的City,那么该State将不会出现在查询结果中。例如,如果Illinois州没有任何城市,上述查询将不会返回Illinois的信息。这与我们期望的左连接行为(包含所有父级)不符。

原生SQL查询的问题

直接使用原生SQL可以精确控制连接类型,从而实现左连接:

sql = '''
SELECT S.*, C.* 
FROM "app_state" S  -- 假设应用名为 'app'
LEFT JOIN "app_city" C
ON (S."id" = C."state_id") 
ORDER BY S."id" ASC
'''

# 注意:如果模型在不同应用中,表名可能不同,例如 'myapp_state'
states_with_cities = State.objects.raw(sql)

for obj in states_with_cities:
    # 尝试打印
    print(f"State ID: {obj.id}, State Name: {obj.name}")
    # 如何访问City的字段?
    # print(f"City ID: {obj.id}, City Name: {obj.name}") # 这会再次打印State的id和name

局限性:

  1. 字段名冲突: 当State和City表都有id和name等相同名称的字段时,原生SQL查询会返回所有字段。但当通过obj.id或obj.name访问时,RawQuerySet实例会优先返回State模型的字段值。要区分并访问City的字段,需要在SQL查询中为字段使用别名,例如C.id AS city_id, C.name AS city_name。这增加了查询的复杂性,且丧失了部分ORM的便利性。
  2. 数据重复: 如果一个State关联了多个City,那么State的数据(如name, abbreviation)会在结果集中重复多次,增加了从数据库传输的数据量和Python处理时的内存开销。
  3. ORM功能受限: 使用raw查询返回的是RawQuerySet,它提供了类似模型实例的访问方式,但失去了QuerySet的许多强大功能,如链式调用、自动类型转换等。

优化方案:prefetch_related详解

为了在Django中高效地实现类似左连接的行为,同时避免上述问题,推荐使用prefetch_related。prefetch_related专为“一对多”或“多对多”关系设计,它通过执行两次独立的数据库查询来获取数据,然后在Python层面将它们关联起来。

星火作家大神
星火作家大神

星火作家大神是一款面向作家的AI写作工具

下载

prefetch_related的工作原理:

  1. 第一次查询: 获取所有父级对象(例如,所有State)。
  2. 第二次查询: 获取所有关联的子级对象(例如,所有City),并根据外键关系进行过滤(例如,City.objects.filter(state__in=list_of_state_ids))。
  3. Python层关联: Django在内存中将第二次查询的结果与第一次查询的父级对象进行匹配和绑定。这样,当访问state.cities.all()时,数据已经预加载,不会再触发额外的数据库查询。

使用prefetch_related实现左连接:

# 核心代码:使用prefetch_related预加载关联的城市
states = State.objects.prefetch_related('cities')

for state in states:
    print(f'州: {state.name} ({state.abbreviation})')

    # 访问关联的城市,这里不会触发新的数据库查询
    if state.cities.exists(): # 检查是否有城市,避免迭代空QuerySet
        for city in state.cities.all():
            print(f'  - 城市: {city.name}, 人口: {city.population}')
    else:
        print('  - 暂无关联城市。')

输出示例:

州: Texas (TX)
  - 城市: Dallas, 人口: 1259404
  - 城市: Houston, 人口: 2264876
州: California (CA)
  - 城市: Los Angeles, 人口: 3769485
州: Illinois (IL)
  - 暂无关联城市。

prefetch_related的优势:

  1. 避免数据重复: State的数据只查询一次,City的数据也只查询一次,避免了LEFT JOIN在数据库层面可能导致的数据重复传输问题。
  2. 包含所有父级: 由于第一次查询是针对所有State对象,即使没有关联的City,State对象也会被包含在结果中,完美符合左连接的需求。
  3. 保持ORM优势: 返回的是完整的State和City模型实例,可以继续使用ORM的所有功能,代码更简洁、可读性更高。
  4. 优化N+1查询问题: 将N次查询(每个State访问cities都会触发一次查询)优化为2次查询(一次State,一次City)。

prefetch_related与select_related的选择

理解何时使用select_related和prefetch_related至关重要:

  • select_related: 用于“一对一”(OneToOneField)和“多对一”(ForeignKey)的正向关系。它通过在数据库层面执行INNER JOIN来减少查询次数。如果不需要包含没有关联的父级,且关系是正向的ForeignKey,则select_related更高效。
  • prefetch_related: 用于“一对多”(ForeignKey的反向关系,如state.cities)和“多对多”(ManyToManyField)关系。它通过两次独立的查询和Python层面的关联来减少查询次数,并且能够包含所有父级记录。

注意事项

  • 内存消耗: prefetch_related在Python层面进行数据关联,这意味着所有预加载的数据都会加载到内存中。对于非常大的数据集,这可能会导致较高的内存消耗。然而,通常情况下,这比传输大量重复数据或执行N+1次查询更优。
  • 查询次数: prefetch_related通常会发出两次数据库查询(一次父级,一次子级),而不是一次。但相比于潜在的N+1次查询,这仍然是一个显著的优化。
  • 链式调用: prefetch_related可以与其他QuerySet方法(如filter(), order_by())链式调用,进一步细化查询结果。

总结

在Django ORM中,当需要实现类似SQL LEFT JOIN的功能,即获取所有父级记录及其关联的子级记录(包括没有子级的父级),并希望最大程度地优化数据库查询性能时,prefetch_related是首选方案。它通过智能地执行两次查询并在Python中完成关联,有效地避免了数据重复和N+1查询问题,同时保持了ORM的强大功能和代码的简洁性。理解其工作原理和适用场景,能够帮助开发者编写出更高效、更健壮的Django应用。

相关专题

更多
python开发工具
python开发工具

php中文网为大家提供各种python开发工具,好的开发工具,可帮助开发者攻克编程学习中的基础障碍,理解每一行源代码在程序执行时在计算机中的过程。php中文网还为大家带来python相关课程以及相关文章等内容,供大家免费下载使用。

769

2023.06.15

python打包成可执行文件
python打包成可执行文件

本专题为大家带来python打包成可执行文件相关的文章,大家可以免费的下载体验。

661

2023.07.20

python能做什么
python能做什么

python能做的有:可用于开发基于控制台的应用程序、多媒体部分开发、用于开发基于Web的应用程序、使用python处理数据、系统编程等等。本专题为大家提供python相关的各种文章、以及下载和课程。

764

2023.07.25

format在python中的用法
format在python中的用法

Python中的format是一种字符串格式化方法,用于将变量或值插入到字符串中的占位符位置。通过format方法,我们可以动态地构建字符串,使其包含不同值。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

659

2023.07.31

python教程
python教程

Python已成为一门网红语言,即使是在非编程开发者当中,也掀起了一股学习的热潮。本专题为大家带来python教程的相关文章,大家可以免费体验学习。

1325

2023.08.03

python环境变量的配置
python环境变量的配置

Python是一种流行的编程语言,被广泛用于软件开发、数据分析和科学计算等领域。在安装Python之后,我们需要配置环境变量,以便在任何位置都能够访问Python的可执行文件。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

549

2023.08.04

python eval
python eval

eval函数是Python中一个非常强大的函数,它可以将字符串作为Python代码进行执行,实现动态编程的效果。然而,由于其潜在的安全风险和性能问题,需要谨慎使用。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

579

2023.08.04

scratch和python区别
scratch和python区别

scratch和python的区别:1、scratch是一种专为初学者设计的图形化编程语言,python是一种文本编程语言;2、scratch使用的是基于积木的编程语法,python采用更加传统的文本编程语法等等。本专题为大家提供scratch和python相关的文章、下载、课程内容,供大家免费下载体验。

730

2023.08.11

AO3中文版入口地址大全
AO3中文版入口地址大全

本专题整合了AO3中文版入口地址大全,阅读专题下面的的文章了解更多详细内容。

1

2026.01.21

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 11.2万人学习

Django 教程
Django 教程

共28课时 | 3.3万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号