Spec-Zone.ru › Django 3.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. Как правило, атрибуты, которые не являются вызываемыми, будут кэшироваться. Например, предположим пример модели 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() может помочь.

Использование explain()

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

Выполняйте работу с базой данных в базе данных, а не в Python

Например:

  • На самом базовом уровне используйте filter и exclude для фильтрации в базе данных.
  • Используйте F expressions для фильтрации на основе других полей в той же модели.
  • Используйте annotate для агрегации в базе данных.

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

Использование RawSQL

Менее портативный, но более мощный метод — это выражение RawSQL, которое позволяет явно добавить некоторое SQL в запрос. Если этого по-прежнему недостаточно:

Использование простого SQL

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

Получение отдельных объектов с помощью уникального индексированного столбца

Существует две причины использования столбца с unique или db_index при использовании get() для получения отдельных объектов. Во-первых, запрос будет быстрее из-за базового индекса базы данных. Во-вторых, запрос может выполняться значительно медленнее, если несколько объектов соответствуют поиску; наличие уникального ограничения на столбце гарантирует, что этого никогда не произойдет.

Итак, использование примера модели Weblog:

>>> 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, которая имеет атрибут subject и многие-ко-многим отношение к User. Следующий код оптимален:

if display_emails:
    emails = user.emails.all()
    if emails:
        print('You have', len(emails), 'emails:')
        for email in emails:
            print(email.subject)
    else:
        print('You do not have any emails.')

Он оптимален, потому что:

  1. Поскольку QuerySets ленивые, это не производит запросов к базе данных, если display_emails является False.
  2. Хранение user.emails.all() в переменной emails позволяет повторно использовать кэш результатов.
  3. Строка if emails вызывает QuerySet.__bool__(), что вызывает запрос к базе данных user.emails.all(). Если результатов нет, она вернёт False, в противном случае True.
  4. Использование len(emails) вызывает QuerySet.__len__(), повторно используя кэш результатов.
  5. Цикл for итерируется по уже заполненному кэшу.

В целом, этот код выполняет один или ноль запросов к базе данных. Единственная преднамеренная оптимизация — использование переменной emails. Использование QuerySet.exists() для if или QuerySet.count() для подсчёта вызвало бы дополнительные запросы.

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

Вместо получения множества объектов, изменения их значений и сохранения по отдельности, используйте массивный оператор SQL UPDATE через 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, поэтому убедитесь, что это подходит для вашего случая.

Обновление массивом

При обновлении объектов, где возможно, используйте метод bulk_update(), чтобы уменьшить количество запросов SQL. Учитывая список или queryset объектов:

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

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/3.2/topics/db/optimization/

Spec-Zone.ru

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