我们检查了 666 个 Django 项目,发现其中存在低效的数据库查询。超过一半的项目存在以下 4 种反模式。
所有成熟的代码库都包含反模式:引入安全漏洞的代码问题、增加代码库使用成本的技术债务,以及违反最佳实践而导致代码速度变慢的代码。
Django Doctor会进行静态分析,以查找并自动修复此类问题。我们检查了 666 个 Django 代码库,以查找低效的 Django ORM 调用,并惊讶于问题的严重程度:在我们检查的所有 Django 代码库中,有 50% 存在以下反模式:
- 16% 被用作
queryset.count() > 0替代queryset.exists() - 15% 被用作
len(queryset)替代queryset.count() - 10%
if queryset:已检查if queryset.exists(): - 10% 的人没有直接使用外键。
这些行为轻则减慢 Django 应用的速度,重则会导致生产环境瘫痪。
你会如何解决这些性能反模式?试试我们的 Django 性能重构挑战吧。
鉴于超过一半的 Django GitHub 代码库都存在这些问题,你的代码库很可能至少存在其中一个问题。经验丰富的 Django 开发人员可能认为自己不会犯这些错误,但事实是,开发人员并非孤立存在:随着时间的推移,开发人员会更换团队,接手积累了技术债务的旧代码库,并且会审查初级开发人员编写的代码。因此,以下几点至关重要:
- 避免犯错
- 知道如何有效地发现并纠正错误
- 知道如何向初级开发人员解释为什么这是一个问题。
使用.count() > 0代替.exists()
比较操作的queryset.count()效率低于检查操作queryset.exists()。Django文档建议,queryset.count()如果只需要统计结果数量,则使用比较操作queryset.exists();如果只需要确定是否存在至少一个结果,则使用检查操作。
原因在于,`SELECT`queryset.count()会执行 SQL 操作,扫描数据库表中的每一行来计算总和。而 `SELECT` 则以最优化的方式queryset.exists()读取单个记录:
- 取消排序
- 移除分组
- 清除查询集中所有开发者定义的
select_related内容distinct
所以,与其说是
def check_hounds():
queryset = HoundsModel.objects.all()
if queryset.count() > 0:
return "oh no. Run!"
Django Doctor 会自动将其修复为:
def check_hounds():
queryset = HoundsModel.objects.all()
if queryset.exists():
return "oh no. Run!"
使用len(queryset)代替queryset.count()
len(queryset)该方法在应用程序级别执行计数。这比queryset.count()直接在数据库级别进行计算并返回计数结果效率低得多。
查询集采用惰性求值——这意味着只有在代码与数据交互时才会从数据库中读取记录。这就是为什么我们无需queryset.all()从数据库中下载所有记录的原因。
一个与数据交互的例子是执行以下操作len(queryset)。这将读取查询集中的所有记录:实际上是通过互联网下载数据库。效率并不高。
另一方面,`doing` 方法queryset.count()通过执行类似 `SQL` 的语句在数据库层面处理计算SELECT COUNT(*) FROM table。这意味着使用 ` queryset.count()doing` 方法可以加快代码运行速度,并提高数据库性能。此外,试想一下,下载 5000 条记录仅仅是为了检查长度然后丢弃它们,这该是多么浪费资源!
所以,与其说是
def check_hounds():
queryset = HoundsModel.objects.all()
if len(queryset) > 2:
return "oh no. Run!"
Django Doctor 会自动将其修复为:
def check_hounds():
if HoundsModel.objects.count() > 2:
return "oh no. Run!"
话虽如此,如果检查长度后还需要读取记录,那么也len(queryset)可能有效。
使用if queryset:代替if queryset.exists():
与上述类似,由于需要对查询集进行求值,因此可以将数据库表中的每一行都加载到内存中。检查查询集是真值还是假值远不如直接检查查询集的真假值高效queryset.exists()。
如果表非常大,这种方法效率尤其低下:它会导致数据库 CPU 使用率飙升,并占用 Web 服务器的大量内存。因此,与其从表中逐行读取数据,queryset.exists()不如使用 `read_file` 方法,该方法可以非常高效地从表中读取单个记录。
所以,与其说是
def check_hounds():
queryset = HoundsModel.objects.all()
if queryset:
return "oh no. Run!"
Django Doctor 会自动将其修复为:
def check_hounds():
queryset = HoundsModel.objects.all()
if queryset.exists():
return "oh no. Run!"
不要直接使用外键
在使用外键时,访问 `get`model_instance.related_field.id会导致在计算 `get` 时读取数据库related_field.id。可以通过使用 `get` 来避免这种情况model_instance.related_field_id,因为 Django 已经将外键值缓存到对象中,从而提高效率。
所以,与其说是
def check_hounds(pk, farm_ids):
hound = HoundsModel.objects.get(pk=pk)
if hound.farm.id in farm_ids:
...
Django Doctor 会自动将其修复为:
def check_hounds(pk, farm_ids):
hound = HoundsModel.objects.get(pk=pk)
if hound.farm_id in farm_ids:
...
你的 Django 代码是否存在这些反模式?
随着时间的推移,技术债务很容易悄悄潜入你的代码库。我可以用django.doctor帮你检查一下,或者也可以查看你的 GitHub PR:
或者尝试一下Django 重构挑战。
文章来源:https://dev.to/codereviewdoctor/666-django-projects-checked-for-inefficient-database-queries-over-half-had-these-4-anti-patterns-4383
