请不要以python思维对待django ORM

如果一个web请求需要花费几秒,99%是因为数据库没用好。 当使用ORM的时候,很自然地会想要用python的思维方式来处理数据查询,但是这种思维方式会杀死你的性能。改用子查询(subqueries)和annotations,以sql的思维思考,可以大幅度提高你的web性能。

有一天你打开Datadog,看到一张这样的图:

红色的区域表示进行了数据库请求。这一次web请求进行了644次数据库请求!只有18.6%的时间在做真正有用的事。单次的数据库请求是很快的,但是这么多请求加起来就会严重拖慢web请求速度。
在django这个上下文下,每一次数据库请求,都需要分配内存,model和数据库映射时,还需要序列化和反序列化,然后还要通过网络传输数据。
对于一次web请求,数据库分配到的工作越多,数据库请求次数越少,效率越高。

如果将这644次数据库请求转换成一次,响应速度可以提高将近40倍。

数据库查询性能清单

  • 无论数据大小,请求次数是不是都是常数?
  • 你是否只从数据库取真正需要的数据?
  • 这个问题只能使用Python循环解决吗?

打破Python思维模式

有一个City model,其中有一个计算城市人口密度的方法density。

1
2
3
4
5
6
7
复制代码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

想要计算一个城市的人口密度,下面这种方式是很自然就能想到的:

1
2
3
4
5
6
7
8
9
复制代码>>> 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)的城市时:

1
2
3
4
5
6
7
8
复制代码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次数利器。

1
2
3
4
5
6
7
8
9
10
11
复制代码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的值。

1
2
3
复制代码self.annotate(
density=F('population') / F('land_area_km')
)

表示对于一个queryset,给他其中的每一项object,加上一个density字段,值为population /land_area_km。

1
2
3
4
5
6
复制代码>>> 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')), ...]>

解释一下:

1
复制代码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的。

1
复制代码illinois.city.dense_cities().values_list('name', 'density')

这个查询语句的queryset是illinois州的所有城市。

这种方法比前面循环的方法效率高多了,因为IO只有一次。

使用subquery

一次查询效率比多次查询高。
杀死django性能最简单的方式就是在for循环中使用query。

要筛选出所有存在dense城市的州:

1
2
3
4
复制代码[
state for state in State.objects.all()
if state.cities.dense_cities().exists()
]

类似这种,exists()会进行一次额外的查询,这会累计很多次毫秒级的查询。加起来的时间也是很可观的。可以用subquery解决这个问题。

最基本的使用方法:

1
2
3
4
复制代码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结合起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码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字段。

最后使用这个查询:

1
复制代码State.objects.add_dense_cities().filter(has_dense_cities=True)

总结

提高数据库查询效率的一个重要原则就是降低IO查询次数,尽量避免使用for循环,试试annotate和subquery吧!

关注我的微信公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%