最近在django项目中碰到了N+1问题,就是查询出数据库的结果后,需要遍历处理所有queryset对象的时候,django默认还会去查询一次数据库,这就导致了ORM的N+1问题,其实叫1+N问题更好,就是1个总的查询,加上查询了n个对象,就会出现n次额外不必要的查询。
1. 出现背景
代码中存在assets和systems两个model,代码如下
1 | class Systems(TimestampModel): |
在输出的时候使用了以下代码:
1 | all_assets = models.Assets.objects.all() |
在这种情况下,all_assets = models.Assets.objects.all()
会执行一次查询,将所有的assets查询出来,而在后面的遍历中,django会默认执行n个数量的查询,n等于资产的数据。
同时,我在get_system_name
的属性中执行了一次关联查询,会查询到第一个system的名字,这样其实产生的查询是1+n*n,效率更加低。
在数据量较大的时候,会产生很严重的数量问题。我在导出2w左右资产的时候,此处的性能消耗达到了20多分钟的时间。
2. 探索解决
为了便于检查SQL执行的情况,可以在django的settings文件中加入以下配置,开启日志显示:
1 | LOGGING = { |
@cached_property
使用@property
属性计算有一个不好的地方,就是每次访问该属性的时候,该属性会重新计算一次,也就是说,在一段代码中访问n次property,就会产生n次全量计算,如果在property中进行了时间比较长的IO操作的话,就会导致比较严重的性能问题,所以python和django均提供了@cached_property
方法来缓存属性。python的包在 from functools import cached_property
,django的包在from django.utils.functional import cached_property
。
加上该缓存属性后,将self.system_count
的计算独立出来,
1 | class Assets(TimestampModel): |
这样会减少一部分查询,但是仍然查询量很大,n的问题仍然没有解决。
prefetch_related
django针对这种情况提供了两个数据库的api,一个是select_related()
另一个是prefetch_related()
,通过这两个API,数据库可以先进行join查询,一次性将关联的数据查询出来,后续在遍历的时候,在该查询的queryset上进行遍历,不会再次查询。
select_related()
主要针对外键查询,prefetch_related()
针对多对多关系的查询。
经过优化后,查询的代码如下
1 | all_assets = models.Assets.objects.all().prefetch_related(Prefetch('system', queryset=models.Systems.objects.all())) |
经过优化后,查询次数可以减少到3次,运行速度大幅度提升。prefetch_related()
还有其他注意的细节部分,可以参考django文档https://docs.djangoproject.com/zh-hans/4.1/ref/models/querysets/#prefetch-related
3. 总结
经过优化后,查询3万资产的时间从20多分钟,减少到1分钟左右,速度提升明显,但是在[asset.to_zh_dict() for asset in all_assets]
循环遍历所有资产的时候,仍然速度较慢,这个跟python语言循环慢很有关系,后续可以考虑异步进行优化。
4. 参考文档
1、django文档:https://docs.djangoproject.com/zh-hans/4.1/ref/models/querysets/#prefetch-related
2、https://blog.labdigital.nl/working-with-huge-data-sets-in-django-169453bca049
5、https://scoutapm.com/blog/django-and-the-n1-queries-problem
6、https://stackoverflow.com/questions/23489548/how-do-i-invalidate-cached-property-in-django
7、https://medium.com/@fdemmer/django-cached-property-on-models-f4673de33990
8、https://blog.csdn.net/study_in/article/details/95366421
9、https://www.cnblogs.com/garyhtml/p/15965617.html
10、https://www.reddit.com/r/django/comments/6aexjt/using_prefetch_with_drf_viewsets_to_include_a/