Оптимизация доступа к базе данных
Уровень баз данных 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()
Тщательно изучите 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()
Если вам понадобятся другие данные из набора результатов, просто оцените его.
Например, предполагая модель Email, которая имеет атрибут body и многие-ко-многим отношение к пользователю, следующий шаблонный код является оптимальным:
{% 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 через 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.9/topics/db/optimization/