Оптимизация доступа к базе данных
Уровень доступа к базе данных Django предоставляет различные способы, помогающие разработчикам извлечь максимальную пользу из своих баз данных. В данном документе собраны ссылки на соответствующую документацию, а также различные советы, организованные по разделам, описывающим шаги, которые следует предпринять при попытке оптимизировать использование базы данных.
Профиль сначала
Как общая практика программирования, это само собой разумеется. Узнайте, какие запросы вы выполняете и сколько они стоят. Вы также можете использовать внешний проект, например django-debug-toolbar, или инструмент, который отслеживает вашу базу данных напрямую.
Помните, что вы можете оптимизировать скорость или память, или оба параметра, в зависимости от ваших потребностей. Иногда оптимизация одного параметра будет вредна для другого, но иногда они будут помогать друг другу. Кроме того, работа, выполняемая процессом базы данных, может иметь не такую же стоимость (для вас), как та же работа, выполненная в вашем процессе Python. Вам нужно решить, каковы ваши приоритеты, где должен быть баланс, и профилировать всё это по мере необходимости, так как это будет зависеть от вашего приложения и сервера.
Во всех последующих случаях помните, что нужно профилировать после каждого изменения, чтобы убедиться, что это изменение приносит пользу, и достаточно большая польза с учетом уменьшения читабельности вашего кода. Все приведенные ниже рекомендации предполагают, что в вашей ситуации общий принцип может не применяться или даже быть обратным.
Использование стандартных методов оптимизации БД
…включая:
-
Индексы. Это один из приоритетов, после того, как вы определили из профилирования, какие индексы следует добавить. Используйте
Field.db_indexилиMeta.index_togetherдля добавления их из Django. Подумайте о добавлении индексов к полям, которые вы часто используете в запросах с помощьюfilter(),exclude(),order_by()и т.д., так как индексы могут помочь ускорить поиск. Обратите внимание, что определение лучших индексов — это сложная тема, зависящая от базы данных, которая будет зависеть от вашего конкретного приложения. Накладные расходы на поддержание индекса могут перевесить любые преимущества в скорости запроса.
- Правильное использование типов полей.
Мы будем предполагать, что вы выполнили очевидные действия, описанные выше. Остальная часть этого документа фокусируется на том, как использовать Django таким образом, чтобы вы не выполняли ненужную работу. Этот документ также не рассматривает другие методы оптимизации, которые применяются ко всем дорогостоящим операциям, таким как кэширование общего назначения.
Понимание QuerySet
Понимание QuerySets имеет важное значение для обеспечения хорошей производительности с помощью простого кода. В частности:
Понимание QuerySet оценки
Чтобы избежать проблем с производительностью, важно понимать:
- что QuerySets являются ленивыми.
- когда они оцениваются.
- как данные хранятся в памяти.
Понимание кэшированных атрибутов
Помимо кэширования всего 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()
- в менеджерах и менеджерах по умолчанию при необходимости. Будьте внимательны, когда ваш менеджер используется, а когда нет; иногда это сложно, поэтому не делайте предположений.
- в коде представления или других слоях, возможно, используя
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 и связь many-to-many с 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 %}
Он оптимален, потому что:
- Так как наборы запросов ленивые, это не вызывает запросов к базе данных, если «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 UPDATE в объёме с помощью 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 имеют связь many-to-many.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/2.0/topics/db/optimization/