如果一个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 | 复制代码class City(models.Model): |
想要计算一个城市的人口密度,下面这种方式是很自然就能想到的:
1 | 复制代码>>> illinois = State.objects.get(name='Illinois') |
问题出在当我们想要查询出所有拥挤(密度大于4000)的城市时:
1 | 复制代码class City(models.Model): |
如果只有5%的城市是拥挤的,那么将会有95%的数据最终会被丢弃。**在数据中过滤,一定是比将数据导入内存,然后让Python过滤效率要高的!**对于不需要的数据,django都需要花时间完成额外、无意义的操作:将数据转换成model实例。对于数据量小的应用到没什么,但是一旦数据库一大,对性能照成的影响是巨大的。
使用annotate
objects = CitySet.as_manager()这一行表示对City这一model使用自定义的ModelManager,这里不展开讲了,有兴趣可以自己搜索一下。
关于annotate的使用,请参考今天一起发的另一篇文章:Django annotation,减少IO次数利器。
1 | 复制代码class CitySet(models.QuerySet): |
annotate(density=F(‘population’) / F(‘land_area_km’))中的F aggregate函数表示获取population和land_area_km的值。
1 | 复制代码self.annotate( |
表示对于一个queryset,给他其中的每一项object,加上一个density字段,值为population /land_area_km。
1 | 复制代码>>> City.objects.dense_cities().values_list('name', 'density') |
解释一下:
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 | 复制代码[ |
类似这种,exists()会进行一次额外的查询,这会累计很多次毫秒级的查询。加起来的时间也是很可观的。可以用subquery解决这个问题。
最基本的使用方法:
1 | 复制代码state_ids = City.objects.dense_cities().values('state_id') |
这样就把很多次的exists查询降低到了一次。
更进一步,和前面说过的annotate结合起来:
1 | 复制代码class StateSet(models.QuerySet): |
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吧!
关注我的微信公众号
本文转载自: 掘金