若是一個web請求須要花費幾秒,99%是由於數據庫沒用好。 當使用ORM的時候,很天然地會想要用python的思惟方式來處理數據查詢,可是這種思惟方式會殺死你的性能。改用子查詢(subqueries)和annotations,以sql的思惟思考,能夠大幅度提升你的web性能。python
有一天你打開Datadog,看到一張這樣的圖: web
對於一次web請求,數據庫分配到的工做越多,數據庫請求次數越少,效率越高。sql
若是將這644次數據庫請求轉換成一次,響應速度能夠提升將近40倍。 數據庫
有一個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實例。對於數據量小的應用到沒什麼,可是一旦數據庫一大,對性能照成的影響是巨大的。函數
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只有一次。
一次查詢效率比屢次查詢高。 殺死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吧!
關注個人微信公衆號