Оптимизация доступа к базе данных
Слой базы данных 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.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_membersявляетсяFalse. - Хранение
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 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-запросов. Учитывая список или набор объектов:
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/5.2/topics/db/optimization/