Spec-Zone.ru › Django 2.2

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

Слои базы данных 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

Например:

  • На самом базовом уровне используйте 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.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-обновление через 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-запросов. Учитывая список или набор объектов:

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.save()
entries[1].headline = 'This is no longer a test'
entries.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 имеют отношение многие-ко-многим.

END_OF_DOCUMENT_MARKER

При удалении различных пар объектов из 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/2.2/topics/db/optimization/

Spec-Zone.ru

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