如果一個web請求需要花費幾秒,99%是因為資料庫沒用好。 當使用ORM的時候,很自然地會想要用python的思維方式來處理資料查詢,但是這種思維方式會殺死你的效能。改用子查詢(subqueries)和annotations,以sql的思維思考,可以大幅度提高你的web效能。
有一天你開啟Datadog,看到一張這樣的圖:
紅色的區域表示進行了資料庫請求。這一次web請求進行了644次資料庫請求!只有18.6%的時間在做真正有用的事。單次的資料庫請求是很快的,但是這麼多請求加起來就會嚴重拖慢web請求速度。 在django這個上下文下,每一次資料庫請求,都需要分配記憶體,model和資料庫對映時,還需要序列化和反序列化,然後還要通過網路傳輸資料。對於一次web請求,資料庫分配到的工作越多,資料庫請求次數越少,效率越高。
如果將這644次資料庫請求轉換成一次,響應速度可以提高將近40倍。
資料庫查詢效能清單
- 無論資料大小,請求次數是不是都是常數?
- 你是否只從資料庫取真正需要的資料?
- 這個問題只能使用Python迴圈解決嗎?
打破Python思維模式
有一個City model,其中有一個計算城市人口密度的方法density。
class City(models.Model):
state = models.ForeignKey(State, related_name='cities')
name = models.TextField()
population = models.DecimalField()
land_area_km = models.DecimalField()
def density(self):
return self.population / self.land_area_km
複製程式碼
想要計算一個城市的人口密度,下面這種方式是很自然就能想到的:
>>> illinois = State.objects.get(name='Illinois')
>>> chicago = City.objects.create(
name="Chicago",
state=illinois,
population=2695598,
land_area_km=588.81
)
>>> chicago.density()
4578.04...
複製程式碼
問題出在當我們想要查詢出所有擁擠(密度大於4000)的城市時:
class City(models.Model):
...
@classmethod
def dense_cities(cls):
return [
city for city in City.objects.all()
if city.density() > 4000
]
複製程式碼
如果只有5%的城市是擁擠的,那麼將會有95%的資料最終會被丟棄。**在資料中過濾,一定是比將資料匯入記憶體,然後讓Python過濾效率要高的!**對於不需要的資料,django都需要花時間完成額外、無意義的操作:將資料轉換成model例項。對於資料量小的應用到沒什麼,但是一旦資料庫一大,對效能照成的影響是巨大的。
使用annotate
objects = CitySet.as_manager()這一行表示對City這一model使用自定義的ModelManager,這裡不展開講了,有興趣可以自己搜尋一下。 關於annotate的使用,請參考今天一起發的另一篇文章:Django annotation,減少IO次數利器。
class CitySet(models.QuerySet):
def add_density(self):
return self.annotate(
density=F('population') / F('land_area_km')
)
def dense_cities(self):
self.add_density().filter(density__gt=4000)
class City(models.Model):
...
objects = CitySet.as_manager()
複製程式碼
annotate(density=F('population') / F('land_area_km'))中的F aggregate函式表示獲取population和land_area_km的值。
self.annotate(
density=F('population') / F('land_area_km')
)
複製程式碼
表示對於一個queryset,給他其中的每一項object,加上一個density欄位,值為population /land_area_km。
>>> City.objects.dense_cities().values_list('name', 'density')
<QuerySet [("New York City", Decimal('10890.23')), ...]>
# Reverse descriptor
>>> illinois.city.dense_cities().values_list('name', 'density')
<QuerySet [("Chicago", Decimal('4578.04')), ...]>
複製程式碼
解釋一下:
City.objects.dense_cities().values_list('name', 'density')
複製程式碼
這個查詢語句的queryset是所有的city object,應該是直接用City這個model呼叫objects。先呼叫annotate(density=F('population') / F('land_area_km')),給每個object加上density這個欄位,最後篩選出density大於4000的。
illinois.city.dense_cities().values_list('name', 'density')
複製程式碼
這個查詢語句的queryset是illinois州的所有城市。
這種方法比前面迴圈的方法效率高多了,因為IO只有一次。
使用subquery
一次查詢效率比多次查詢高。 殺死django效能最簡單的方式就是在for迴圈中使用query。
要篩選出所有存在dense城市的州:
[
state for state in State.objects.all()
if state.cities.dense_cities().exists()
]
複製程式碼
類似這種,exists()會進行一次額外的查詢,這會累計很多次毫秒級的查詢。加起來的時間也是很可觀的。可以用subquery解決這個問題。
最基本的使用方法:
state_ids = City.objects.dense_cities().values('state_id')
State.objects.filter(id__in=Subquery(state_ids))
// 或者也可以把Subquery省略掉
State.objects.filter(id__in=state_ids)
複製程式碼
這樣就把很多次的exists查詢降低到了一次。
更進一步,和前面說過的annotate結合起來:
class StateSet(models.QuerySet):
def add_dense_cities(self):
return self.annotate(
has_dense_cities=Exists(
City
.objects
.filter(state=OuterRef('id'))
.dense_cities()
)
)
class State(models.Model):
...
objects = StateSet.as_manager()
複製程式碼
filter(state=OuterRef('id'))就是篩選出 state object的所有city,然後呼叫dense_cities篩選dense城市,然後呼叫Exists聚合函式,返回True或False。add_dense_cities就給state queryset裡的每一個object加上了一個has_dense_cities欄位。
最後使用這個查詢:
State.objects.add_dense_cities().filter(has_dense_cities=True)
複製程式碼
總結
提高資料庫查詢效率的一個重要原則就是降低IO查詢次數,儘量避免使用for迴圈,試試annotate和subquery吧!
關注我的微信公眾號