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

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

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

As general programming practice, this goes without saying. Find out what queries you are doing and what they are costing you. Use QuerySet.explain() to understand how specific QuerySets are executed by your database. You may also want to use an external project like django-debug-toolbar, or a tool that monitors your database directly.

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

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

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

…включая:

  • Indexes. This is a number one priority, after you have determined from profiling what indexes should be added. Use Meta.indexes or Field.db_index to add these from Django. Consider adding indexes to fields that you frequently query using filter(), exclude(), order_by(), etc. as indexes may help to speed up lookups. Note that determining the best indexes is a complex database-dependent topic that will depend on your particular application. The overhead of maintaining an index may outweigh any gains in query speed.
  • Используйте правильные типы полей.

We will assume you have done the things listed above. The rest of this document focuses on how to use Django in such a way that you are not doing unnecessary work. This document also does not address other optimization techniques that apply to all expensive operations, such as general purpose caching.

Понимание QuerySet

Понимание 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

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

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

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

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

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

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

Use explain()

QuerySet.explain() gives you detailed information about how the database executes a query, including indexes and joins that are used. These details may help you find queries that could be rewritten more efficiently, or identify indexes that could be added to improve performance.

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

Например:

Если этого не достаточно для создания необходимого 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.values() и values_list()

When you only want a dict or list of values, and don’t need ORM model objects, make appropriate usage of values(). These can be useful for replacing model objects in template code - as long as the dicts you supply have the same attributes as those used in the template, you are fine.

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

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

Don’t be too aggressive in deferring fields without profiling as the database has to read most of the non-text, non-VARCHAR data from the disk for a single row in the results, even if it ends up only using a few columns. The defer() and only() methods are most useful when you can avoid loading a lot of text data or for fields that might take a lot of processing to convert back to Python. As always, profile first, then optimize.

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

…вместо``len(queryset)``, если вам необходимо только количество объектов.

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

…если необходимо проверить есть ли результат, вместо if queryset.

Но:

Но не переусердствуйте с count() и exists()

If you are going to need other data from the QuerySet, evaluate it immediately.

Например, возьмем модель 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(). Аналогично используйте массовое удаление при возможности.

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

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

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

entry.blog_id

вместо:

entry.blog.id

Не сортируйте данные, если вам это не требуется

Сортировка требует ресурсы. Каждое поле, по которому производится сортировка, требует от базы данных дополнительных ресурсов. Если модель имеет сортировку по-умолчанию (Meta.ordering) и она вам не нужна, уберите её из запроса с помощью order_by() (без параметров).

Добавление индекса в вашу базу данных может улучшить производительность операции сортировки.

Use bulk methods

Use bulk methods to reduce the number of SQL statements.

Create in bulk

При создании объектов, если возможно, используйте метод 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')

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

Update in bulk

New in Django 2.2.

When updating objects, where possible, use the bulk_update() method to reduce the number of SQL queries. Given a list or queryset of objects:

entries = Entry.objects.bulk_create([
    Entry(headline='This is a test'),
    Entry(headline='This is only a test'),
])

The following example:

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

Note that there are a number of caveats to this method, so make sure it’s appropriate for your use case.

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

When inserting objects into ManyToManyFields, use add() with multiple objects to reduce the number of SQL queries. For example:

my_band.members.add(me, my_friend)

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

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

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

When inserting different pairs of objects into ManyToManyField or when the custom through table is defined, use bulk_create() method to reduce the number of SQL queries. For example:

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)

…where Pizza and Topping have a many-to-many relationship. Note that there are a number of caveats to this method, so make sure it’s appropriate for your use case.

Remove in bulk

When removing objects from ManyToManyFields, use remove() with multiple objects to reduce the number of SQL queries. For example:

my_band.members.remove(me, my_friend)

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

my_band.members.remove(me)
my_band.members.remove(my_friend)

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

When removing different pairs of objects from ManyToManyFields, use delete() on a Q expression with multiple through model instances to reduce the number of SQL queries. For example:

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)

…where Pizza and Topping have a many-to-many relationship.