Используйте массовые запросы, предварительно загружайте внешние ключи и т. Д.

Мотивация

Вы закончили свое следующее веб-приложение Django, но оно работает слишком медленно? Теперь вы думаете, что вам следовало перейти на C ++ или Java? Да ладно, не о чем беспокоиться, некоторый рефакторинг вашего кода может сделать ваше приложение вдвое быстрее!

Например, вместо того, чтобы вставлять объекты по одному, вы можете вставить 1000 объектов за один раз. Другой пример - выборка данных по внешнему ключу - он каждый раз попадает в базу данных. Вы можете заранее загрузить все необходимые данные, знаете ли вы об этом?

Подготовка

Здесь мы будем играть с двумя действительно простыми моделями:

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)

Итак, у нас много учеников, и у этих учеников есть оценки. Нет ничего проще.

Анализируйте ваши запросы

Во-первых, вам нужно увидеть все взаимодействия с базой данных. А именно, мы хотели бы видеть все SQL-запросы, которые Django делает, когда вы запрашиваете некоторые данные.

Для этого перейдите в свой 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.

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']

Мы можем видеть время, необходимое для вставки Student вместе с данными запроса.

Массовая вставка

Давайте попробуем вставить 1000 студентов с 10 оценками каждый:

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()

Для завершения потребовалось 9,45 секунд, а в файле журнала содержится 11000 строк.

Теперь проделайте то же самое массово:

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)
Grade.objects.bulk_create(grades, batch_size)

Вы не поверите, но прошло 0,26 секунды, а журнал состоит из 5 строк!

Значит, он примерно в 35 раз быстрее !!! Хотя мы делаем дополнительный запрос для студентов посередине!

Массовое обновление

Хорошо, а как насчет обновлений? Допустим, мы хотим, чтобы все оценки были равны 100:

grades = Grade.objects.all()
for grade in grades:
    grade.grade = 100
    grade.save()

Обновление заняло 0,455 секунды. Неплохо для 10 000 записей и базы данных SQLite :)

Хорошо, попробуем массовое обновление:

batch_size = 500
grades = Grade.objects.all()
for grade in grades:
    grade.grade = 100
Grade.objects.bulk_update(grades, ['grade'], batch_size=batch_size)

Прошло 0,11 секунды! В пять раз быстрее! Ага!

Предварительная загрузка данных (выберите связанные)

Давайте переберем все записи и распечатаем каждую оценку каждого ученика:

grades = Grade.objects.all()
for grade in grades:
    print(f"The student {grade.student.first_name} has got {grade.grade}%")

4,9 секунды и 10 000 запросов!

Вы не поверите, но добавление нескольких символов в код сделает то же самое за 0,19 секунды:

grades = Grade.objects.select_related("student").all()
for grade in grades:
    print(f"The student {grade.student.first_name} has got {grade.grade}%")

И он делает один-единственный запрос SELECT!

Идея состоит в том, что все связанные данные также извлекаются в рамках одного запроса.

Выбрать массово

В предыдущем примере вы можете запросить некоторых студентов в отдельном запросе. Скажем, мы хотим, чтобы все студенты, у которых было 100, на любом курсе. В нашем случае мы получаем всех студентов, но нам все равно.

Обратите внимание, что мы не используем здесь select_related:

ids = []
grades = Grade.objects.all()
for grade in grades:
    if grade.grade == 100:
        ids.append(grade.student_id)
students = Student.objects.in_bulk(ids)
for student in students:
    print(student.first_name, student.last_name)

Этот код также выполняется за 0,19 секунды.

Итак, вы собираете все необходимые идентификаторы и передаете их в одном запросе.

Бонус

Вы можете сделать свое приложение еще быстрее с помощью этой статьи: