在Django文档中:

select_related ()“遵循”外键关系,在执行查询时选择其他相关对象数据。 prefetch_related()对每个关系执行单独的查找,并在Python中执行“连接”。

“在python中进行连接”是什么意思?谁能举例说明一下吗?

我的理解是,对于外键关系,使用select_related;对于M2M关系,使用prefetch_related。这对吗?


当前回答

让我试着向你展示Django如何在select_related和prefetch_related中调用db

class a(models.Model):
    name = models.CharField(max_length=100)

class b(models.Model):
    name = models.CharField(max_length=100)
    a = models.ForeignKey(A, on_delete=models.CASCADE)

# select_related查询- >

b.objects.select_related('a').first()

为此执行的SQL查询将为

SELECT * FROM "b" LEFT OUTER JOIN "a" ON ("b"."a_id" = "a"."id") LIMIT 1

这里,Django将使用JOIN获取“一个”模型细节

# prefetch_related查询- >

B.objects.prefetch_related('a').first()

为此执行的SQL查询将为

SELECT * FROM "b" LIMIT 1
SELECT * FROM "a" WHERE "a"."id" IN (ids collected from above query)

这里Django将执行两个SQL查询并通过python将它们合并

其他回答

不要困惑 **select_related:**用于ForeignKey关系, **prefetch_related:**用于多对多字段关系或反向ForeignKey。 他们做同样的事情减少查询的数量 例如:

class ExampleClassA(models.Model):
    title = models.CharField(max_length=50)

class ExampleClassB(models.Model):
    example_class_a = models.ForeignKey(ExampleClassA,
        on_delete=models.CASCADE)



objects = ExampleClassB.objects.all() 

for obj in objects:
    print(obj.example_class_a.title) 

查询数量(访问相关字段):N+1 (# N是ExampleClassA对象的数量) 如果我们使用这个查询:

objects = ExampleClassB.objects.select_related('example_class_a').all()

查询的个数只有一个。

浏览了已经公布的答案。只是觉得如果我加上一个实际的例子会更好。

假设你有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)

在这里,您可以使用select_relation字段查询M2模型及其相对M1对象,使用prefetch_relation字段查询M3对象。

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

但是M3与M2的关系是一个多对多字段,它可以返回任意数量的M1对象。

考虑这样一个情况,您有2个M2对象m21, m22,它们有相同的5个关联的M3对象,id分别为1、2、3、4、5。当你为每一个M2对象获取关联的M3对象时,如果你使用select related,它是这样工作的。

步骤:

找到m21对象。 查询与m21对象id为1、2、3、4、5的所有M3对象。 对m22对象和所有其他M2对象重复同样的事情。

由于m21和m22对象的id都是相同的1,2,3,4,5,如果我们使用select_related选项,它将在DB中查询两次已经获取的相同id。

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

通过这种方式,您只查询一次所有M3对象,这提高了性能,因为python连接比数据库连接更便宜。

select_related()可以将多个SELECT查询减少为1个SELECT查询,在一对一关系中使用前向外键和反向外键,在一对多和多对多关系中使用前向外键。 在一对一、一对多和多对多的关系中,prefetch_related()可以使用正向外键和反向外键将多个SELECT查询减少到最少2个SELECT查询。

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

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

> <一对一的关系

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

class Person(models.Model):
    name = models.CharField(max_length=20)

    def __str__(self):
        return self.name

class PersonDetail(models.Model):
    person = models.OneToOneField(Person, on_delete=models.CASCADE)
    age = models.IntegerField()
    gender = models.CharField(max_length=20)

    def __str__(self):
        return str(self.age) + " " + self.gender

然后,Person和PersonDetail模型各有5个对象,如下所示:

<前向外键>

然后,我从PersonDetail模型迭代Person模型,如下所示:

for obj in PersonDetail.objects.all():
    print(obj.person)

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

John
Lisa
David
Anna
Kai

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

接下来,我用select_related(“Person”)从PersonDetail模型迭代Person模型,如下所示。* select_related()和all()的顺序不重要:

for obj in PersonDetail.objects.select_related("person").all():
    print(obj.person)

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

John
Lisa
David
Anna
Kai

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

接下来,我用prefetch_related(“Person”)从PersonDetail模型迭代Person模型,如下所示。* prefetch_related()和all()的顺序不重要:

for obj in PersonDetail.objects.prefetch_related("person").all():
    print(obj.person)

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

John
Lisa
David
Anna
Kai

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

<反向外键>

接下来,我迭代Person模型中的PersonDetail模型,如下所示:

for obj in Person.objects.all():
    print(obj.persondetail)

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

32 Male
26 Female
18 Male
27 Female
57 Male

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

接下来,我用select_related(" PersonDetail ")迭代Person模型中的PersonDetail模型,如下所示:

for obj in Person.objects.select_related("persondetail").all():
    print(obj.persondetail)

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

32 Male
26 Female
18 Male
27 Female
57 Male

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

接下来,我用prefetch_related(“PersonDetail”)从Person模型迭代PersonDetail模型,如下所示:

for obj in Person.objects.prefetch_related("persondetail").all():
    print(obj.persondetail)

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

32 Male
26 Female
18 Male
27 Female
57 Male

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

> <一对多关系

例如,Category和Product模型是一对多的关系,如下图所示:

class Category(models.Model):
    name = models.CharField(max_length=20)

    def __str__(self):
        return self.name

class Product(models.Model):
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    name = models.CharField(max_length=50)
    price = models.DecimalField(decimal_places=2, max_digits=5)
    
    def __str__(self):
        return str(self.category) + " " + self.name + " " + str(self.price)

那么Category模型有4个对象,Product模型有6个对象,如下图所示:

<前向外键>

然后,我从Product模型迭代Category模型,如下图所示:

for obj in Product.objects.all():
    print(obj.category)

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

Fruits
Fruits
Vegetable
Meat
Meat
Fish

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

接下来,我用select_related(“Category”)从Product模型迭代Category模型,如下所示:

for obj in Product.objects.select_related("category").all():
    print(obj.category)

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

Fruits
Fruits
Vegetable
Meat
Meat
Fish

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

接下来,我用prefetch_related(“Category”)从Product模型迭代Category模型,如下所示:

for obj in Product.objects.prefetch_related("category").all():
    print(obj.category)

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

Fruits
Fruits
Vegetable
Meat
Meat
Fish

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

<反向外键>

接下来,我从Category模型中迭代Product模型,如下所示:

for obj in Category.objects.all():
    print(obj.product_set.all())

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

<QuerySet [<Product: Fruits Apple 10.00>, <Product: Fruits Orange 20.00>]>
<QuerySet [<Product: Vegetable Carrot 30.00>]>
<QuerySet [<Product: Meat Chicken 40.00>, <Product: Meat Beef 50.00>]>
<QuerySet [<Product: Fish Salmon 60.00>]>

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

接下来,我尝试用select_related(“product_set”)从类别模型迭代产品模型,如下所示:

for obj in Category.objects.select_related("product_set").all():
    print(obj.product_set.all())

然后,出现以下错误,因为select_related("product_set")不能与反向外键一起使用:

django.core.exceptions.FieldError:在 select_related:“product_set”。选项为:(无)

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

                                        # ↓ No argument
for obj in Category.objects.select_related().all():
    print(obj.product_set.all())

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

<QuerySet [<Product: Fruits Apple 10.00>, <Product: Fruits Orange 20.00>]>
<QuerySet [<Product: Vegetable Carrot 30.00>]>
<QuerySet [<Product: Meat Chicken 40.00>, <Product: Meat Beef 50.00>]>
<QuerySet [<Product: Fish Salmon 60.00>]>

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

接下来,我用prefetch_related(“product_set”)从Category模型迭代Product模型,如下所示:

for obj in Category.objects.prefetch_related("product_set").all():
    print(obj.product_set.all())

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

<QuerySet [<Product: Fruits Apple 10.00>, <Product: Fruits Orange 20.00>]>
<QuerySet [<Product: Vegetable Carrot 30.00>]>
<QuerySet [<Product: Meat Chicken 40.00>, <Product: Meat Beef 50.00>]>
<QuerySet [<Product: Fish Salmon 60.00>]>

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

< many - to - many关系>

例如,Category和Product模型具有多对多的关系,如下所示:

class Category(models.Model):
    name = models.CharField(max_length=20)

    def __str__(self):
        return self.name

class Product(models.Model):
    categories = models.ManyToManyField(Category)
    name = models.CharField(max_length=50)
    price = models.DecimalField(decimal_places=2, max_digits=5)
    
    def __str__(self):
        return self.name + " " + str(self.price)

那么Category模型有5个对象,Product模型有6个对象,如下图所示:

<前向外键>

然后,我从Product模型迭代Category模型,如下图所示:

for obj in Product.objects.all():
    print(obj.categories.all())

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

<QuerySet [<Category: Fruits>, <Category: 20% OFF>]>
<QuerySet [<Category: Fruits>]>
<QuerySet [<Category: Vegetable>]>
<QuerySet [<Category: Meat>, <Category: 20% OFF>]>
<QuerySet [<Category: Meat>]>
<QuerySet [<Category: Fish>, <Category: 20% OFF>]>

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

接下来,我用select_related(“categories”)从Product模型迭代Category模型,如下所示:

for obj in Product.objects.select_related("categories").all():
    print(obj.categories.all())

然后,出现以下错误,因为select_related("categories")不能与反向外键一起使用:

django.core.exceptions.FieldError:在 select_related:“类别”。选项为:(无)

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

                                       # ↓ No argument
for obj in Product.objects.select_related().all():
    print(obj.categories.all())

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

<QuerySet [<Category: Fruits>, <Category: 20% OFF>]>
<QuerySet [<Category: Fruits>]>
<QuerySet [<Category: Vegetable>]>
<QuerySet [<Category: Meat>, <Category: 20% OFF>]>
<QuerySet [<Category: Meat>]>
<QuerySet [<Category: Fish>, <Category: 20% OFF>]>

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

接下来,我迭代类别模型从产品模型prefetch_related(“类别”)如下所示:

for obj in Product.objects.prefetch_related("categories").all():
    print(obj.categories.all())

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

<QuerySet [<Category: Fruits>, <Category: 20% OFF>]>
<QuerySet [<Category: Fruits>]>
<QuerySet [<Category: Vegetable>]>
<QuerySet [<Category: Meat>, <Category: 20% OFF>]>
<QuerySet [<Category: Meat>]>
<QuerySet [<Category: Fish>, <Category: 20% OFF>]>

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

<反向外键>

接下来,我从Category模型中迭代Product模型,如下所示:

for obj in Category.objects.all():
    print(obj.product_set.all())

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

<QuerySet [<Product: Apple 10.00>, <Product: Orange 20.00>]>
<QuerySet [<Product: Carrot 30.00>]>
<QuerySet [<Product: Chicken 40.00>, <Product: Beef 50.00>]>
<QuerySet [<Product: Salmon 60.00>]>
<QuerySet [<Product: Apple 10.00>, <Product: Chicken 40.00>, <Product: Salmon 60.00>]>

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

接下来,我用select_related(“product_set”)从Category模型迭代Product模型,如下所示:

for obj in Category.objects.select_related("product_set").all():
    print(obj.product_set.all())

然后,出现以下错误,因为select_related("categories")不能与反向外键一起使用:

django.core.exceptions.FieldError:在 select_related:“product_set”。选项为:(无)

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

                                        # ↓ No argument
for obj in Category.objects.select_related().all():
    print(obj.product_set.all())

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

<QuerySet [<Product: Apple 10.00>, <Product: Orange 20.00>]>
<QuerySet [<Product: Carrot 30.00>]>
<QuerySet [<Product: Chicken 40.00>, <Product: Beef 50.00>]>
<QuerySet [<Product: Salmon 60.00>]>
<QuerySet [<Product: Apple 10.00>, <Product: Chicken 40.00>, <Product: Salmon 60.00>]>

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

接下来,我用prefetch_related(“product_set”)从Category模型迭代Product模型,如下所示:

for obj in Category.objects.prefetch_related("product_set").all():
    print(obj.product_set.all())

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

<QuerySet [<Product: Apple 10.00>, <Product: Orange 20.00>]>
<QuerySet [<Product: Carrot 30.00>]>
<QuerySet [<Product: Chicken 40.00>, <Product: Beef 50.00>]>
<QuerySet [<Product: Salmon 60.00>]>
<QuerySet [<Product: Apple 10.00>, <Product: Chicken 40.00>, <Product: Salmon 60.00>]>

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

< >额外的实验

例如,有Country、State和City模型,它们由外键链接,如下所示:

class Country(models.Model):
    name = models.CharField(max_length=20)

    def __str__(self):
        return self.name

class State(models.Model):
    country = models.ForeignKey(Country, on_delete=models.CASCADE)
    name = models.CharField(max_length=20)

    def __str__(self):
        return self.name
    
class City(models.Model):
    state = models.ForeignKey(State, on_delete=models.CASCADE)
    name = models.CharField(max_length=20)

    def __str__(self):
        return self.name

那么,Country模型有2个对象,State模型有3个对象,City有7个对象,如下图所示:

<前向外键>

然后,我用select_related("state__country")从City模型迭代Country模型,如下所示:

for obj in City.objects.all().select_related("state__country"):
    print(obj.state.country)

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

USA
USA
USA
USA
USA
Japan
Japan

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

接下来,我用prefetch_related("state__country")从City模型迭代Country模型,如下所示:

for obj in City.objects.all().prefetch_related("state__country"):
    print(obj.state.country)

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

USA
USA
USA
USA
USA
Japan
Japan

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

<反向外键>

接下来,我用prefetch_related("state_set__city_set")从国家模型迭代城市模型,如下所示:

for country_obj in Country.objects.all().prefetch_related("state_set__city_set"):
    for state_obj in country_obj.state_set.all():
        for city_obj in state_obj.city_set.all():
            print(city_obj)

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

San Francisco
Los Angeles
San Diego
Kansas City
St. Louis
Ginza
Akihabara

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

接下来,我用prefetch_related("state_set__city_set")用filter()而不是all()从国家模型迭代城市模型,如下所示。*prefetch_related() with filter()不起作用:

                                  # Here
for country_obj in Country.objects.filter().prefetch_related("state_set__city_set"):
    for state_obj in country_obj.state_set.filter(): # Here
        for city_obj in state_obj.city_set.filter(): # Here
            print(city_obj)

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

San Francisco
Los Angeles
San Diego
Kansas City
St. Louis
Ginza
Akihabara

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

因此,为了减少8个SELECT查询,我需要使用Prefetch()和filter(),但我只知道如何使用Prefetch()和filter()从国家模型迭代国家模型,如下所示。*我在Stack Overflow上问了一个关于如何用Prefetch()和filter()从国家模型迭代城市模型的问题:

for country_obj in Country.objects.filter().prefetch_related(
    Prefetch('state_set', # Here
        queryset=State.objects.filter(),
        to_attr='state_obj'
    )
):
    print(country_obj.state_obj)

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

[<State: California>, <State: Missouri>]
[<State: Tokyo>]

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

让我试着向你展示Django如何在select_related和prefetch_related中调用db

class a(models.Model):
    name = models.CharField(max_length=100)

class b(models.Model):
    name = models.CharField(max_length=100)
    a = models.ForeignKey(A, on_delete=models.CASCADE)

# select_related查询- >

b.objects.select_related('a').first()

为此执行的SQL查询将为

SELECT * FROM "b" LEFT OUTER JOIN "a" ON ("b"."a_id" = "a"."id") LIMIT 1

这里,Django将使用JOIN获取“一个”模型细节

# prefetch_related查询- >

B.objects.prefetch_related('a').first()

为此执行的SQL查询将为

SELECT * FROM "b" LIMIT 1
SELECT * FROM "a" WHERE "a"."id" IN (ids collected from above query)

这里Django将执行两个SQL查询并通过python将它们合并

这两种方法都达到了相同的目的,即放弃不必要的db查询。但是他们使用不同的方法来提高效率。

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

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

连接有可能增加查询中的行数。当您通过外键或一对一字段执行连接时,行数不会增加。然而,多对多连接没有这种保证。因此,Django将select_related限制为不会意外导致大规模连接的关系。

prefetch_related的“join in python”有点令人担忧。它为要连接的每个表创建一个单独的查询。它用WHERE IN子句过滤这些表,比如:

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

每个表都被分割成一个单独的查询,而不是执行一个可能有太多行的单一连接。