請不要以python思惟對待django ORM

若是一個web請求須要花費幾秒,99%是由於數據庫沒用好。 當使用ORM的時候,很天然地會想要用python的思惟方式來處理數據查詢,可是這種思惟方式會殺死你的性能。改用子查詢(subqueries)和annotations,以sql的思惟思考,能夠大幅度提升你的web性能。python

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

紅色的區域表示進行了數據庫請求。這一次web請求進行了644次數據庫請求!只有18.6%的時間在作真正有用的事。單次的數據庫請求是很快的,可是這麼多請求加起來就會嚴重拖慢web請求速度。 在django這個上下文下,每一次數據庫請求,都須要分配內存,model和數據庫映射時,還須要序列化和反序列化,而後還要經過網絡傳輸數據。

對於一次web請求,數據庫分配到的工做越多,數據庫請求次數越少,效率越高。sql

若是將這644次數據庫請求轉換成一次,響應速度能夠提升將近40倍。 數據庫

數據庫查詢性能清單

  • 不管數據大小,請求次數是否是都是常數?
  • 你是否只從數據庫取真正須要的數據?
  • 這個問題只能使用Python循環解決嗎?

打破Python思惟模式

有一個City model,其中有一個計算城市人口密度的方法density。django

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的值。spa

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吧!

關注個人微信公衆號

相關文章
相關標籤/搜索