請不要以python思維對待django ORM

liaochangjiang發表於2019-02-05

如果一個web請求需要花費幾秒,99%是因為資料庫沒用好。 當使用ORM的時候,很自然地會想要用python的思維方式來處理資料查詢,但是這種思維方式會殺死你的效能。改用子查詢(subqueries)和annotations,以sql的思維思考,可以大幅度提高你的web效能。

有一天你開啟Datadog,看到一張這樣的圖:

請不要以python思維對待django ORM
紅色的區域表示進行了資料庫請求。這一次web請求進行了644次資料庫請求!只有18.6%的時間在做真正有用的事。單次的資料庫請求是很快的,但是這麼多請求加起來就會嚴重拖慢web請求速度。 在django這個上下文下,每一次資料庫請求,都需要分配記憶體,model和資料庫對映時,還需要序列化和反序列化,然後還要通過網路傳輸資料。

對於一次web請求,資料庫分配到的工作越多,資料庫請求次數越少,效率越高。

如果將這644次資料庫請求轉換成一次,響應速度可以提高將近40倍。

請不要以python思維對待django ORM

資料庫查詢效能清單

  • 無論資料大小,請求次數是不是都是常數?
  • 你是否只從資料庫取真正需要的資料?
  • 這個問題只能使用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吧!

關注我的微信公眾號

請不要以python思維對待django ORM

相關文章