[筆記] Django Query 優化

Lin Po-An
4 min readNov 16, 2017

--

最近處理的 server 有個 api 每次都要花好幾秒才能處理完,所以就開始著手研究怎麼把它優化。在 Django 中基本的優化就是想辦法用select_relatedprefetch_related去降低 server 對 DB 的 query 次數。雖然一直都知道這件事情,平常有時候記得的話偶爾也會用,但這次真的優化起來才發現沒這麼簡單,發現有一些小細節要處理:

首先,對於 Foreign key,如果是正向的,可以直接用 select_related,可是反向的,又或者 ManyToMany 的情況就要用 prefetch_relatedDjango可以對不止一層的關係做 related 處理,不過如果有任何一層是反向或 ManyToMany 就要用 prefetch_related 。不過其實用錯很快就會噴 error,所以這應該只是基本常識(但我其實之前一直沒有分得很清楚哈哈)。

再來,使用prefetch_related要注意系統會幫你先做好的 query 是什麼。以官網的例子為例:Pizza.objects.all().prefetch_related(‘toppings’),那實際上,他會先幫你將 `toppings.all()` query 起來,所以下面這段 code 就可以用到 prefetch 的特性不會對 for loop 裡的 topping 去每次 query。

pizzas = Pizza.objects.all().prefetch_related(‘toppings’)
for pizza in pizzas:
for topping in pizza.toppings.all():
# do something with topping

但是,如果不是用他幫你先存好的 query,那就會沒效果。例如下面這段就無法用到 prefetch:

pizzas = Pizza.objects.all().prefetch_related(‘toppings’)
for pizza in pizzas:
for topping in pizza.toppings.filter(...):
# do something with toppping

要注意的事情是,如果要取第一個值,這邊如果用 .first() 就會多一次 query,因為這不是他先幫你處理過的。在這個情況,反而要用 all()[0] 的話可以省下多餘的 query(不過就要注意有沒有 index 的問題,如果 all() 是空的就會GG)。

pizzas = Pizza.objects.all().prefetch_related(‘toppings’)
for pizza in pizzas:
pizza.toppings.first() <---- query one more time!!
pizza.toppings.all()[0] <---- save your query!!

Annotate 的搭配使用也是個不錯的優化方式。我們原本有類似以下的寫法:

for pizza in pizzas:
pizza.num_toppings = Topping.objects.filter(...).count()

這樣很明顯地就會每一次都會需要 count() 一次,會有 O(n) 個 query。如果使用 annotate() 的話,可以一次把所需的 Count 放進去。

pizzas = Pizza.objects.annotate(num_toppins=Count('toppings'))

不過這樣的缺點就是,如果只想算特定的 Topping 數量,會需要在 annotate 前面先 filter,然後就也會把 pizza 也 filter 掉了。

pizzas = Pizza.objects.filter(toppings__name__contains='wow').annotate(num_wow_toppings=Count('toppings'))

這樣在計算 num_wow_toppings 的時候,就只會把名字有 wow 的放入計算,但,這樣你也同時只會拿到含有 wow toppings的 pizza。像我的實際需求是如果沒有的話,那個值應該要是零,這樣我就只好額外跑一個全部的,然後再額外把這邊拿到的數量填入。雖然麻煩且醜了一些,但至少可以把 O(n) 的 query 次數給降到常數次。

以上就純粹是我嘗試優化的心得筆記,也許其實有更好的寫法或小撇步也歡迎教教我哈哈。

Reference:

https://docs.djangoproject.com/en/1.11/ref/models/querysets/#contains

--

--