Оптимизация работы с базой данных

Django’s database layer provides various ways to help developers get the most out of their databases. This document gathers together links to the relevant documentation, and adds various tips, organized under a number of headings that outline the steps to take when attempting to optimize your database usage.

Первым делом - профайлинг

Для практикующих программистов - это само собой разумеется. Первым делом определите какие запросы выполняются и как быстро. Вы так же можете использовать сторонние приложения, например, django-debug-toolbar, или инструменты, которые мониторят непосредственно базу данных.

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

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

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

...включая:

  • Индексы. Используйте их в первую очередь, после того, как определите через профайлинг какие индексы необходимо добавить. Используйте django.db.models.Field.db_index что бы добавить их через Django.

  • Используйте правильные типы полей.

Предположим вы уже выполнили эти очевидные вещи. Остальная часть этого раздела расскажет как оптимально использовать Django. Этот документ не описывает другие методы оптимизации, которые применимы для всех “тяжелых” операций, например кэширование.

Понимание QuerySets

Понимание QuerySets - важная часть для написания эффективного простого кода. В частности:

Понимание выполнения QuerySet

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

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

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

Будьте внимательны читая код шаблонов - шаблонизатор не позволяет использовать скобки и автоматом вызывает функции и методы.

Будьте внимательны с собственными свойствами - вы должны самостоятельно реализовать кэширование.

Используйте шаблонный тэг with

Для использования кэширования в QuerySet можно использовать шаблонный тэг with.

Используйте iterator()

Если у вас очень много объектов, кэширование в QuerySet может использовать большой объем памяти. В этом случае может помочь iterator().

Выполняйте задачи базы данных в базе данных, а не в Python

Например:

Если этого не достаточно для создания необходимого SQL:

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

Не совсем portable, но очень мощный метод extra(), который позволяет добавить SQL непосредственно в запрос. Если и этого вам не достаточно:

Используйте SQL

Используйте собственный SQL запрос для получечния данных и загрузки в модели. Используйте django.db.connection.queries, что бы понять что создает Django и начните с изменения этого запроса.

Загружайте все данные сразу, если уверены, что будете использовать их.

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

Не получайте данные, которые вам не нужны

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

Если вам нужен dict или list значений, а не объекты моделей ORM, используйте values(). Это можно использовать для подмены объектов моделей в шаблоне - если атрибуты словаря совпадают с используемыми атрибутами моделей, все будет хорошо работать.

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

Используйте defer() и only(), если есть колонки в базе данных, которые вы не будете использовать. Запомните, что если вы все же будете их использовать, ORM сделает дополнительный запрос для их получения, что уменьшит производительность.

Заметим, что при создании моделей с “deferred” полями Django выполняет некоторую дополнительную работу. Не перестарайтесь, так как база данных читайте большинство не текстовых, не-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.__nonzero__(), который выполняет``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(). Аналогично используйте массовое удаление при возможности.

Однако учтите, что эти методы не вызывают save() или delete() объектов. Это означает, что логика добавленная вами в эти методы, не будет выполнена, учитывая обработчики сигналов от объектов.

Используйте значения ключей непосредственно

Если вам необходимо только значение внешнего ключа, используйте его т.к. оно уже в объекте, который вы получили, вместо получения всего связанного объекта и использования первичного ключа. Например, делайте:

entry.blog_id

вместо:

entry.blog.id

Используйте общее добавление

При создании объектов, если возможно, используйте метод bulk_create() что бы сократить количество SQL запросов. Например:

Entry.objects.bulk_create([
    Entry(headline="Python 3.0 Released"),
    Entry(headline="Python 3.1 Planned")
])

...предпочтительнее чем:

Entry.objects.create(headline="Python 3.0 Released")
Entry.objects.create(headline="Python 3.1 Planned")

Заметим, что есть несколько предостережений к этому методу, убедитесь что этот метод подходит для всего случая.

Это так же относится к ManyToManyFields, это:

my_band.members.add(me, my_friend)

...предпочтительнее чем:

my_band.members.add(me)
my_band.members.add(my_friend)

...где Bands и Artists связаны через многое-ко-многому.