Оптимизация доступа к базе данных
В Django слой работы с базой данных предоставляет различные способы, которые помогут разработчикам извлечь максимальную пользу из своих баз данных. Этот документ собирает ссылки на соответствующую документацию и добавляет различные советы, организованные по разделам, которые описывают шаги, которые необходимо предпринять при попытке оптимизации использования вашей базы данных.
Профилирование в первую очередь
В качестве общей практики программирования это само собой разумеется. Выясните, какие запросы вы выполняете и сколько они стоят. Используйте QuerySet.explain(), чтобы понять, как конкретные QuerySet запросы выполняются вашей базой данных. Возможно, вам также захочется использовать сторонний проект, такой как django-debug-toolbar, или инструмент, который отслеживает вашу базу данных напрямую.
Помните, что вы можете оптимизировать для скорости или памяти, или для того и другого, в зависимости от ваших требований. Иногда оптимизация для одного будет вредна для другого, но иногда они будут помогать друг другу. Кроме того, работа, выполняемая процессом базы данных, может иметь не ту же стоимость (для вас), что и аналогичный объем работы, выполненный в вашем процессе Python. Вам нужно будет решить, каковы ваши приоритеты, где должен лежать баланс, и профилировать всё это, как требуется, так как это будет зависеть от вашего приложения и сервера.
В связи со всем, что следует ниже, помните о том, чтобы профилировать после каждого изменения, чтобы убедиться, что это изменение приносит пользу и достаточно значимую пользу, учитывая ухудшение читабельности вашего кода. Все приведенные ниже предложения имеют оговорку о том, что в вашем случае общий принцип может не применяться или даже быть обратным.
Используйте стандартные методы оптимизации БД
…включая:
-
Индексы. Это приоритет номер один, после того, как вы определили с помощью профилирования, какие индексы следует добавить. Используйте
Field.db_indexилиMeta.index_togetherдля добавления этих индексов из Django. Подумайте о добавлении индексов к полям, которые вы часто используете с помощьюfilter(),exclude(),order_by()и т. д., так как индексы могут помочь ускорить поиск. Обратите внимание, что определение лучших индексов — это сложная тема, зависящая от базы данных, и она будет зависеть от вашего конкретного приложения. Нагрузка по обслуживанию индекса может перевесить любые преимущества в скорости запросов.
- Правильное использование типов полей.
Мы будем считать, что вы выполнили очевидные вещи выше. Остальная часть этого документа фокусируется на том, как использовать Django таким образом, чтобы вы не выполняли ненужной работы. Этот документ также не затрагивает другие методы оптимизации, которые применяются ко всем дорогим операциям, таким как кеширование общего назначения.
Понимание QuerySet
Понимание наборов запросов имеет важное значение для получения хорошей производительности с простым кодом. В частности:
Понимание оценки QuerySet
Чтобы избежать проблем с производительностью, важно понимать:
- что наборы запросов ленивые.
- когда они оцениваются.
- как данные хранятся в памяти.
Понимание кэшированных атрибутов
Помимо кеширования всего 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() может помочь.
Использование explain()
QuerySet.explain() предоставляет подробную информацию о том, как база данных выполняет запрос, включая используемые индексы и соединения. Эта информация может помочь вам найти запросы, которые можно переписать более эффективно, или идентифицировать индексы, которые можно добавить для улучшения производительности.
Выполняйте работу с базой данных в базе данных, а не в 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()
- в менеджерах и менеджерах по умолчанию по возможности. Следите за тем, когда ваш менеджер используется, а когда нет; иногда это сложно, поэтому не делайте предположений.
- в коде представления или других слоях, возможно, используя
prefetch_related_objects()по мере необходимости.
Не запрашивайте то, что вам не нужно
Используйте 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()
Если вам понадобятся другие данные из набора запросов, просто оцените его.
Например, предположим, что у модели 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 %}
Он оптимален, потому что:
- Поскольку наборы запросов ленивые, это не приводит к запросам к базе данных, если «отобразить_входящие» — 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='This is a test'),
Entry(headline='This is only a test'),
])
…предпочтительнее:
Entry.objects.create(headline='This is a test') Entry.objects.create(headline='This is only a test')
Обратите внимание, что существует ряд 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/2.1/topics/db/optimization/