如何编写快上千倍的 Django 查询?

1. 写在前面

你是否在为 Django Web 应用程序运行速度太慢而犯愁?是否在考虑使用 Go 或者 Java 进行重写?本文通过对现有 Django 代码进行一些简单的重构,让应用程序快上几倍。

例如,你可以一次插入 1000 个对象,而不是一个一个地插入。又或者通过外键获取数据,它每次都会访问数据库不仅耗时同时加大数据库开销,你可以考虑预先加载所有需要的数据,减少数据查询次数。

公众号: 滑翔的纸飞机

2. 准备重构

那接下去我们就正式介绍。本文均在本地测试,数据库采用SQLite;

2.1 准备工作

在这里,我们将使用两个非常简单的模型来进行说明:

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)  # 成绩

考虑,我们有很多学生,这些学生有对应的课程和成绩。应该没有比这更简单的数据模型了。

2.2. 打开 SQL 日志输出

首先,我们需要查看 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']

我们可以看到插入一个学生和所需的时间。

2.3. 批量插入

让我们尝试插入 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

2.4. 批量更新

很好,那么更新呢?比方说,我们想让所有成绩都等于 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)

2.5. 预载数据

让我们遍历所有记录,打印出每个学生的成绩:

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 查询!
这样做的目的是在同一查询中获取所有相关数据。

2.6. 批量选择

在前面的示例中,你可能希望在一个单独的查询中查询部分学生。比方说,我们想查询所有课程中成绩达到 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。

3. 最后

避免对数据库进行过多查询,因为这会降低应用程序的速度。使用查询优化技术,除了上诉提到的 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)
感谢您花时间阅读文章
关注公众号不迷路

你可能感兴趣的:(django,python)