Оптимизация доступа к базе данных¶
Уровень баз данных 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.
Понять <<< 0 >>s¶
Понимание QuerySets жизненно важно для достижения хорошей производительности при работе с простым кодом. В частности:
Понять QuerySet
оценка¶
Чтобы избежать проблем с производительностью, важно понимать:
- что QuerySets are lazy.
- когда they are evaluated.
- как the data is held in memory.
Понимание атрибутов кэширования¶
As well as caching of the whole QuerySet
, there is caching of the result of
attributes on ORM objects. In general, attributes that are not callable will be
cached. For example, assuming the 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()
для получения отдельных объектов. Во-первых, запрос будет выполняться быстрее благодаря индексу базы данных. Кроме того, запрос может выполняться намного медленнее, если несколько объектов соответствуют запросу; наличие уникального ограничения на столбец гарантирует, что этого никогда не произойдет.
So using the 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 придется получить их в отдельном запросе, что делает это пессимизацией, если вы используете его не по назначению.
Don’t be too aggressive in deferring fields without profiling as the database
has to read most of the non-text, non-VARCHAR
data from the disk for a
single row in the results, even if it ends up only using a few columns. The
defer()
and only()
methods are most useful when you can avoid loading a
lot of text data or for fields that might take a lot of processing to convert
back to Python. As always, profile first, then optimize.
Use QuerySet.contains(obj)
¶
…if you only want to find out if obj
is in the queryset, rather than
if obj in queryset
.
Используйте QuerySet.count()
¶
…если вам нужен только счетчик, а не выполнение len(queryset)
.
Используйте QuerySet.exists()
¶
…если вы хотите узнать, существует ли хотя бы один результат, а не if queryset
.
Но:
Don’t overuse contains()
, count()
, and exists()
¶
Если вам понадобятся другие данные из QuerySet, оцените их немедленно.
For example, assuming a Group
model that has a many-to-many relation to
User
, the following code is optimal:
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.")
Это оптимальный вариант:
- Since QuerySets are lazy, this does no database queries if
display_group_members
isFalse
. - Storing
group.members.all()
in themembers
variable allows its result cache to be reused. - The line
if members:
causesQuerySet.__bool__()
to be called, which causes thegroup.members.all()
query to be run on the database. If there aren’t any results, it will returnFalse
, otherwiseTrue
. - Строка
if current_user in members:
проверяет, находится ли пользователь в кэше результатов, поэтому дополнительные запросы к базе данных не выполняются. - The use of
len(members)
callsQuerySet.__len__()
, reusing the result cache, so again, no database queries are issued. - The
for member
loop iterates over the result cache.
In total, this code does either one or zero database queries. The only
deliberate optimization performed is using the members
variable. Using
QuerySet.exists()
for the if
, QuerySet.contains()
for the in
,
or QuerySet.count()
for the count would each cause additional queries.
Используйте 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
имеют отношение «многие ко многим».