Spec-Zone.ru › Django 5.1

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

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

Профилирование в первую очередь

В качестве общей практики программирования это само собой разумеется. Узнайте, какие запросы вы выполняете и сколько они вам стоят. Используйте QuerySet.explain(), чтобы понять, как ваши запросы выполняются в базе данных. Возможно, вам также потребуется использовать внешний проект, например django-debug-toolbar, или инструмент, который непосредственно отслеживает вашу базу данных.

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

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

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

…включая:

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

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

Понимание QuerySet

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

Понимание оценки QuerySet

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

  • что QuerySets ленивы.
  • когда они вычисляются.
  • как данные хранятся в памяти.

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

Помимо кэширования всего QuerySet, существует кэширование результатов атрибутов объектов ORM. В общем случае, атрибуты, которые не являются вызываемыми, будут кэшироваться. Например, предполагая пример модели блога:

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

Например:

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

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

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

…если вам нужно только узнать, есть ли obj в наборе результатов, а не if obj in queryset.

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

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

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

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

Но:

Не злоупотребляйте contains(), count(), и exists()

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

Например, предположим модель Group, которая имеет отношение "многие ко многим" к User, оптимальный код выглядит так:

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. Так как наборы результатов ленивые, это не выполняет запросы к базе данных, если display_group_members — False.
  2. Сохранение group.members.all() в переменной members позволяет повторно использовать её кэш результатов.
  3. Строка if members: вызывает QuerySet.__bool__(), что вызывает запрос к базе данных group.members.all(). Если результатов нет, он вернёт False, в противном случае — True.
  4. Строка if current_user in members: проверяет, есть ли пользователь в кэше результатов, поэтому дополнительные запросы к базе данных не выполняются.
  5. Использование len(members) вызывает QuerySet.__len__(), повторно используя кэш результатов, поэтому снова не выполняются запросы к базе данных.
  6. Цикл for member проходит по кэшу результатов.

В итоге, этот код выполняет один или ноль запросов к базе данных. Единственная преднамеренная оптимизация — использование переменной members.

Использование QuerySet.exists() для if, QuerySet.contains() для in или QuerySet.count() для подсчёта приведёт к дополнительным запросам.

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

Вместо извлечения множества объектов, изменения значений и их сохранения по отдельности, используйте объёмную операцию UPDATE в SQL с помощью QuerySet.update(). Аналогично, используйте массовые удаления по возможности.

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

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

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

entry.blog_id

вместо:

entry.blog.id

Не сортируйте результаты, если вам это не нужно

Сортировка не бесплатна; каждое поле для сортировки — операция, которую должна выполнить база данных. Если у модели есть стандартная сортировка (Meta.ordering), и вам она не нужна, удалите её, вызвав 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 имеют отношение "многие ко многим".

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/5.1/topics/db/optimization/

Spec-Zone.ru

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