Оптимизация доступа к базе данных
В Django слой работы с базой данных предоставляет различные способы помочь разработчикам извлечь максимальную пользу из своих баз данных. Данный документ собирает ссылки на соответствующую документацию и добавляет различные советы, организованные по нескольким разделам, которые описывают шаги по оптимизации использования вашей базы данных.
Профиль сначала
Как общая практика программирования, это само собой разумеется. Узнайте, какие запросы вы выполняете и сколько они вам стоят. Также вы можете использовать сторонний проект, например, django-debug-toolbar, или инструмент, который отслеживает вашу базу данных напрямую.
Помните, что вы можете оптимизировать скорость или память, или и то, и другое, в зависимости от ваших требований. Иногда оптимизация одного параметра будет негативно сказываться на другом, но иногда они будут дополнять друг друга. Кроме того, работа, выполняемая процессом базы данных, может иметь не ту же стоимость (для вас), что и та же работа, выполняемая в вашем процессе Python. Вам решать, каковы ваши приоритеты, где должен быть баланс, и профилировать всё это по мере необходимости, так как это будет зависеть от вашего приложения и сервера.
В каждом случае, после внесения изменений, помните о необходимости повторного профилирования для проверки того, что изменение приносит пользу и достаточно ли эта польза, учитывая ухудшение читаемости вашего кода. Все приведенные ниже рекомендации имеют оговорку о том, что в ваших условиях общий принцип может быть неприменим или даже обратным.
Использование стандартных методов оптимизации БД
…включая:
-
Индексы. Это приоритет номер один, после того, как вы определили на основе профилирования, какие индексы следует добавить. Используйте
Field.db_indexилиMeta.index_togetherдля добавления их из Django. Подумайте о добавлении индексов к полям, которые вы часто используете в запросах с помощьюfilter(),exclude(),order_by()и т. д., так как индексы могут помочь ускорить поиск. Обратите внимание, что определение оптимальных индексов — это сложная, зависящая от базы данных тема, которая будет зависеть от вашего конкретного приложения. Накладные расходы на обслуживание индекса могут перевесить любые преимущества в скорости запроса.
- Правильное использование типов полей.
Мы предполагаем, что вы выполнили очевидные действия, описанные выше. Остальная часть данного документа фокусируется на том, как использовать Django таким образом, чтобы вы не выполняли ненужную работу. В данном документе также не рассматриваются другие методы оптимизации, которые применимы ко всем дорогостоящим операциям, таким как кеширование общего назначения.
Понимание наборов запросов
Понимание наборов запросов имеет решающее значение для достижения хорошей производительности с помощью простого кода. В частности:
Понимание оценки наборов запросов
Для предотвращения проблем с производительностью важно понимать:
- что наборы запросов ленивы.
- когда они оцениваются.
- как данные хранятся в памяти.
Понимание кэшированных атрибутов
Помимо кэширования всего QuerySet, существует кэширование результатов атрибутов объектов ORM. В общем случае, атрибуты, которые не являются вызываемыми, будут кэшированы. Например, предположим, что в примере модели Weblog:
>>> entry = Entry.objects.get(id=1) >>> entry.blog # Blog object is retrieved at this point >>> entry.blog # cached version, no DB access
Но в общем случае вызываемые атрибуты каждый раз вызывают поиск в БД:
>>> entry = Entry.objects.get(id=1) >>> entry.authors.all() # query performed >>> entry.authors.all() # query performed again
Будьте осторожны при чтении кода шаблонов — система шаблонов не допускает использование скобок, но автоматически вызывает вызываемые элементы, скрывая это различие.
Будьте внимательны с собственными пользовательскими свойствами — вам нужно самостоятельно реализовывать кэширование по мере необходимости, например, используя декоратор cached_property.
Используйте тег шаблона with
Для использования поведения кэширования QuerySet, вам может потребоваться использовать тег шаблона with.
Используйте iterator()
Когда у вас много объектов, поведение кэширования QuerySet может привести к использованию большого объема памяти. В этом случае iterator() может помочь.
Выполняйте работу с базой данных в базе данных, а не в Python
Например:
- На самом базовом уровне используйте filter и exclude для фильтрации в базе данных.
- Используйте
F expressionsдля фильтрации на основе других полей в той же модели. - Используйте annotate для агрегации в базе данных.
Если этого недостаточно для генерации необходимого SQL:
Используйте RawSQL
Менее переносимый, но более мощный метод — это выражение RawSQL, которое позволяет явно добавлять некоторый SQL в запрос. Если этого по-прежнему недостаточно:
Использование прямого SQL
Напишите свой собственный собственный SQL для получения данных или заполнения моделей. Используйте django.db.connection.queries для выяснения того, что Django пишет за вас, и начните с этого.
Получение отдельных объектов с помощью уникального индексированного столбца
Существует две причины использовать столбец с unique или db_index при использовании get() для извлечения отдельных объектов. Во-первых, запрос будет быстрее из-за индекса базы данных. Во-вторых, запрос может выполняться значительно медленнее, если несколько объектов соответствуют поиску; наличие уникального ограничения на столбец гарантирует, что этого никогда не произойдет.
Итак, используя примере модели Weblog:
>>> entry = Entry.objects.get(id=10)
будет быстрее, чем:
>>> entry = Entry.objects.get(headline="News Item Title")
потому что id индексируется базой данных и гарантированно является уникальным.
Выполнение следующего может быть довольно медленным:
>>> entry = Entry.objects.get(headline__startswith="News")
Во-первых, headline не индексируется, что замедлит извлечение данных в базе данных.
Во-вторых, поиск не гарантирует, что будет возвращен только один объект. Если запрос соответствует нескольким объектам, он извлечет и передаст все из них из базы данных. Эта затрата может быть существенной, если возвращается сотни или тысячи записей. Затраты усугублятся, если база данных расположена на отдельном сервере, где также играют роль сетевая нагрузка и задержка.
Извлекайте все сразу, если вам это потребуется
Обращение к базе данных несколько раз для разных частей одного набора данных, которые вам нужны все, как правило, менее эффективно, чем извлечение всего в одном запросе. Это особенно важно, если у вас запрос, который выполняется в цикле, и поэтому в итоге может выполняться много запросов к базе данных, когда нужен только один. Итак:
Используйте QuerySet.select_related() и prefetch_related()
Тщательно изучите select_related() и prefetch_related() и используйте их:
- в коде представления,
- и в менеджерах и менеджерах по умолчанию при необходимости. Будьте внимательны, когда используется ваш менеджер, а когда нет; иногда это сложно, поэтому не делайте предположений.
Не извлекайте то, что вам не нужно
Используйте QuerySet.values() и values_list()
Когда вам нужны только dict или list значений, и вам не нужны объекты модели ORM, используйте values(). Они могут быть полезны для замены объектов модели в коде шаблонов — если предоставленные вами словари содержат те же атрибуты, что и те, которые используются в шаблоне, то все в порядке.
Используйте QuerySet.defer() и only()
Используйте defer() и only(), если есть столбцы базы данных, которые вам не нужны (или, как правило, не нужны), чтобы избежать их загрузки. Обратите внимание, что если вы используете их, ORM придётся получить их в отдельном запросе, что является ухудшением производительности при неправильном применении.
Также имейте в виду, что при построении модели с отложенными полями в Django возникает определённая (небольшая дополнительная) нагрузка. Не стоит слишком активно откладывать поля без профилирования, так как база данных должна считывать большую часть данных, не являющихся текстом и не являющихся VARCHAR, с диска для одной строки результатов, даже если в итоге используются только несколько столбцов. Методы defer() и only() наиболее полезны, когда можно избежать загрузки большого количества текстовых данных или для полей, преобразование которых в Python может потребовать значительных вычислений. Как всегда, сначала профилируйте, затем оптимизируйте.
Использование QuerySet.count()
…если вам нужен только счёт, а не len(queryset).
Использование QuerySet.exists()
…если вам нужно только выяснить, существует ли хотя бы один результат, а не if
queryset.
Но:
Не злоупотребляйте count() и exists()
Если вам понадобятся другие данные из QuerySet, просто вычислите их.
Например, предположим модель Email, у которой есть атрибут body и многозначное отношение к User. Следующий фрагмент шаблона является оптимальным:
{% if display_inbox %}
{% with emails=user.emails.all %}
{% if emails %}
<p>You have {{ emails|length }} email(s)</p>
{% for email in emails %}
<p>{{ email.body }}</p>
{% endfor %}
{% else %}
<p>No messages today.</p>
{% endif %}
{% endwith %}
{% endif %}
Он оптимален, потому что:
- Так как QuerySet ленивы, это не вызывает запросов к базе данных, если ‘display_inbox’ равен False.
- Использование
withозначает, что мы сохраняемuser.emails.allв переменной для последующего использования, что позволяет повторно использовать кэш. - Строка
{% if emails %}вызываетQuerySet.__bool__(), что вызывает запросuser.emails.all()к базе данных, и, по крайней мере, первая строка превращается в объект ORM. Если результатов нет, возвращается False, в противном случае True. - Использование
{{ emails|length }}вызываетQuerySet.__len__(), заполняя остальную часть кэша без дополнительного запроса. - Цикл
forперебирает уже заполненный кэш.
В итоге этот код выполняет один или ноль запросов к базе данных. Единственная преднамеренная оптимизация — использование тега with. Использование QuerySet.exists() или QuerySet.count() в любой момент приведёт к дополнительным запросам.
Использование QuerySet.update() и delete()
Вместо извлечения множества объектов, изменения значений и сохранения их по отдельности, используйте оператор массового обновления SQL через QuerySet.update(). Аналогично, используйте массовые удаления по возможности.
Однако эти методы массового обновления не могут вызывать методы save() или delete() отдельных экземпляров, что означает, что любая дополнительная функциональность, которую вы добавили для этих методов, не будет выполняться, включая всё, что управляется обычными сигналами базы данных сигналами.
Использование значений внешних ключей напрямую
Если вам нужен только внешний ключ, используйте значение внешнего ключа, которое уже есть в объекте, а не получайте весь связанный объект и не получайте его первичный ключ. То есть делайте так:
entry.blog_id
вместо:
entry.blog.id
Не сортируйте результаты, если вам это не нужно
Сортировка не бесплатна; каждый столбец для сортировки — это операция, которую должна выполнить база данных. Если у модели есть стандартная сортировка (Meta.ordering), и вам она не нужна, удалите её в QuerySet, вызвав order_by() без параметров.
Добавление индекса в вашу базу данных может помочь улучшить производительность сортировки.
Массовое добавление
При создании объектов, по возможности, используйте метод bulk_create() для уменьшения количества запросов SQL. Например:
Entry.objects.bulk_create([
Entry(headline="Python 3.0 Released"),
Entry(headline="Python 3.1 Planned")
])
…лучше, чем:
Entry.objects.create(headline="Python 3.0 Released") Entry.objects.create(headline="Python 3.1 Planned")
Обратите внимание, что существует ряд caveats to this method, поэтому убедитесь, что это подходит для вашего случая.
Это также относится к ManyToManyFields, поэтому выполнение:
my_band.members.add(me, my_friend)
…лучше, чем:
my_band.members.add(me) my_band.members.add(my_friend)
…где Bands и Artists имеют многозначное отношение.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/1.8/topics/db/optimization/