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

Уровень баз данных Django предоставляет различные способы помочь разработчикам получить максимальную отдачу от своих баз данных. В этом документе собраны ссылки на соответствующую документацию и добавлены различные советы, организованные под несколькими заголовками, которые описывают шаги, которые следует предпринять при попытке оптимизировать использование базы данных.

Профиль первый

Как общая практика программирования, это само собой разумеется. Выясните what queries you are doing and what they are costing you. Используйте QuerySet.explain(), чтобы понять, как конкретные QuerySets выполняются вашей базой данных. Вы также можете использовать внешний проект, такой как django-debug-toolbar, или инструмент, который отслеживает вашу базу данных напрямую.

Помните, что вы можете оптимизировать скорость или память, или и то, и другое, в зависимости от ваших требований. Иногда оптимизация для одного будет вредить другому, но иногда они будут помогать друг другу. Кроме того, работа, выполняемая процессом базы данных, может не иметь такой же стоимости (для вас), как тот же объем работы, выполняемый в вашем процессе Python. Вы сами должны решить, каковы ваши приоритеты, где должен быть баланс, и профилировать все это по мере необходимости, поскольку это зависит от вашего приложения и сервера.

После каждого изменения не забывайте проводить профилирование, чтобы убедиться, что изменение принесло пользу, и пользу достаточно большую, учитывая снижение читабельности вашего кода. Все приведенные ниже предложения сопровождаются оговоркой, что в ваших обстоятельствах общий принцип может быть неприменим или даже обратен.

Используйте стандартные методы оптимизации БД

…в том числе:

  • Индексы. Это приоритет номер один, после того, как вы определили с помощью профилирования, какие индексы должны быть добавлены. Используйте Meta.indexes или Field.db_index, чтобы добавить их из Django. Рассмотрите возможность добавления индексов к полям, которые вы часто запрашиваете, используя filter(), exclude(), order_by() и т.д., так как индексы могут помочь ускорить поиск. Обратите внимание, что определение оптимальных индексов - это сложная тема, зависящая от базы данных, которая будет зависеть от вашего конкретного приложения. Накладные расходы на поддержание индекса могут перевесить любой выигрыш в скорости выполнения запросов.
  • Правильное использование типов полей.

Мы будем считать, что вы сделали очевидные вещи, описанные выше. Остальная часть этого документа посвящена тому, как использовать Django таким образом, чтобы не делать лишней работы. В этом документе также не рассматриваются другие методы оптимизации, которые применяются ко всем дорогим операциям, таким как general purpose caching.

Понять QuerySet

Понимание QuerySets жизненно важно для достижения хорошей производительности при работе с простым кодом. В частности:

Понять QuerySet оценка

Чтобы избежать проблем с производительностью, важно понимать:

Понимание атрибутов кэширования

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

Поэтому, используя example Weblog 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 придется получить их в отдельном запросе, что делает это пессимизацией, если вы используете его не по назначению.

Не будьте слишком агрессивны в откладывании полей без профилирования, так как базе данных приходится считывать с диска большую часть нетекстовых, не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. Поскольку QuerySets ленивы, это не делает никаких запросов к базе данных, если „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, используя 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, поэтому убедитесь, что он подходит для вашего случая использования.

Обновление в массовом порядке

New in 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 имеют отношение «многие ко многим».

При удалении различных пар объектов из 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 имеют отношение «многие ко многим».

Вернуться на верх