Spec-Zone.ru › Django 3.0

Оптимизация доступа к базе данных

Слой базы данных 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. В целом, атрибуты, которые не являются вызываемыми, будут кэшироваться. Например, предполагая примерные модели Weblog:

>>> 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() для получения отдельных объектов. Во-первых, запрос будет быстрее благодаря базовому индексу базы данных. Во-вторых, запрос может выполняться намного медленнее, если несколько объектов соответствуют поиску; наличие уникального ограничения в столбце гарантирует, что этого никогда не произойдёт.

Итак, используя примерные модели Weblog:

>>> 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()

Понимайте 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.count()

…если вам нужен только счёт, а не len(queryset).

Используйте QuerySet.exists()

…если вы хотите только узнать, существует ли хотя бы один результат, а не if queryset.

Но:

Не злоупотребляйте count() и exists()

Если вам понадобятся другие данные из QuerySet, оцените его немедленно.

Например, предполагая модель Email, которая имеет атрибут body и многие-ко-многим отношения с User, следующий код шаблона оптимален:

{% if display_inbox %}
  {% with emails=user.emails.all %}
    {% if emails %}
      <p>You have {{ emails|length }} email(s)</p>
      {% for email in emails %}
        <p>{{ email.body }}</p>
      {% endfor %}
    {% else %}
      <p>No messages today.</p>
    {% endif %}
  {% endwith %}
{% endif %}

Он оптимален, потому что:

  1. Поскольку QuerySet ленивы, это не вызывает запросов к базе данных, если ‘display_inbox’ равен False.
  2. Использование with означает, что мы сохраняем user.emails.all в переменную для последующего использования, позволяя повторно использовать её кеш.
  3. Строка {% if emails %} вызывает QuerySet.__bool__(), что вызывает запрос к базе данных user.emails.all(), и как минимум первая строка преобразуется в объект ORM. Если результатов нет, будет возвращено False, в противном случае True.
  4. Использование {{ emails|length }} вызывает QuerySet.__len__(), заполняя остальную часть кеша без выполнения другого запроса.
  5. Цикл for перебирает уже заполненный кеш.

В итоге этот код выполняет один или ноль запросов к базе данных. Единственная преднамеренная оптимизация — использование тега with. Использование QuerySet.exists() или QuerySet.count() в любом месте приведёт к дополнительным запросам.

Используйте QuerySet.update() и delete()

Вместо извлечения большого количества объектов, изменения значений и сохранения их по отдельности, используйте оператор SQL UPDATE в виде пакетного 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, поэтому убедитесь, что это подходит для вашего случая.

Обновлять в пакете

Новое в Django 2.2.

При обновлении объектов, где это возможно, используйте метод bulk_update() для уменьшения количества запросов SQL. Учитывая список или queryset объектов:

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/3.0/topics/db/optimization/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API