本篇筆記目錄如下:
- select_related
- 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。
select_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()
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
如果想獲取更多相關文章,可掃碼關注閱讀: