Оптимизация доступа к базе данных¶
Уровень баз данных Django предоставляет различные способы помочь разработчикам получить максимальную отдачу от своих баз данных. В этом документе собраны ссылки на соответствующую документацию и добавлены различные советы, организованные под несколькими заголовками, которые описывают шаги, которые следует предпринять при попытке оптимизировать использование базы данных.
Профиль первый¶
Как общая практика программирования, это само собой разумеется. Выясните what queries you are doing and what they are costing you. Используйте QuerySet.explain()
, чтобы понять, как конкретные QuerySet
s выполняются вашей базой данных. Вы также можете использовать внешний проект, такой как django-debug-toolbar, или инструмент, который отслеживает вашу базу данных напрямую.
Помните, что вы можете оптимизировать скорость или память, или и то, и другое, в зависимости от ваших требований. Иногда оптимизация для одного будет вредить другому, но иногда они будут помогать друг другу. Кроме того, работа, выполняемая процессом базы данных, может не иметь такой же стоимости (для вас), как тот же объем работы, выполняемый в вашем процессе Python. Вы сами должны решить, каковы ваши приоритеты, где должен быть баланс, и профилировать все это по мере необходимости, поскольку это зависит от вашего приложения и сервера.
После каждого изменения не забывайте проводить профилирование, чтобы убедиться, что изменение принесло пользу, и пользу достаточно большую, учитывая снижение читабельности вашего кода. Все приведенные ниже предложения сопровождаются оговоркой, что в ваших обстоятельствах общий принцип может быть неприменим или даже обратен.
Используйте стандартные методы оптимизации БД¶
…в том числе:
- Индексы. Это приоритет номер один, после того, как вы определили с помощью профилирования, какие индексы должны быть добавлены. Используйте
Meta.indexes
илиField.db_index
, чтобы добавить их из Django. Рассмотрите возможность добавления индексов к полям, которые вы часто запрашиваете, используяfilter()
,exclude()
,order_by()
и т.д., так как индексы могут помочь ускорить поиск. Обратите внимание, что определение оптимальных индексов - это сложная тема, зависящая от базы данных, которая будет зависеть от вашего конкретного приложения. Накладные расходы на поддержание индекса могут перевесить любой выигрыш в скорости выполнения запросов.
- Правильное использование типов полей.
Мы будем считать, что вы уже сделали все, что перечислено выше. Остальная часть этого документа посвящена тому, как использовать Django таким образом, чтобы не делать лишней работы. В этом документе также не рассматриваются другие методы оптимизации, которые применяются ко всем дорогим операциям, таким как general purpose caching.
Понимание QuerySet
¶
Понимание QuerySets жизненно важно для достижения хорошей производительности при работе с простым кодом. В частности:
Понять QuerySet
оценка¶
Чтобы избежать проблем с производительностью, важно понимать:
- что QuerySets are lazy.
- когда they are evaluated.
- как the data is held in memory.
Понимание атрибутов кэширования¶
Помимо кэширования всего QuerySet
, существует кэширование результатов работы атрибутов на объектах ORM. В общем случае кэшируются атрибуты, которые не являются вызываемыми. Например, если предположить, что атрибут example blog models:
>>> 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
Будьте внимательны при чтении шаблонного кода - система шаблонов не позволяет использовать круглые скобки, но будет вызывать callables автоматически, скрывая указанное выше различие.
Будьте осторожны с вашими собственными пользовательскими свойствами - вы сами должны реализовать кэширование, когда это необходимо, например, используя декоратор cached_property
.
Используйте тег шаблона with
¶
Чтобы использовать поведение кэширования QuerySet
, вам может понадобиться использовать тег шаблона with
.
Используйте iterator()
¶
Когда у вас много объектов, поведение кэширования QuerySet
может привести к использованию большого количества памяти. В этом случае может помочь iterator()
.
Используйте explain()
¶
QuerySet.explain()
дает вам подробную информацию о том, как база данных выполняет запрос, включая используемые индексы и соединения. Эти сведения могут помочь вам найти запросы, которые можно переписать более эффективно, или определить индексы, которые можно добавить для повышения производительности.
Работайте с базой данных в базе данных, а не в Python¶
Например:
- На самом базовом уровне используйте filter and exclude для выполнения фильтрации в базе данных.
- Используйте
F expressions
для фильтрации на основе других полей в той же модели. - Используйте annotate to do aggregation in the database.
Если этого недостаточно для создания необходимого вам SQL:
Используйте RawSQL
¶
Менее переносимым, но более мощным методом является выражение RawSQL
, которое позволяет явно добавить в запрос некоторый SQL. Если этого все еще недостаточно:
Используйте необработанный SQL¶
Напишите свой собственный custom SQL to retrieve data or populate models. Используйте django.db.connection.queries
, чтобы узнать, что Django пишет за вас, и начните с этого.
Извлечение отдельных объектов с помощью уникального индексированного столбца¶
Есть две причины использовать столбец с индексом unique
или db_index
при использовании get()
для получения отдельных объектов. Во-первых, запрос будет выполняться быстрее благодаря индексу базы данных. Кроме того, запрос может выполняться намного медленнее, если несколько объектов соответствуют запросу; наличие уникального ограничения на столбец гарантирует, что этого никогда не произойдет.
Поэтому использование example blog models:
>>> 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()
. Они могут быть полезны для замены объектов модели в коде шаблона - до тех пор, пока поставляемые вами dicts имеют те же атрибуты, что и используемые в шаблоне, все в порядке.
Используйте 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()
.¶
Если вам понадобятся другие данные из QuerySet, оцените их немедленно.
Например, если взять модель 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.")
Это оптимальный вариант:
- Поскольку QuerySets являются ленивыми, это не делает запросов к базе данных, если
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()
для count привело бы к дополнительным запросам.
Используйте QuerySet.update()
и delete()
¶
Вместо того чтобы извлекать груз объектов, устанавливать некоторые значения и сохранять их по отдельности, используйте массовый оператор SQL UPDATE, используя QuerySet.update(). Аналогично, по возможности используйте bulk deletes.
Обратите внимание, однако, что эти методы массового обновления не могут вызывать методы save()
или delete()
отдельных экземпляров, что означает, что любое пользовательское поведение, добавленное вами для этих методов, не будет выполнено, включая все, что управляется из обычного объекта базы данных signals.
Используйте значения внешних ключей напрямую¶
Если вам нужно только значение внешнего ключа, используйте значение внешнего ключа, которое уже есть у имеющегося объекта, вместо того чтобы получать весь связанный объект и брать его первичный ключ. т.е. сделайте:
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
имеют отношение «многие ко многим».