Оптимизация доступа к базе данных
Слой базы данных Django предоставляет разработчикам различные способы эффективнее использовать базы данных. В этом документе собраны ссылки на соответствующую документацию и приведены различные советы, сгруппированные по разделам, описывающим шаги, которые следует предпринять при оптимизации использования базы данных.
Сначала профилируйте
В рамках общей практики программирования это само собой разумеется. Выясните, какие запросы вы выполняете и во что они вам обходятся. Используйте QuerySet.explain(), чтобы понять, как ваша база данных выполняет определённые QuerySets. Возможно, вам также пригодится внешний проект, например django-debug-toolbar, или инструмент, который отслеживает работу непосредственно вашей базы данных.
Помните, что в зависимости от ваших требований вы можете оптимизировать скорость, использование памяти или и то и другое. Иногда оптимизация одного показателя ухудшает другой, но порой они взаимно улучшаются. Кроме того, работа, выполняемая процессом базы данных, может обходиться вам не так дорого, как такой же объём работы, выполняемый процессом Python. Вам решать, что для вас важнее, где найти баланс и что именно профилировать, поскольку это зависит от вашего приложения и сервера.
При выполнении всех последующих рекомендаций не забывайте профилировать приложение после каждого изменения, чтобы убедиться, что оно приносит пользу и что эта польза достаточно велика, чтобы оправдать снижение читаемости кода. Все приведённые ниже рекомендации сопровождаются оговоркой: в ваших обстоятельствах общий принцип может не работать или даже иметь обратный эффект.
Используйте стандартные методы оптимизации БД
…в том числе:
-
Индексы. Это приоритетная задача, после того как вы с помощью профилирования определили, какие индексы следует добавить. Для добавления индексов в Django используйте
Meta.indexesилиField.db_index. Рассмотрите возможность добавления индексов к полям, по которым вы часто выполняете запросы с помощьюfilter(),exclude(),order_by()и т. д., поскольку индексы могут ускорить поиск. Помните, что выбор оптимальных индексов — сложная тема, зависящая от конкретной базы данных и вашего приложения. Затраты на поддержку индекса могут перевесить выигрыш в скорости запросов.
- Правильный выбор типов полей.
Будем считать, что всё перечисленное выше уже сделано. В остальной части документа рассматривается, как использовать Django, избегая ненужной работы. Здесь также не рассматриваются другие методы оптимизации, применимые ко всем ресурсоёмким операциям, например кэширование общего назначения.
Понимание QuerySet
Понимание QuerySet крайне важно для достижения высокой производительности при простом коде. В частности:
Понимание вычисления QuerySet
Чтобы избежать проблем с производительностью, важно понимать:
- что QuerySet вычисляются отложенно.
- когда они вычисляются.
- как данные хранятся в памяти.
Понимание кэшируемых атрибутов
Помимо кэширования всего 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
Например:
- На самом базовом уровне используйте filter и exclude, чтобы выполнять фильтрацию в базе данных.
- Используйте
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()
Если вам понадобятся и другие данные из 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.")
Он оптимален по следующим причинам:
- Поскольку QuerySet вычисляются отложенно, запросы к базе данных не выполняются, если
display_group_membersравноFalse. - Сохранение
group.members.all()в переменнойmembersпозволяет повторно использовать кэш результатов. - Строка
if members:вызываетQuerySet.__bool__(), что приводит к выполнению запросаgroup.members.all()в базе данных. Если результатов нет, будет возвращеноFalse, иначе —True. - Строка
if current_user in members:проверяет, есть ли пользователь в кэше результатов, поэтому дополнительные запросы к базе данных не выполняются. - Вызов
QuerySet.__len__()с помощьюlen(members)повторно использует кэш результатов, поэтому запросы к базе данных снова не выполняются. - Цикл
for memberперебирает кэш результатов.
В целом этот код выполняет один или ноль запросов к базе данных. Единственная намеренная оптимизация — использование переменной members. Использование QuerySet.exists() для if, QuerySet.contains() для in или QuerySet.count() для подсчёта привело бы к дополнительным запросам.
Используйте QuerySet.update() и delete()
Вместо того чтобы получать множество объектов, задавать им значения и сохранять каждый по отдельности, выполните массовый SQL-оператор UPDATE с помощью QuerySet.update(). Аналогично, по возможности выполняйте массовое удаление.
Однако учтите, что эти методы массового обновления не вызывают методы 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)
…где Band и Artist — модели со связью «многие ко многим».
При добавлении разных пар объектов в 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)
…где Band и Artist — модели со связью «многие ко многим».
При удалении разных пар объектов из 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/6.0/topics/db/optimization/