資料庫存取優化

Django 的資料庫層提供了多種方式來幫助開發人員充分利用他們的資料庫。本文收集了相關文件的連結,並新增了各種提示,這些提示組織在一些標題下,概述了在嘗試優化資料庫使用時應採取的步驟。

首先進行效能分析

作為一般的程式設計實務,這是理所當然的。找出您正在執行的查詢以及它們的成本。使用QuerySet.explain()來了解資料庫如何執行特定的QuerySet。您可能還想使用像django-debug-toolbar這樣的外部專案,或是直接監控資料庫的工具。

請記住,您可能會針對速度或記憶體或兩者進行優化,具體取決於您的需求。有時,針對一個進行優化會不利於另一個,但有時它們會互相幫助。此外,資料庫程序所做的工作可能與 Python 程序中所做的相同工作量(對您而言)成本不同。您需要自行決定您的優先順序是什麼,平衡點在哪裡,並在需要時分析所有這些,因為這將取決於您的應用程式和伺服器。

對於以下所有內容,請記住在每次變更後進行分析,以確保變更是有益的,並且考慮到程式碼可讀性的降低,這種益處是否足夠大。以下所有建議都附帶警告,在您的情況下,一般原則可能不適用,甚至可能相反。

使用標準的資料庫優化技術

...包括

  • 索引。這是優先事項的第一位,您從效能分析中確定應該新增哪些索引之後。使用Meta.indexesField.db_index從 Django 新增這些索引。考慮在您經常使用filter()exclude()order_by()等查詢的欄位新增索引,因為索引可能有助于加快查詢速度。請注意,確定最佳索引是一個複雜的、依賴資料庫的主題,它將取決於您的特定應用程式。維護索引的開銷可能會超過查詢速度的任何提升。

  • 適當使用欄位類型。

我們假設您已經完成了上面列出的事情。本文的其餘部分側重於如何使用 Django,使您不會執行不必要的工作。本文也不會討論適用於所有昂貴操作的其他優化技術,例如通用快取

了解 QuerySets

了解QuerySets對於透過簡單的程式碼獲得良好的效能至關重要。特別是

了解 QuerySet 評估

為了避免效能問題,了解以下內容非常重要

了解快取屬性

除了快取整個 QuerySet 之外,ORM 物件上的屬性結果也有快取。一般來說,不可呼叫的屬性將被快取。例如,假設範例部落格模型

>>> entry = Entry.objects.get(id=1)
>>> entry.blog  # Blog object is retrieved at this point
>>> entry.blog  # cached version, no DB access

但一般來說,可呼叫的屬性每次都會導致資料庫查詢

>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all()  # query performed
>>> entry.authors.all()  # query performed again

在閱讀範本程式碼時要小心 - 範本系統不允許使用括號,但會自動呼叫可呼叫的內容,從而隱藏上述區別。

小心您自己的自訂屬性 - 您需要自行在需要時實作快取,例如使用cached_property裝飾器。

使用 with 範本標籤

若要使用 QuerySet 的快取行為,您可能需要使用with範本標籤。

使用 iterator()

當您有很多物件時,QuerySet 的快取行為可能會導致大量記憶體被使用。在這種情況下,iterator() 可能會有幫助。

使用 explain()

QuerySet.explain()提供有關資料庫如何執行查詢的詳細資訊,包括使用的索引和聯結。這些詳細資訊可以幫助您找到可以更有效率地重寫的查詢,或識別可以新增以提高效能的索引。

在資料庫中而不是在 Python 中執行資料庫工作

例如

如果這些不足以產生您需要的 SQL

使用 RawSQL

一個較不具可攜性但更強大的方法是RawSQL表達式,允許將一些 SQL 明確地新增到查詢中。如果這仍然不夠強大

使用原始 SQL

編寫您自己的自訂 SQL 來檢索資料或填入模型。使用django.db.connection.queries來找出 Django 為您編寫的內容,並從那裡開始。

使用唯一的索引欄位檢索個別物件

當使用get()檢索個別物件時,有兩個原因要使用具有uniquedb_index的欄位。首先,由於底層資料庫索引,查詢速度會更快。此外,如果多個物件符合查詢,查詢速度可能會慢得多;在欄位上具有唯一約束可保證永遠不會發生這種情況。

因此,使用範例部落格模型

>>> entry = Entry.objects.get(id=10)

會比

>>> entry = Entry.objects.get(headline="News Item Title")

快,因為 id 由資料庫建立索引,並保證是唯一的。

執行以下操作可能會很慢

>>> entry = Entry.objects.get(headline__startswith="News")

首先,headline 沒有建立索引,這會使底層資料庫提取速度變慢。

其次,查詢無法保證只會傳回一個物件。如果查詢符合多個物件,它將從資料庫中檢索並傳輸所有物件。如果傳回數百或數千筆記錄,這種懲罰可能會很嚴重。如果資料庫位於單獨的伺服器上,其中網路開銷和延遲也會發揮作用,則懲罰將會加劇。

如果您知道您需要它,請一次檢索所有內容

對於單個「集合」的不同部分多次命中資料庫,您需要該集合的所有部分,通常不如在一個查詢中檢索所有內容有效率。如果您有一個在迴圈中執行的查詢,並且可能會因此執行許多資料庫查詢,而只需要一個查詢,這點尤其重要。所以

不要檢索您不需要的東西

使用 QuerySet.values()values_list()

當您只需要值的 dictlist,而不需要 ORM 模型物件時,請適當使用 values()。這些對於在樣板程式碼中取代模型物件很有用 - 只要您提供的 dict 具有與樣板中使用的屬性相同的屬性,就可以了。

使用 QuerySet.defer()only()

如果有您知道不需要(或在大多數情況下不需要)的資料庫欄位,請使用 defer()only() 來避免載入它們。請注意,如果您確實使用了它們,ORM 將必須在單獨的查詢中取得它們,如果您不當使用它,這會造成效能上的負擔。

不要在沒有效能分析的情況下過於積極地延遲欄位,因為即使最終只使用幾個欄位,資料庫也必須從磁碟中讀取結果中單一列的大部分非文字、非-VARCHAR 資料。defer()only() 方法在您可以避免載入大量文字資料或用於可能需要大量處理才能轉換回 Python 的欄位時最有用。一如既往,先進行效能分析,然後再進行最佳化。

使用 QuerySet.contains(obj)

...如果您只想找出 obj 是否在查詢集中,而不是使用 if obj in queryset

使用 QuerySet.count()

...如果您只需要計數,而不是執行 len(queryset)

使用 QuerySet.exists()

...如果您只想找出是否至少存在一個結果,而不是使用 if queryset

但是

不要過度使用 contains()count()exists()

如果您需要來自 QuerySet 的其他資料,請立即評估它。

例如,假設一個 Group 模型與 User 有多對多關係,以下程式碼是最佳的

members = group.members.all()

if display_group_members:
    if members:
        if current_user in members:
            print("You and", len(members) - 1, "other users are members of this group.")
        else:
            print("There are", len(members), "members in this group.")

        for member in members:
            print(member.username)
    else:
        print("There are no members in this group.")

它是最佳的,因為

  1. 由於 QuerySet 是延遲載入的,如果 display_group_membersFalse,則不會執行資料庫查詢。

  2. group.members.all() 儲存在 members 變數中,可以重複使用其結果快取。

  3. 程式碼行 if members: 會呼叫 QuerySet.__bool__(),這會導致在資料庫上執行 group.members.all() 查詢。如果沒有任何結果,它將返回 False,否則返回 True

  4. 程式碼行 if current_user in members: 會檢查使用者是否在結果快取中,因此不會發出額外的資料庫查詢。

  5. 使用 len(members) 會呼叫 QuerySet.__len__(),重複使用結果快取,因此同樣不會發出資料庫查詢。

  6. for member 迴圈會迭代結果快取。

總體而言,此程式碼會執行零個或一個資料庫查詢。執行的唯一刻意最佳化是使用 members 變數。對 if 使用 QuerySet.exists(),對 in 使用 QuerySet.contains(),或對計數使用 QuerySet.count() 都會導致額外的查詢。

使用 QuerySet.update()delete()

不要檢索大量物件、設定一些值並個別儲存它們,而是透過 QuerySet.update() 使用批次 SQL UPDATE 陳述式。類似地,在可能的情況下執行 批次刪除

但是請注意,這些批次更新方法無法呼叫個別實例的 save()delete() 方法,這表示您為這些方法新增的任何自訂行為都不會執行,包括從正常資料庫物件 信號 (signals) 驅動的任何行為。

直接使用外鍵值

如果您只需要外鍵值,請使用您已有的物件上的外鍵值,而不是取得整個相關物件並取得其主鍵。也就是說,執行

entry.blog_id

而不是

entry.blog.id

如果您不在意順序,請不要排序結果

排序並非沒有成本;每個要排序的欄位都是資料庫必須執行的操作。如果模型具有預設排序 (Meta.ordering) 而您不需要它,請透過呼叫不帶參數的 order_by()QuerySet 上移除它。

在資料庫中新增索引可能有助於提高排序效能。

使用批次方法

使用批次方法來減少 SQL 陳述式的數量。

批次建立

在建立物件時,盡可能使用 bulk_create() 方法來減少 SQL 查詢的數量。例如

Entry.objects.bulk_create(
    [
        Entry(headline="This is a test"),
        Entry(headline="This is only a test"),
    ]
)

…比以下做法更好

Entry.objects.create(headline="This is a test")
Entry.objects.create(headline="This is only a test")

請注意,此方法有一些注意事項,因此請確保它適合您的使用案例。

批次更新

在更新物件時,盡可能使用 bulk_update() 方法來減少 SQL 查詢的數量。給定一個物件列表或查詢集

entries = Entry.objects.bulk_create(
    [
        Entry(headline="This is a test"),
        Entry(headline="This is only a test"),
    ]
)

以下範例

entries[0].headline = "This is not a test"
entries[1].headline = "This is no longer a test"
Entry.objects.bulk_update(entries, ["headline"])

…比以下做法更好

entries[0].headline = "This is not a test"
entries[0].save()
entries[1].headline = "This is no longer a test"
entries[1].save()

請注意,此方法有一些注意事項,因此請確保它適合您的使用案例。

批次插入

在將物件插入 ManyToManyFields 時,請使用帶有多個物件的 add() 來減少 SQL 查詢的數量。例如

my_band.members.add(me, my_friend)

…比以下做法更好

my_band.members.add(me)
my_band.members.add(my_friend)

...其中 BandsArtists 具有多對多關係。

當將不同的物件對插入 ManyToManyField 或當定義了自訂的 through 表格時,請使用 bulk_create() 方法來減少 SQL 查詢的次數。例如

PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.bulk_create(
    [
        PizzaToppingRelationship(pizza=my_pizza, topping=pepperoni),
        PizzaToppingRelationship(pizza=your_pizza, topping=pepperoni),
        PizzaToppingRelationship(pizza=your_pizza, topping=mushroom),
    ],
    ignore_conflicts=True,
)

…比以下做法更好

my_pizza.toppings.add(pepperoni)
your_pizza.toppings.add(pepperoni, mushroom)

…其中 PizzaTopping 之間存在多對多關係。請注意,這個方法有一些注意事項,因此請確保它適合您的使用案例。

批量移除

當從 ManyToManyFields 移除物件時,請使用帶有多個物件的 remove() 來減少 SQL 查詢的次數。例如

my_band.members.remove(me, my_friend)

…比以下做法更好

my_band.members.remove(me)
my_band.members.remove(my_friend)

...其中 BandsArtists 具有多對多關係。

當從 ManyToManyFields 移除不同的物件對時,請在帶有多個 through 模型實例的 Q 表達式上使用 delete() 來減少 SQL 查詢的次數。例如

from django.db.models import Q

PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.filter(
    Q(pizza=my_pizza, topping=pepperoni)
    | Q(pizza=your_pizza, topping=pepperoni)
    | Q(pizza=your_pizza, topping=mushroom)
).delete()

…比以下做法更好

my_pizza.toppings.remove(pepperoni)
your_pizza.toppings.remove(pepperoni, mushroom)

…其中 PizzaTopping 之間存在多對多關係。

返回頂部