Spec-Zone.ru › Django 4.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

Например:

  • На самом базовом уровне используйте filter и exclude для фильтрации в базе данных.
  • Используйте 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.

Но:

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

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

Например, предположим модель 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. Поскольку наборы запросов ленивые, это не выполнит запросов к базе данных, если 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 с помощью 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/4.0/topics/db/optimization/

Spec-Zone.ru

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