Django ORM中select_related和prefetch_related的区别是什么?

Neo*_*ang 244 python django django-models django-orm

在Django doc中,

select_related() "跟随"外键关系,在执行查询时选择其他相关对象数据.

prefetch_related() 对每个关系进行单独查找,并在Python中进行"加入".

"在python中加入"是什么意思?有人能说明一个例子吗?

我的理解是,对于外键关系,使用select_related; 对于M2M关系,请使用prefetch_related.它是否正确?

Cra*_*sta 369

你的理解大多是正确的.select_related当您要选择的对象是单个对象时,您可以使用,OneToOneField或者a ForeignKey.prefetch_related当你要得到一套"东西"时,你会使用,所以ManyToManyField你所说的或反过来ForeignKey的.只是为了澄清我的意思"反向ForeignKeys"这里是一个例子:

class ModelA(models.Model):
    pass

class ModelB(models.Model):
    a = ForeignKey(ModelA)

ModelB.objects.select_related('a').all() # Forward ForeignKey relationship
ModelA.objects.prefetch_related('modelb_set').all() # Reverse ForeignKey relationship
Run Code Online (Sandbox Code Playgroud)

不同之处在于select_relatedSQL连接,因此从SQL服务器获取结果作为表的一部分.prefetch_related另一方面,执行另一个查询,因此减少了原始对象中的冗余列(ModelA在上面的示例中).您可以使用prefetch_related任何可以用于的东西select_related.

权衡是prefetch_related必须创建并发送一个ID列表以选择回服务器,这可能需要一段时间.我不确定在事务中是否有一种很好的方法可以做到这一点,但我的理解是Django总是只发送一个列表并说SELECT ... WHERE pk IN(...,...,...)基本上.在这种情况下,如果预取的数据是稀疏的(假设US状态对象链接到人们的地址),这可能非常好,但如果它更接近一对一,这可能会浪费大量的通信.如果有疑问,请尝试两者,看看哪个表现更好.

上面讨论的所有内容基本上都与数据库的通信有关.然而,在Python方面,prefetch_related还有一个额外的好处,即使用单个对象来表示数据库中的每个对象.随着select_related重复的对象将Python中的每个"父"对象被创建.由于Python中的对象具有相当大的内存开销,因此这也是一个考虑因素.

  • @eladsilver对不起,答复很慢.这实际上取决于.`select_related`在SQL中使用JOIN,而`prefetch_related`在第一个模型上运行查询,收集预取所需的所有ID,然后在WHERE中运行带有所需ID的IN子句的查询.如果你说使用相同外键的3-5个模型,`select_related`几乎肯定会更好.如果您有100或1000个使用相同外键的模型,`prefetch_related`实际上可能更好.在两者之间,你必须测试并看看会发生什么. (27认同)
  • `select_related`是一个查询,而`prefetch_related`是2,所以前者更快.但是`select_related`对于'ManyToManyField`来说无济于事 (17认同)
  • 什么是更快? (3认同)
  • 我对您关于预取相关的评论“通常没有多大意义”提出异议。对于标记为唯一的 FK 字段来说确实如此,但在多行具有相同 FK 值(作者、用户、类别、城市等)的任何地方,预取会减少 Django 和数据库之间的带宽,但不会减少重复的行。它通常还使用较少的数据库内存。其中任何一个通常都比单个额外查询的开销更重要。鉴于这是一个相当流行的问题的最佳答案,我认为应该在答案中注明。 (3认同)

小智 26

选择相关()
\n
    \n
  • 可以将一对一关系中使用正向外键反向外键以及一对多和多对多关系中使用正向外键的SELECT多个查询减少到仅 1 个查询。SELECT

    \n
  • \n
  • 不能在一对多和多对多关系中与反向外键一起使用。

    \n
  • \n
\n
预取相关()
\n
    \n
  • 通过一对一、一对多、多对多关系中的正向外键反向外键,可以将多个查询最少减少SELECT到 2 个查询。SELECT
  • \n
\n

*你可以看我的回答解释正向外键反向外键的含义。

\n

下面显示了我在一对一、一对多和多对多关系中使用正向外键和反向select_related()的实验。prefetch_related()

\n

<一对一关系>

\n

例如,有PersonPersonDetail模型具有一对一的关系,如下所示:

\n
# "app/models.py\n\nfrom django.db import models\n\nclass Person(models.Model):\n    name = models.CharField(max_length=20)\n\n    def __str__(self):\n        return self.name\n\nclass PersonDetail(models.Model):\n    person = models.OneToOneField(Person, on_delete=models.CASCADE)\n    age = models.IntegerField()\n    gender = models.CharField(max_length=20)\n\n    def __str__(self):\n        return str(self.age) + " " + self.gender\n
Run Code Online (Sandbox Code Playgroud)\n

并且,还有Person管理员PersonDetail,如下所示:

\n
# "app/admin.py\n\nfrom django.contrib import admin\nfrom .models import Person, PersonDetail\n\nclass PersonDetailInline(admin.TabularInline):\n    model = PersonDetail\n    \n@admin.register(Person)\nclass PersonAdmin(admin.ModelAdmin):\n    inlines = (PersonDetailInline,)\n    list_display = (\'id\', \'name\')\n    ordering = (\'id\',)\n\n@admin.register(PersonDetail)\nclass PersonDetailAdmin(admin.ModelAdmin):\n    list_display = (\'id\', \'age\', \'gender\', \'person\')\n    ordering = (\'id\',)\n
Run Code Online (Sandbox Code Playgroud)\n

那么,Personadmin有5个对象,如下所示:

\n

在此输入图像描述

\n

并且,PersonDetailadmin 有 5 个对象,如下所示:

\n

在此输入图像描述

\n

<转发外键>

\n

然后,我PersonPersonDetail模型迭代模型,如下所示:

\n
for obj in PersonDetail.objects.all():\n    print(obj.person)\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
John\nDavid\nLisa\nKai\nAnna\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 6 个查询,如下所示。*我使用 PostgreSQL,下面是 PostgreSQL 的查询日志,你可以看到我的答案解释如何启用和禁用 PostgreSQL 上的查询日志:

\n

在此输入图像描述

\n

接下来,我PersonPersonDetail模型迭代模型,select_related("person")如下所示。select_related()*和的顺序all()无关紧要:

\n
for obj in PersonDetail.objects.select_related("person").all():\n    print(obj.person)\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
John\nDavid\nLisa\nKai\nAnna\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 1 个查询,如下所示:

\n

在此输入图像描述

\n

接下来,我PersonPersonDetail模型迭代模型,prefetch_related("person")如下所示。prefetch_related()*和的顺序all()无关紧要:

\n
for obj in PersonDetail.objects.prefetch_related("person").all():\n    print(obj.person)\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
John\nDavid\nLisa\nKai\nAnna\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 2 个查询,如下所示:

\n

在此输入图像描述

\n

<反向外键>

\n

接下来,我PersonDetailPerson模型迭代模型,如下所示:

\n
for obj in Person.objects.all():\n    print(obj.persondetail)\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
32 Male\n18 Male\n26 Female\n36 Male\n21 Female\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 6 个查询,如下所示:

\n

在此输入图像描述

\n

接下来,我PersonDetailPerson模型迭代模型,select_related("persondetail")如下所示:

\n
for obj in Person.objects.select_related("persondetail").all():\n    print(obj.persondetail)\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
32 Male\n18 Male\n26 Female\n36 Male\n21 Female\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 1 个查询,如下所示:

\n

在此输入图像描述

\n

接下来,我PersonDetailPerson模型迭代模型,prefetch_related("persondetail")如下所示:

\n
for obj in Person.objects.prefetch_related("persondetail").all():\n    print(obj.persondetail)\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
32 Male\n18 Male\n26 Female\n36 Male\n21 Female\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 2 个查询,如下所示:

\n

在此输入图像描述

\n

<一对多关系>

\n

例如,有CategoryProduct模型具有一对多关系,如下所示:

\n
# "app/models.py"\n\nfrom django.db import models\n\nclass Category(models.Model):\n    name = models.CharField(max_length=20)\n\n    def __str__(self):\n        return self.name\n\nclass Product(models.Model):\n    category = models.ForeignKey(Category, on_delete=models.CASCADE)\n    name = models.CharField(max_length=50)\n    price = models.DecimalField(decimal_places=2, max_digits=5)\n\n    def __str__(self):\n        return str(self.category) + " " + self.name + " " + str(self.price)\n
Run Code Online (Sandbox Code Playgroud)\n

并且,还有Category管理员Product,如下所示:

\n
# "app/admin.py"\n\nfrom django.contrib import admin\nfrom .models import Category, Product\n    \n@admin.register(Category)\nclass CategoryAdmin(admin.ModelAdmin):\n    list_display = (\'id\', \'name\')\n    ordering = (\'id\',)\n\n@admin.register(Product)\nclass ProductAdmin(admin.ModelAdmin):\n    list_display = (\'id\', \'name\', \'price\', \'category\')\n    ordering = (\'id\',)\n
Run Code Online (Sandbox Code Playgroud)\n

那么,Categoryadmin有4个对象,如下所示:

\n

在此输入图像描述

\n

并且,Productadmin 有 6 个对象,如下所示:

\n

在此输入图像描述

\n

<转发外键>

\n

然后,我CategoryProduct模型迭代模型,如下所示:

\n
for obj in Product.objects.all():\n    print(obj.category)\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
Fruits\nFruits\nVegetable\nMeat\nMeat\nFish\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 7 个查询,如下所示:

\n

在此输入图像描述

\n

接下来,我CategoryProduct模型迭代模型,select_related("category")如下所示:

\n
for obj in Product.objects.select_related("category").all():\n    print(obj.category)\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
Fruits\nFruits\nVegetable\nMeat\nMeat\nFish\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 1 个查询,如下所示:

\n

在此输入图像描述

\n

接下来,我CategoryProduct模型迭代模型,prefetch_related("category")如下所示:

\n
for obj in Product.objects.prefetch_related("category").all():\n    print(obj.category)\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
Fruits\nFruits\nVegetable\nMeat\nMeat\nFish\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 2 个查询,如下所示:

\n

在此输入图像描述

\n

<反向外键>

\n

接下来,我ProductCategory模型迭代模型,如下所示:

\n
for obj in Category.objects.all():\n    print(obj.product_set.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
<QuerySet [<Product: Fruits Apple 10.00>, <Product: Fruits Orange 20.00>]>\n<QuerySet [<Product: Vegetable Carrot 30.00>]>\n<QuerySet [<Product: Meat Chicken 40.00>, <Product: Meat Beef 50.00>]>\n<QuerySet [<Product: Fish Salmon 60.00>]>\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 5 个查询,如下所示:

\n

在此输入图像描述

\n

接下来,我尝试ProductCategory模型迭代模型,select_related("product_set")如下所示:

\n
for obj in Category.objects.select_related("product_set").all():\n    print(obj.product_set.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,由于无法与反向外键select_related("product_set")一起使用,因此出现以下错误:

\n
\n

django.core.exceptions.FieldError:\nselect_lated 中给出的字段名称无效:\'product_set\'。选项有:(无)

\n
\n

select_related()实际上,如果我不带参数使用就不会出现错误,如下所示:

\n
                                        # \xe2\x86\x93 No argument\nfor obj in Category.objects.select_related().all():\n    print(obj.product_set.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
<QuerySet [<Product: Fruits Apple 10.00>, <Product: Fruits Orange 20.00>]>\n<QuerySet [<Product: Vegetable Carrot 30.00>]>\n<QuerySet [<Product: Meat Chicken 40.00>, <Product: Meat Beef 50.00>]>\n<QuerySet [<Product: Fish Salmon 60.00>]>\n
Run Code Online (Sandbox Code Playgroud)\n

但是,SELECT仍然运行 5 个查询,而不是 1 个SELECT查询,如下所示:

\n

在此输入图像描述

\n

接下来,我ProductCategory模型迭代模型,prefetch_related("product_set")如下所示:

\n
for obj in Category.objects.prefetch_related("product_set").all():\n    print(obj.product_set.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
<QuerySet [<Product: Fruits Apple 10.00>, <Product: Fruits Orange 20.00>]>\n<QuerySet [<Product: Vegetable Carrot 30.00>]>\n<QuerySet [<Product: Meat Chicken 40.00>, <Product: Meat Beef 50.00>]>\n<QuerySet [<Product: Fish Salmon 60.00>]>\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 2 个查询,如下所示:

\n

在此输入图像描述

\n

<多对多关系>

\n

例如,CategoryProductmodels 之间存在多对多关系,如下所示:

\n
# "app/models.py"\n\nfrom django.db import models\n\nclass Category(models.Model):\n    name = models.CharField(max_length=20)\n\n    def __str__(self):\n        return self.name\n\nclass Product(models.Model):\n    categories = models.ManyToManyField(Category)\n    name = models.CharField(max_length=50)\n    price = models.DecimalField(decimal_places=2, max_digits=5)\n    \n    def __str__(self):\n        return self.name + " " + str(self.price)\n
Run Code Online (Sandbox Code Playgroud)\n

并且,还有Category管理员Product,如下所示:

\n
# "app/admin.py\n\nfrom django.contrib import admin\nfrom .models import Category, Product\n    \n@admin.register(Category)\nclass CategoryAdmin(admin.ModelAdmin):\n    list_display = (\'id\', \'name\', \'get_products\')\n    ordering = (\'id\',)\n\n    @admin.display(description=\'products\')\n    def get_products(self, obj):\n        return [product.name for product in obj.product_set.all()]\n\n@admin.register(Product)\nclass ProductAdmin(admin.ModelAdmin):\n    list_display = (\'id\', \'name\', \'price\', \'get_categories\')\n    ordering = (\'id\',)\n\n    @admin.display(description=\'categories\')\n    def get_categories(self, obj):\n        return [category.name for category in obj.categories.all()]\n
Run Code Online (Sandbox Code Playgroud)\n

那么,Categoryadmin有5个对象,如下所示:

\n

在此输入图像描述

\n

并且,Productadmin 有 6 个对象,如下所示:

\n

在此输入图像描述

\n

<转发外键>

\n

然后,我CategoryProduct模型迭代模型,如下所示:

\n
for obj in Product.objects.all():\n    print(obj.categories.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
<QuerySet [<Category: Fruits>, <Category: 20% OFF>]>\n<QuerySet [<Category: Fruits>]>\n<QuerySet [<Category: Vegetable>]>\n<QuerySet [<Category: Meat>, <Category: 20% OFF>]>\n<QuerySet [<Category: Meat>]>\n<QuerySet [<Category: Fish>, <Category: 20% OFF>]>\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 7 个查询,如下所示:

\n

在此输入图像描述

\n

接下来,我CategoryProduct模型迭代模型,select_related("categories")如下所示:

\n
for obj in Product.objects.select_related("categories").all():\n    print(obj.categories.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,由于无法与反向外键select_related("categories")一起使用,因此出现以下错误:

\n
\n

django.core.exceptions.FieldError:\nselect_lated 中给出的字段名称无效:\'categories\'。选项有:(无)

\n
\n

select_related()实际上,如果我不带参数使用就不会出现错误,如下所示:

\n
                                       # \xe2\x86\x93 No argument\nfor obj in Product.objects.select_related().all():\n    print(obj.categories.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
<QuerySet [<Category: Fruits>, <Category: 20% OFF>]>\n<QuerySet [<Category: Fruits>]>\n<QuerySet [<Category: Vegetable>]>\n<QuerySet [<Category: Meat>, <Category: 20% OFF>]>\n<QuerySet [<Category: Meat>]>\n<QuerySet [<Category: Fish>, <Category: 20% OFF>]>\n
Run Code Online (Sandbox Code Playgroud)\n

但是,SELECT仍然运行 7 个查询,而不是 1 个SELECT查询,如下所示:

\n

在此输入图像描述

\n

接下来,我CategoryProduct模型迭代模型,prefetch_related("categories")如下所示:

\n
for obj in Product.objects.prefetch_related("categories").all():\n    print(obj.categories.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
<QuerySet [<Category: Fruits>, <Category: 20% OFF>]>\n<QuerySet [<Category: Fruits>]>\n<QuerySet [<Category: Vegetable>]>\n<QuerySet [<Category: Meat>, <Category: 20% OFF>]>\n<QuerySet [<Category: Meat>]>\n<QuerySet [<Category: Fish>, <Category: 20% OFF>]>\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 2 个查询,如下所示:

\n

在此输入图像描述

\n

<反向外键>

\n

接下来,我ProductCategory模型迭代模型,如下所示:

\n
for obj in Category.objects.all():\n    print(obj.product_set.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
<QuerySet [<Product: Apple 10.00>, <Product: Orange 20.00>]>\n<QuerySet [<Product: Carrot 30.00>]>\n<QuerySet [<Product: Chicken 40.00>, <Product: Beef 50.00>]>\n<QuerySet [<Product: Salmon 60.00>]>\n<QuerySet [<Product: Apple 10.00>, <Product: Chicken 40.00>, <Product: Salmon 60.00>]>\n
Run Code Online (Sandbox Code Playgroud)\n

然后,SELECT运行 6 个查询,如下所示:

\n

在此输入图像描述

\n

接下来,我ProductCategory模型迭代模型,select_related("product_set")如下所示:

\n
for obj in Category.objects.select_related("product_set").all():\n    print(obj.product_set.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,由于无法与反向外键select_related("categories")一起使用,因此出现以下错误:

\n
\n

django.core.exceptions.FieldError:\nselect_lated 中给出的字段名称无效:\'product_set\'。选项有:(无)

\n
\n

select_related()实际上,如果我不带参数使用就不会出现错误,如下所示:

\n
                                        # \xe2\x86\x93 No argument\nfor obj in Category.objects.select_related().all():\n    print(obj.product_set.all())\n
Run Code Online (Sandbox Code Playgroud)\n

然后,在控制台上输出以下内容:

\n
<QuerySet [<Product: Apple 10.00>, <Product: Orange 20.00>]>\n<QuerySet [<Product: Carrot 30.00>]>\n<QuerySet [<Product: Chicken 40.00>, <Product: Beef 50.00>]>\n<QuerySet [<Product: Salmon 60.00>]>\n<QuerySet [<Product: Apple 10.00>, <Product: Chicken 40.00>, <Product: Salmon 60.00>]>\n
Run Code Online (Sandbox Code Playgroud)\n

但是,SELECT


cdo*_*orn 20

两种方法都达到了相同的目的,放弃了不必要的数据库查询.但他们使用不同的方法来提高效率.

使用这些方法之一的唯一原因是单个大型查询比许多小型查询更可取.Django使用大型查询抢先在内存中创建模型,而不是对数据库执行按需查询.

select_related对每个查找执行连接,但扩展select以包括所有连接表的列.然而,这种方法有一个警告.

联接有可能乘以查询中的行数.当您通过外键或一对一字段执行连接时,行数不会增加.但是,多对多连接没有这种保证.因此,Django限制select_related了不会出乎意料地导致大规模加入的关系.

"加入蟒蛇"prefetch_related是一个小更令人震惊的话,应该是.它为要连接的每个表创建单独的查询.它使用WHERE IN子句过滤每个表,如:

SELECT "credential"."id",
       "credential"."uuid",
       "credential"."identity_id"
FROM   "credential"
WHERE  "credential"."identity_id" IN
    (84706, 48746, 871441, 84713, 76492, 84621, 51472);
Run Code Online (Sandbox Code Playgroud)

而不是执行具有可能太多行的单个连接,而是将每个表拆分为单独的查询.


Suk*_*djf 7

浏览了已经发布的答案。只是认为如果我用实际示例添加答案会更好。

假设您有 3 个相关的 Django 模型。

class M1(models.Model):
    name = models.CharField(max_length=10)

class M2(models.Model):
    name = models.CharField(max_length=10)
    select_relation = models.ForeignKey(M1, on_delete=models.CASCADE)
    prefetch_relation = models.ManyToManyField(to='M3')

class M3(models.Model):
    name = models.CharField(max_length=10)
Run Code Online (Sandbox Code Playgroud)

在这里您可以使用字段和使用字段的对象查询M2模型及其相关M1对象。select_relationM3prefetch_relation

然而,正如我们所提到M1的,从M2是 a的关系ForeignKey,它只返回任何对象的1 条记录M2。同样的事情也适用OneToOneField

但是M3的关系 fromM2是 a ManyToManyField,它可能返回任意数量的M1对象。

考虑这样一种情况,您有 2 个M2对象m21m22它们具有相同的5 个M3带有 ID 的关联对象1,2,3,4,5。当您M3为每个对象获取关联对象时M2,如果您使用 select related,这就是它的工作方式。

脚步:

  1. m21对象。
  2. 查询所有M3m21ID 为的对象相关的对象1,2,3,4,5
  3. m22对象和所有其他M2对象重复相同的操作。

因为我们有相同1,2,3,4,5的ID两个m21m22对象,如果我们使用select_related选项,它会查询数据库两次,这已经获取相同的ID。

相反,如果您使用 prefetch_related,当您尝试获取M2对象时,它会记录您的对象在查询M2表时返回的所有 ID(注意:仅 ID),并且作为最后一步,Django 将对M3表进行查询带有您的M2对象返回的所有 ID 的集合。并M2使用 Python 而不是数据库将它们连接到对象。

通过这种方式,您M3只查询一次所有对象,从而提高性能。