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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/548578.html
標籤:Python
上一篇:ChatGPT 開源了第一款插件,都來學習一下原始碼吧!
下一篇:Python面向物件-高級用法
