Spec-Zone.ru › Django 5.0

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

Слой базы данных 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. В целом, атрибуты, которые не являются вызываемыми, будут кэшироваться. Например, предполагая пример модели блога:

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

Используйте 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. Поскольку QuerySets ленивы, это не выполняет запросов к базе данных, если 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()

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

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

Spec-Zone.ru

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