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

Уровень баз данных 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.

Понять <<< 0 >>s

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

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

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

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

As well as caching of the whole QuerySet, there is caching of the result of attributes on ORM objects. In general, attributes that are not callable will be cached. For example, assuming the example blog 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() для получения отдельных объектов. Во-первых, запрос будет выполняться быстрее благодаря индексу базы данных. Кроме того, запрос может выполняться намного медленнее, если несколько объектов соответствуют запросу; наличие уникального ограничения на столбец гарантирует, что этого никогда не произойдет.

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

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.

Use QuerySet.contains(obj)

…if you only want to find out if obj is in the queryset, rather than if obj in queryset.

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

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

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

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

Но:

Don’t overuse contains(), count(), and exists()

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

For example, assuming a Group model that has a many-to-many relation to User, the following code is optimal:

members = group.members.all()

if display_group_members:
    if members:
        if current_user in members:
            print("You and", len(members) - 1, "other users are members of this group.")
        else:
            print("There are", len(members), "members in this group.")

        for member in members:
            print(member.username)
    else:
        print("There are no members in this group.")

Это оптимальный вариант:

  1. Since QuerySets are lazy, this does no database queries if display_group_members is False.
  2. Storing group.members.all() in the members variable allows its result cache to be reused.
  3. The line if members: causes QuerySet.__bool__() to be called, which causes the group.members.all() query to be run on the database. If there aren’t any results, it will return False, otherwise True.
  4. Строка if current_user in members: проверяет, находится ли пользователь в кэше результатов, поэтому дополнительные запросы к базе данных не выполняются.
  5. The use of len(members) calls QuerySet.__len__(), reusing the result cache, so again, no database queries are issued.
  6. The for member loop iterates over the result cache.

In total, this code does either one or zero database queries. The only deliberate optimization performed is using the members variable. Using QuerySet.exists() for the if, QuerySet.contains() for the in, or QuerySet.count() for the count would each cause additional queries.

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

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

При обновлении объектов, где это возможно, используйте метод 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[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 имеют отношение «многие ко многим».

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