Оптимизация доступа к базе данных
Слои базы данных 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.")
Он оптимален, потому что:
- Поскольку 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() для счётчика каждый раз вызовет дополнительные запросы.
Используйте 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.1/topics/db/optimization/