Django Diving — QuerySet Performance

Falldog Hsieh
GoFreight HQ
Published in
8 min readJul 13, 2020
Photo by Faisal M on Unsplash

Django framework 應該是 Python 裡最受歡迎也最多人使用的 Web framework 了吧。曾經在使用 Django 一陣子後,有思考過是不是可以把 Django 內建的 ORM 替換成另一個知名的 ORM library — SQLAlchemy,有看到其他人分享替換成功的教學,但是自己再仔細想了一下,如果把 Django 的核心模組 ORM 換掉的話,那 Django 還剩下什麼!? 靈魂都沒了,也只是空留驅殼而已吧。

Django ORM 裡面,把所有讀取資料庫的功能全部都包進 QuerySet裡頭,說穿了,QuerySet 也只是一種封裝好的糖衣,讓你可以不用寫那落落長的 Raw SQL 就可以輕鬆的讀到你想要的資料。但是吃糖是要有代價的,不看說明書猛吃糖,是會糖尿病的… 😆😆😆

以下,介紹一下各式各樣在操作 Django QuerySet 時,應該注意到的細節

# Preparation

這次的文章並不會深入研究 Django QuerySet 的 source code。俗話說的好,工具選得好,要飯要到老,想要深入了解 QuerySet 的操作細節時,需要一些好的工具來輔助。

## 1. django-extensions (Document)

$ pip install django-extensions

(安裝細節就不贅述了,非本文重點) django-extensions 最方便的地方就是它的 command shell_plus ,在進入 shell_plus 後,會自動的將所有 INSTALL_APPS 的 model 們,自動 import ,省去自己 import 的麻煩。另一個,也是最重要的,就是每次輸入執行任何 expression 時,會自動印出任何跟 database 有關操作的 Raw SQL,所以可以在 Django shell 裡操作一些 QuerySet 試試看,就可以發現一些平常不會注意到的細節。

$ ./manage.py shell_plus --print-sql

需要注意的是,預設 shell_plus 印出來的 RawSQL 內容長度只有 1000,如果要加長的話,可以在 settings 裡面定義變數 SHELL_PLUS_PRINT_SQL_TRUNCATE ,設 None 的話,就不會有上限了。

## 2. [optional] django-debug-toolbar (Document)
可以在 GUI 上,直接看目前的 view / api 產生的所有 DB 操作的 Raw SQL 與 call stack。除了套 django-silk 或是 New Relic 之外,靠 django-debug-toolbar 簡單看特定的 view / api 的 Raw SQL 讀取 DB 的時間、次數…等等。

$ pip install django-debug-toolbar

# Model example

以下會介紹各式各樣的 QuerySet,都會基於這個 blog app 的 model 來做試範,就是寫個簡單的 Blog ,有 Author 與 Tag,每一篇 Blog 會有一個 Author ,與多個 Tag。

# Basic — QuerySet cache

Django 的 QuerySet 的大部分操作都會回傳 QuerySet,所以可以用 chain 的寫法,把一堆操作用比較高的可讀性來讀出想要的 data。

## 1. QuerySet expression apply database query
QuerySet 真正對 DB 讀取資料的時間點,只有在真的對 QuerySet 做 evaluate 的時間點 (ex: list, for loop, len, …) (範例如下…)

## 2. QuerySet cache
QuerySet 在跟 DB 取得資料後,會把資料暫存起來,所以之後同一個 QuerySet 的操作,是不會再跟 DB 要資料的。(範例如下…)

要注意的是,QuerySet 的大部分操作回傳的都是「新的」QuerySet 副本,所以,如果原本的 QuerySet 已經有 cache 的資料後,再用 .all() .filter() 都會是新的副本,而且沒有 cache 的資料,所以再進行一次 evaluate,會再跟 DB 要資料的。(範例如下… —-print-sql)

# Basic — database operation

QuerySet 的 .count().exists() 的使用點時機,會影響執行時會不會對 DB 做讀取的動作。主要的差別來自於 QuerySet 的 cache 的行為,在有 cache 後,都不會對 DB 再做額外的讀取。(範例如下… —-print-sql)

# Basic — select_related & prefetch_related

Django 的 ORM 封裝下,當你想要讀取兩個 Table 的資料時,你可能不會清楚 Django 怎麼做 SQL JOIN 的,這邊也不會深入探討。但是,至少要了解的是 Django 怎麼處理 ForeignKeyManyToManyField 的。

在預設的情形下,Django ORM 是不會自動幫你把 ForeignKeyManyToManyField 的資料自動 JOIN 進來的,當你試著操作 Blog 的 ForeignKey author 時,才會再一次從 DB 讀取 author 的資料出來。所以當沒有做額外的處理的話,以下的邏輯會對 DB 讀取 2 次。( 範例如下… —-print-sql)

如果想對 ForeignKeyManyToManyField 增進讀取的效能,降低操作 DB 的次數的話,就必須透過 select_related()prefetch_related()

## select_related()
針對 ForeignKey 或是 OneToOneField 這種 * -> 1 或是 1 -> 1的關係,Django 就會對指定的欄位做 JOIN。( 範例如下… —-print-sql)

## prefetch_related()
針對 ManyToManyField 或是 ForeignKey 的 related_name 這種 1 -> * 或是 * -> * 的關係,Django 會對指定的欄位多做一次 DB 讀取,把所有指定欄位的資料一口氣抓回來。( 範例如下… —-print-sql)

# Advance — Prefetch class

Django 提供了一個 Prefetch class ,讓我們在處理 prefetch_related() 時,可以做更細膩的操作。想像一下,如果今天我們的 ManyToManyField tags 裡面還有一個 ForeignKey 的欄位,該怎麼用 prefetch_related() 來提早做 JOIN 呢? 透過 Prefetch 指定做好 select_related() 的 queryset 即可在 prefetch 的同時,一起做 JOIN 了。( 範例如下… —-print-sql)

# Advance — prefetch_related_objects

針對 ManyToManyField ,如果沒有一開始就做好 prefetch 的話,接下來存取裡面的內容時,都會做一次 DB 的讀取,而且都無法被 cache 起來。比如我們想操作 blog.tags.all() ,對 Django QuerySet 來說 .all() 就是「複製」一份新的 QuerySet 副本出來,所以這一次讀出來的 QuerySet 如果沒被用變數儲存起來,下一次再透過 blog.tags.all() 操作內容的話,又會再對 DB 做一次讀取。

通常會有這種無法一開始就做好 prefetch 的情形,都是在一開始的 QuerySet 是不需要 ManyToManyField 的資料,在相對深層的 function 在處理時,某些條件才會去操作 ManyToManyField ,對一個深層的 function 來說,你不能預期傳進來的 QuerySet 到底有沒有做過 Prefetch ,此時就可以用 prefetch_related_objects() 做一次 cache ,之後深層的 function 如果會被重覆呼叫到的話,也不會多做 DB 的讀取了。( 範例如下… —-print-sql)

Conclusion

以上的範例都是基於 Django 3.0.8 ,django-extensions 3.0.2,Database SQLite

所條列出來的項目,都會影響到 performance ,如果你的 view or api 還沒遇到效能瓶頸的話,其實是可以忽略的,畢竟不用過早最佳化,也是一個指標。但是一但遇到效能問題,上面條列的項目應該可以解決七八成的問題吧,剩下的就是動搖國本的修正了,可能需要改作法、改架構、加暫存的欄位、拆 Table 等等的,也就只能含淚吞下去了。

喜歡我的文章的話,記得按讚、訂閱以及分享喔 😆😆😆

你知道我在找你嗎?

有興趣加入 GoFreight 團隊一起解決世界級的難題的話,請參考 Teamdoor 職缺表,等待高手的加入。

--

--