Оптимизация доступа к базе данных
В Django слой работы с базой данных предоставляет различные способы помочь разработчикам извлечь максимум из своих баз данных. Этот документ собирает ссылки на соответствующую документацию и добавляет различные советы, организованные по темам, которые описывают шаги, которые необходимо предпринять при попытке оптимизировать использование вашей базы данных.
Профилирование в первую очередь
В качестве общей практики программирования это само собой разумеется. Узнайте, какие запросы вы выполняете и сколько они стоят. Используйте QuerySet.explain(), чтобы понять, как конкретные QuerySet выполняются вашей базой данных. Вы также можете использовать внешний проект, такой как django-debug-toolbar, или инструмент, который напрямую отслеживает вашу базу данных.
Помните, что вы можете оптимизировать скорость, память или оба параметра в зависимости от ваших потребностей. Иногда оптимизация одного параметра ухудшит другой, но иногда они помогут друг другу. Кроме того, работа, выполняемая процессом базы данных, может иметь не ту же стоимость (для вас), что и та же работа, выполненная в вашем процессе Python. Вам необходимо определить ваши приоритеты, где должен лежать баланс, и профилировать все это по мере необходимости, так как это будет зависеть от вашего приложения и сервера.
В связи со всем, что следует, помните, что необходимо выполнять профилирование после каждого изменения, чтобы убедиться, что это изменение приносит пользу, а также достаточную пользу с учетом снижения читабельности вашего кода. Все приведенные ниже рекомендации подразумевают, что в вашей ситуации общее правило может не применяться или даже быть обратным.
Использование стандартных методов оптимизации БД
…включая:
-
Индексы. Это приоритет номер один после того, как вы определили из профилирования, какие индексы следует добавить. Используйте
Meta.indexesилиField.db_indexдля добавления их из Django. Рассмотрите возможность добавления индексов к полям, которые вы часто используете с помощьюfilter(),exclude(),order_by()и т. д., так как индексы могут помочь ускорить поиск. Обратите внимание, что определение лучших индексов — это сложная тема, зависящая от базы данных, которая будет зависеть от вашего конкретного приложения. Накладные расходы на поддержание индекса могут превысить любые выгоды в скорости запросов.
- Правильное использование типов полей.
Мы будем исходить из того, что вы выполнили вышеперечисленные действия. Остальная часть этого документа фокусируется на том, как использовать Django таким образом, чтобы вы не выполняли ненужную работу. В этом документе также не рассматриваются другие методы оптимизации, применимые ко всем дорогостоящим операциям, такие как кеширование общего назначения.
Понимание QuerySet
Понимание QuerySets имеет решающее значение для достижения хорошей производительности с простым кодом. В частности:
Понимание оценки QuerySet
Чтобы избежать проблем с производительностью, важно понять:
- что QuerySets ленивы.
- когда они оцениваются.
- как данные хранятся в памяти.
Понимание кэшированных атрибутов
Помимо кэширования всего QuerySet, существует кэширование результатов атрибутов объектов ORM. В общем случае атрибуты, которые не являются вызываемыми, будут кэшироваться. Например, предположим, что пример модели блога:
>>> 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() для получения отдельных объектов. Во-первых, запрос будет быстрее из-за подлежащего индекса базы данных. Кроме того, запрос может выполняться намного медленнее, если несколько объектов соответствуют запросу; наличие уникального ограничения в столбце гарантирует, что этого никогда не произойдёт.
Итак, используя пример модели блога:
>>> 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.contains(obj)
…если вы хотите только узнать, есть ли obj в наборе запросов, а не if obj in queryset.
Используйте QuerySet.count()
…если вам нужен только подсчёт, а не len(queryset).
Используйте QuerySet.exists()
…если вы хотите только узнать, существует ли хотя бы один результат, а не if
queryset.
Но:
Не злоупотребляйте contains(), count(), и exists()
Если вам понадобятся другие данные из набора запросов, оцените их немедленно.
Например, предположим модель Group, которая имеет много-ко-многие отношения с User, следующий код является оптимальным:
members = group.members.all()
if display_group_members:
if members:
if current_user in members:
print("You and", len(members) - 1, "other users are members of this group.")
else:
print("There are", len(members), "members in this group.")
for member in members:
print(member.username)
else:
print("There are no members in this group.")
Он оптимален, потому что:
- Так как наборы запросов ленивы, это не создаёт запросов к базе данных, если
display_group_membersFalse. - Хранение
group.members.all()в переменнойmembersпозволяет повторно использовать кэш результатов. - Строка
if members:вызываетQuerySet.__bool__(), что вызывает запрос к базе данныхgroup.members.all(). Если результатов нет, он вернётFalse, в противном случаеTrue. - Строка
if current_user in members:проверяет, находится ли пользователь в кэше результатов, поэтому дополнительные запросы к базе данных не выполняются. - Использование
len(members)вызываетQuerySet.__len__(), повторно используя кэш результатов, и снова, без запросов к базе данных. - Цикл
for memberперебирает кэш результатов.
В целом, этот код выполняет один или ноль запросов к базе данных. Единственная преднамеренная оптимизация — использование переменной members. Использование QuerySet.exists() для if, QuerySet.contains() для in или QuerySet.count() для подсчёта приведёт к дополнительным запросам.
Используйте QuerySet.update() и delete()
Вместо извлечения большого количества объектов, изменения значений и их сохранения по отдельности, используйте массовое SQL-обновление через 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-запросов. Учитывая список или набор запросов объектов:
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/4.2/topics/db/optimization/