你是否在为 Django Web 应用程序运行速度太慢而犯愁?是否在考虑使用 Go 或者 Java 进行重写?本文通过对现有 Django 代码进行一些简单的重构,让应用程序快上几倍。
例如,你可以一次插入 1000 个对象,而不是一个一个地插入。又或者通过外键获取数据,它每次都会访问数据库不仅耗时同时加大数据库开销,你可以考虑预先加载所有需要的数据,减少数据查询次数。
公众号: 滑翔的纸飞机
那接下去我们就正式介绍。本文均在本地测试,数据库采用SQLite;
在这里,我们将使用两个非常简单的模型来进行说明:
from django.db import models
class Student(models.Model):
"""
学生
"""
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
class Grade(models.Model):
"""
课程成绩
"""
student = models.ForeignKey(Student, on_delete=models.CASCADE)
course = models.CharField(max_length=100) # 课程
grade = models.IntegerField(default=0) # 成绩
考虑,我们有很多学生,这些学生有对应的课程和成绩。应该没有比这更简单的数据模型了。
首先,我们需要查看 Django 与数据库的所有交互。也就是说,我们希望看到 Django 在查询某些数据时进行的所有 SQL 查询。
为此,请在 settings.py 中添加以下代码:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': 'sql.log',
},
},
'loggers': {
'django.db.backends': {
'handlers': ['file'],
'level': 'DEBUG',
'propagate': True,
},
},
}
就是这样。现在,与数据库的所有交互都将存储在 sql.log 文件中。sql.log 文件默认在 Django 项目根路径下。
.
├── db.sqlite3
├── demo01
├── manage.py
├── mysite
└── sql.log
现在,让我们添加一些学生,看看 sql.log 中都写了些什么。
In [1]: from demo01.models import Student, Grade
In [2]: student = Student()
...: student.first_name = "1"
...: student.last_name = "2"
...: student.save()
日志输出:
(0.002) INSERT INTO "student_student" ("first_name", "last_name") VALUES ('1', '2'); args=['1', '2']
我们可以看到插入一个学生和所需的时间。
让我们尝试插入 1000 个学生,每个学生有 10 门课程:
In [4]: from demo01.models import Student,Grade
...: for i in range(1000):
...: student = Student()
...: student.first_name = str(i)
...: student.last_name = str(i)
...: student.save()
...: for j in range(10):
...: grade = Grade()
...: grade.student = student
...: grade.course = "Math"
...: grade.grade = j*10
...: grade.save()
它花了 12.346 秒才完成,SQL日志输出 11000 行。
现在以批量的方式做同样的事情:
In [5]: students = []
...: grades = []
...: batch_size = 500
...: for i in range(1000):
...: student = Student()
...: student.first_name = str(i)
...: student.last_name = str(i)
...: students.append(student)
...: Student.objects.bulk_create(students, batch_size)
...: for student in Student.objects.all():
...: for j in range(10):
...: grade = Grade()
...: grade.student = student
...: grade.course = "Math"
...: grade.grade = j*10
...: grades.append(grade)
你一定不会相信,这只用了 0.006 秒!SQl输出只有5行;
因此,速度快了大约 2000 倍!!!即使我们在中间为 “student” 做了一个额外的查询!
【备注】不同环境验证,时间存在差异,本环境多次验证均在这个数量级。这里我们统计 sql 日志输出中的时间统计,即下例中0.004:
(0.004) INSERT INTO "demo01_student" ("first_name", "last_name") VALUES ('0', '0'); args=['0', '0']; alias=default
很好,那么更新呢?比方说,我们想让所有成绩都等于 200:
In [19]: grades = Grade.objects.all()
...: for grade in grades:
...: grade.grade = 200
...: grade.save()
更新耗时 11.041 秒。更新10 000 条记录;
很好,让我们尝试批量更新:
In [22]: batch_size = 1000
...: grades = Grade.objects.all()
...: for grade in grades:
...: grade.grade = 200
...: Grade.objects.bulk_update(grades, ['grade'], batch_size=batch_size)
用时 0.06 秒!快了184 倍!!!
或许你也可以尝试类似以下的方式进行批量更新:
Grade.objects.all().update(grade=100)
让我们遍历所有记录,打印出每个学生的成绩:
In [38]: grades = Grade.objects.all()
...: for grade in grades:
...: print(f"The student {grade.student.first_name} has got {grade.grade}%")
0.587 秒和 10 000 次查询!
你一定不会相信,在代码中添加几个符号就能在 0.001 秒内完成同样的操作:
In [38]: grades = Grade.objects.select_related("student").all()
...: for grade in grades:
...: print(f"The student {grade.student.first_name} has got {grade.grade}%")
它只进行一次 SELECT 查询!
这样做的目的是在同一查询中获取所有相关数据。
在前面的示例中,你可能希望在一个单独的查询中查询部分学生。比方说,我们想查询所有课程中成绩达到 200 分的学生。在我们下面的例子中,我们得到的是所有学生,但这并不重要。
请注意,这里我们不使用 “select_related”:
In [44]: ids = []
...: grades = Grade.objects.all()
...: for grade in grades:
...: if grade.grade == 200:
...: ids.append(grade.student_id)
...: students = Student.objects.in_bulk(ids)
...: for student in students:
...: print(student)
这段代码的运行时间是 0.06 秒,在一次查询中收集并为下个查询传递所需的所有 ID。
避免对数据库进行过多查询,因为这会降低应用程序的速度。使用查询优化技术,除了上诉提到的 select_related 还包括 prefetch_related,来最大程度地减少查询数。
最后,善用 only 避免过大的查询集:
# Bad code
users = User.objects.all()
for user in users:
print(user.username)
# Good code
users = User.objects.only('username')
for user in users:
print(user.username)