Django筆記十一之外來鍵查詢最佳化select_related和prefetch_related

XHunter 發表於 2023-03-29
Django

本篇筆記目錄如下:

  1. select_related
  2. prefetch_related

在介紹 select_related 和 prefetch_related 這兩個函式前,我們先來看一個例子。

對於,Entry 和 Blog 這兩個 model,前面介紹過,Blog 是 Entry 的外來鍵,如下:

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()


class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField()
    authors = models.ManyToManyField(Author)
    number_of_comments = models.IntegerField()
    number_of_pingbacks = models.IntegerField()
    rating = models.IntegerField()

比如我們需要獲取 Entry 的前十條資料,然後列印出關聯的 Blog 的 name 欄位資訊。

我們一般會如此操作:

for entry in Entry.objects.all()[:10]
	if entry.blog:
		print(entry.blog.name)
	else:
		print("沒有關聯 blog 資料")

但是這樣會有一個問題,那就是,這個 for 迴圈的操作會查詢資料十一次,一次查詢 Entry 資料,十次是查詢每個 entry_obj 關聯的 blog 資料。

這個設計對於系統來說是不合理的,想一想如果我們查詢的資料是一千條,一萬條,無論是系統介面的等待時間,還是資料庫的訪問壓力,都是不可接受的。

因此我們可以引入 外來鍵 和 ManyToManyTo 的一種能夠減少資料庫的訪問次數的方式:select_related,prefetch_related。

當我們在使用的時候,如果有需要獲取的外來鍵資料,比如 Entry 關聯的 Blog 資料,則可以將其欄位名作為引數傳入,這樣在獲取資料的時候就可以一次性將所有關聯的 Blog 資料也取出來,而不用單獨再去查詢一遍資料庫。

如下,批次操作

for entry in Entry.objects.select_related("blog").all():
	print(e.blog)  # 這個操作不會額外再去查詢資料庫

當然也適用於單條資料

e = Entry.objects.get(id=5).select_related("blog")

為了驗證 select_related() 確實會只查詢一遍資料庫,有兩種方法:
一種是在資料庫層面列印出來所有查詢的 SQL語句,
另一種可以從側面表示,那就是在系統層面列印出我們的查詢條件轉化的 SQL 語句。

比如:

Entry.objects.select_related("blog").all().query.__str__()

可以看到會輸出一個 關聯了 Blog 表的 inner join 的 SQL 語句。

SELECT `blog_entry`.`id`, `blog_entry`.`blog_id`, `blog_entry`.`headline`, `blog_entry`.`body_text`, `blog_entry`.`pub_date`, `blog_entry`.`mod_date`, `blog_entry`.`number_of_comments`, `blog_entry`.`number_of_pingbacks`, `blog_entry`.`rating`, `blog_blog`.`id`, `blog_blog`.`name`, `blog_blog`.`tagline` FROM `blog_entry` INNER JOIN `blog_blog` ON (`blog_entry`.`blog_id` = `blog_blog`.`id`)

鏈式獲取外來鍵資料

比如下面的 model:

class City(models.Model):
    pass


class Person(models.Model):
    hometown = models.ForeignKey(
        City, on_delete=models.SET_NULL, blank=True, null=True)


class Book(models.Model):
    author = models.ForeignKey(Person, on_delete=models.CASCADE)

我們可以透過以下語句來將 Book 關聯的 Person,以及該條 Person 資料關聯的 City 資料一起查詢出來:

book = Book.objects.select_related("author__hometown").get(id=4)
person = book.author
city = person.hometown

因為我們在第一步查詢的時候,透過雙下劃線將兩個外來鍵欄位連線在一起取了出來,所以在第二步和第三步取 Person 資料和 City 資料的時候,就不需要再次查詢資料庫了。

同時獲取多個外來鍵關聯欄位

如果一個 model 有兩個外來鍵欄位 foo 和 bar,那麼下面的兩種寫法都將這兩個外來鍵欄位關聯取出:

select_related("foo", "bar")
select_related("foo").select_related("bar")

需要注意的是,這個鏈式的操作和 order_by() 的結果是不一樣的哦,前面提到的 order_by() 的鏈式操作會導致後面的覆蓋前面的,但是取外來鍵資料的時候會同時取出。

注意: select_related() 僅作用於 ForeignKey 和 OneToOne,如果是 ManyToMany 欄位,則需要用到下面的 prefetch_related() 函式。

prefetch_related() 和 select_related() 作用類似,都是透過減少查詢的次數,來實現查詢最佳化。

但 prefetch_related() 是針對 ManyToMany 的操作。

舉個例子:

from django.db import models


class Topping(models.Model):
    name = models.CharField(max_length=30)


class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)


    def __str__(self):
        return "%s (%s)" % (
            self.name,
            ", ".join(topping.name for topping in self.toppings.all()),
        )

當我們執行:

Pizza.objects.all()

的時候,因為每一條 Pizza 資料例項化的時候,都會呼叫 str() 函式,而這個函式會再次去請求一遍資料庫,所以多條 Pizza 資料會導致查詢多次資料庫。

因為我們可以使用 prefetch_related() 函式來達到減少查詢的目的:

Pizza.objects.prefetch_related('toppings').all()

這樣的話,對資料庫的查詢會減少到兩次,一次是查詢出所有的 Pizza 資料,一次是根據所有的 pizza_id 找到所有關聯的 topping 資料。

如果有興趣,可以比對下面兩條語句在 shell 中執行的時候,MySQL 伺服器接收到的 SQL 查詢語句:

Pizza.objects.all()

Pizza.objects.prefetch_related('toppings').all()

下面一種情況需要注意哦:

pizzas = Pizza.objects.prefetch_related('toppings')
[list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]

因為第二步操作裡,會對 toppings 資料進行一次新的 filter 過濾操作,所以會導致每次該語句重新去查詢資料庫,也就是說,我們的 prefetch_related() 操作是失效的。

以上就是本篇筆記全部內容,接下來會介紹查詢裡的 defer 和 only 函式。

本文首發於本人微信公眾號:Django筆記。

原文連結:Django筆記十一之外來鍵查詢最佳化select_related和prefetch_related

如果想獲取更多相關文章,可掃碼關注閱讀:
image