Используйте массовые запросы, предварительно загружайте внешние ключи и т. Д.
Мотивация
Вы закончили свое следующее веб-приложение 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 секунды.
Итак, вы собираете все необходимые идентификаторы и передаете их в одном запросе.
Бонус
Вы можете сделать свое приложение еще быстрее с помощью этой статьи: