Оптимизация доступа к базе данных
Слой базы данных Django предоставляет различные способы, которые помогут разработчикам получить максимальную отдачу от своих баз данных. В этом документе собраны ссылки на соответствующую документацию, а также различные советы, организованные по разделам, которые описывают шаги, которые необходимо предпринять при попытке оптимизировать использование вашей базы данных.
Профилирование в первую очередь
Как общая практика программирования, это само собой разумеется. Узнайте, какие запросы вы выполняете и сколько они вам стоят. Используйте QuerySet.explain(), чтобы понять, как конкретные QuerySet выполняются вашей базой данных. Вы также можете использовать сторонний проект, например, django-debug-toolbar, или инструмент, который напрямую отслеживает вашу базу данных.
Помните, что вы можете оптимизировать скорость или память, или то и другое, в зависимости от ваших потребностей. Иногда оптимизация одного параметра ухудшит другой, но иногда они могут помочь друг другу. Кроме того, работа, выполняемая процессом базы данных, может иметь не ту же стоимость (для вас), что и та же работа, выполненная в вашем процессе Python. Вам нужно самостоятельно определить свои приоритеты, где должно быть равновесие, и профилировать все это по мере необходимости, поскольку это будет зависеть от вашего приложения и сервера.
В каждом случае, что следует далее, помните о профилировании после каждого изменения, чтобы убедиться, что изменение приносит пользу и достаточно большую пользу, учитывая снижение читаемости вашего кода. Все приведенные ниже рекомендации предполагают, что в вашем случае общий принцип может не применяться или даже быть обратным.
Использование стандартных методов оптимизации БД
…включая:
-
Индексы. Это приоритет номер один, после того, как вы определили с помощью профилирования, какие индексы нужно добавить. Используйте
Meta.indexesилиField.db_indexдля добавления их из 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
Например:
- На базовом уровне используйте фильтрацию и исключение для фильтрации в базе данных.
- Используйте
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 придется получить их в отдельном запросе, что является ухудшением производительности, если вы используете его неуместно.
Не стоит слишком активно откладывать поля без профилирования, так как база данных должна прочитать большую часть данных, не являющихся текстом, не 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 ленивые, при False в «display_inbox» запросы к базе данных не выполняются.
- Использование
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() без параметров.
Добавление индекса в базу данных может помочь улучшить производительность сортировки.
Используйте методы массовой обработки
Используйте методы массовой обработки для уменьшения количества SQL-запросов.
Создание в массовом режиме
При создании объектов, где это возможно, используйте метод 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, поэтому убедитесь, что это подходит для вашего случая.
Обновление в массовом режиме
При обновлении объектов, где это возможно, используйте метод bulk_update() для уменьшения количества SQL-запросов. Учитывая список или queryset объектов:
entries = Entry.objects.bulk_create([
Entry(headline='This is a test'),
Entry(headline='This is only a test'),
])
Следующий пример:
entries[0].headline = 'This is not a test' entries[1].headline = 'This is no longer a test' Entry.objects.bulk_update(entries, ['headline'])
…предпочтительнее:
entries[0].headline = 'This is not a test' entries[0].save() entries[1].headline = 'This is no longer a test' entries[1].save()
Обратите внимание, что существует ряд caveats to this method, поэтому убедитесь, что это подходит для вашего случая.
Вставка в массовом режиме
При вставке объектов в ManyToManyFields, используйте add() с несколькими объектами, чтобы уменьшить количество SQL-запросов. Например:
my_band.members.add(me, my_friend)
…предпочтительнее:
my_band.members.add(me) my_band.members.add(my_friend)
…где Bands и Artists имеют отношение многие-ко-многим.
При вставке различных пар объектов в ManyToManyField или когда определена пользовательская таблица through, используйте метод bulk_create() для уменьшения количества SQL-запросов. Например:
PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.bulk_create([
PizzaToppingRelationship(pizza=my_pizza, topping=pepperoni),
PizzaToppingRelationship(pizza=your_pizza, topping=pepperoni),
PizzaToppingRelationship(pizza=your_pizza, topping=mushroom),
], ignore_conflicts=True)
…предпочтительнее:
my_pizza.toppings.add(pepperoni) your_pizza.toppings.add(pepperoni, mushroom)
…где Pizza и Topping имеют отношение многие-ко-многим. Обратите внимание, что существует ряд caveats to this method, поэтому убедитесь, что это подходит для вашего случая.
Удаление в массовом режиме
При удалении объектов из ManyToManyFields, используйте remove() с несколькими объектами, чтобы уменьшить количество SQL-запросов. Например:
my_band.members.remove(me, my_friend)
…предпочтительнее:
my_band.members.remove(me) my_band.members.remove(my_friend)
…где Bands и Artists имеют отношение многие-ко-многим.
При удалении различных пар объектов из ManyToManyFields, используйте delete() с выражением Q и несколькими экземплярами модели through, чтобы уменьшить количество SQL-запросов. Например:
from django.db.models import Q
PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.filter(
Q(pizza=my_pizza, topping=pepperoni) |
Q(pizza=your_pizza, topping=pepperoni) |
Q(pizza=your_pizza, topping=mushroom)
).delete()
…предпочтительнее, чем:
my_pizza.toppings.remove(pepperoni) your_pizza.toppings.remove(pepperoni, mushroom)
…где Pizza и Topping имеют взаимосвязь «многие ко многим».
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/3.1/topics/db/optimization/