如果一个 web 请求需要花费几秒, 99% 是因为数据库没用好. 当使用 ORM 的时候, 很自然地会想要用 python 的思维方式来处理数据查询, 但是这种思维方式会杀死你的性能. 改用子查询 (subqueries) 和 annotations, 以 sql 的思维思考, 可以大幅度提高你的 Web 性能.
有一天你打开 Datadog, 看到一张这样的图:
红色的区域表示进行了数据库请求. 这一次 Web 请求进行了 644 次数据库请求! 只有 18.6% 的时间在做真正有用的事. 单次的数据库请求是很快的, 但是这么多请求加起来就会严重拖慢 Web 请求速度. 在 django 这个上下文下, 每一次数据库请求, 都需要分配内存, model 和数据库映射时, 还需要序列化和反序列化, 然后还要通过网络传输数据.
对于一次 Web 请求, 数据库分配到的工作越多, 数据库请求次数越少, 效率越高.
如果将这 644 次数据库请求转换成一次, 响应速度可以提高将近 40 倍.
数据库查询性能清单
无论数据大小, 请求次数是不是都是常数?
你是否只从数据库取真正需要的数据?
这个问题只能使用 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 吧!
来源: https://juejin.im/post/5c58efb25188256283253121