Оптимізація продуктивності запитів Django має вирішальне значення для створення швидких веб застосунків. У документації з оптимізації доступу до бази даних Django надає багато інструментів і методів для оптимізації запитів до бази даних. У цій статті ми розглянемо колекцію додаткових і важливих порад, які я збирав протягом багатьох років, щоб допомогти вам виявити та вирішити ваші неефективні запити Django.
Вбиваємо довготривалі запити за допомогою Statement Timeout
PostgreSQL підтримує параметр statement_timeout
, який дозволяє встановити максимальний час виконання запиту. Це корисно для запобігання довготривалим запитам, які блокують дорогоцінні ресурси та сповільнюють роботу програми. Моя команда в PixieBrix зіткнулася з інцидентом, коли кілька довготривалих запитів призвели до повного відключення бази даних. Встановлення таймауту виконання запитів у налаштуваннях Django може допомогти запобігти цьому.
DATABASES = {
"default": {
...
"OPTIONS": {
"options": "-c statement_timeout=30s",
},
}
}
Тепер будь-який запит, який триває довше 30 секунд, буде перерваний.
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("select pg_sleep(31)")
# django.db.utils.OperationalError: canceling statement due to statement timeout
Кілька зауважень:
- Згідно з документацією, якщо
log_min_error_statement
має значенняERROR
(за замовчуванням), запит, який завершився, також буде записано як помилкуquery_canceled
(код 57014). Ви повинні використовувати ці журнали для виявлення повільних запитів. - PostgreSQL підтримує встановлення таймауту для всієї бази даних, але документація не рекомендує цього робити, оскільки це може спричинити проблеми з довготривалими завданнями обслуговування, такими як резервне копіювання. Замість цього рекомендується встановлювати таймаут для кожного з'єднання, як показано вище.
- MySQL, здається, підтримує подібний параметр
max_execution_time
, але я його не тестував. - На різних серверах таймаути операторів можуть відрізнятися. Наприклад, ви, ймовірно, захочете встановити більший таймаут для своїх celery воркерів, у порівнянні з веб-серверами. Ви можете зробити це, умовно встановивши таймаут оператора:
# https://stackoverflow.com/a/50843002/6611672
IN_CELERY_WORKER = sys.argv and sys.argv[0].endswith("celery") and "worker" in sys.argv
if IN_CELERY_WORKER:
STATEMENT_TIMEOUT = "1min"
else:
STATEMENT_TIMEOUT = "30s"
Контроль кількості запитів в модульних тестах за допомогою assertNumQueries
При написанні юніт-тестів важливо переконатися, що ваш код робить очікувану кількість запитів. Django надає зручний метод assertNumQueries
, який дозволяє перевірити кількість запитів, зроблених вашим кодом.
class MyTestCase(TestCase)
def test_something(self):
with self.assertNumQueries(5)
# code that makes 5 expected queries
Якщо ви використовуєте pytest-django
, ви можете використовувати django_assert_num_queries
для досягнення тієї ж функціональності.
Перехоплення N+1 запитів за допомогою nplusone
N+1 запит - це поширена проблема продуктивності, яка виникає, коли ваш код робить більше запитів до бази даних, ніж потрібно. Пакет nplusone виявляє ці помилкові запити у вашому коді. Він працює шляхом генерування NPlusOneError
, коли один запит виконується багаторазово у циклі. Детальніше про це можна прочитати в попередньому пості блогу.
Хоча nplusone є незамінним інструментом, який я використовую у всіх своїх Django проектах, важливо відзначити, що пакет є недопрацьованим і ловить не всі порушення. Наприклад, я помітив, що він не працює з .only()
або .defer()
.
for user in User.objects.defer("email"):
# This should raise an NPlusOneError but it doesn't
email = user.email
Через ці недоліки разом з nplusone важливо використовувати інші методи оптимізації.
Перехоплення N+1 запитів за допомогою django-zen-queries
Пакет django-zen-queries
дозволяє вам контролювати, яким частинам вашого коду дозволено виконувати запити. Він включає менеджер/декоратор контексту queries_disabled()
, який згенерує виняток QueriesDisabledError
, коли всередині нього виконується запит. Ви можете використовувати його, щоб запобігти непотрібним запитам до попередньо вибраних об'єктів, або щоб переконатися, що запити викликаються тільки тоді, коли вони потрібні. Я використовую його для заповнення прогалин, яких не вистачає у nplusone
.
Наприклад, як зазначено у попередньому розділі, nplusone не перехопить наступний N+1 запит, а django-zen-queries
перехопить.
from zen_queries import fetch, queries_disabled
# The fetch function forces evaluation of the queryset, which is
# necessary before entering the queries_disabled context
qs = fetch(User.objects.defer("email"))
with queries_disabled():
for user in qs:
# Raises a QueriesDisabledError exception
email = user.email
Виправлення N+1 запитів шляхом уникнення нових запитів до попередньо вибраних об'єктів
Django ORM бракує вродженої здатності розрізняти, коли дані слід отримувати безпосередньо з бази даних, а коли використовувати ті, що зберігаються у пам'яті від попередніх запитів. При роботі з попередньо вибраними об'єктами, дані завжди повинні бути в пам'яті, припускаючи, що початкова вибірка отримує всі необхідні дані. Щоб переконатися, що Django використовує дані з пам'яті та уникає зайвих запитів, ви можете використовувати стандартний Python з Django методом all()
замість специфічних методів Django для набору запитів.
Наприклад, розглянемо наступний код:
for user in User.objects.prefetch_related("groups"):
# BAD: N+1 query
first_group = user.groups.first()
# GOOD: Does not make a new query
first_group = user.groups.all()[0]
qs.first()
робить новий запит до бази даних, тоді як qs.all()[0]
цього не робить.
Ось ще кілька прикладів:
Завжди виконує запит | Не виконує запит, якщо дані було отримано заздалегідь |
---|---|
qs.values_list("x", flat=True) |
[obj.x for obj in qs.all()] |
qs.values("x") |
[{"x": obj.x} for obj in qs.all()] |
qs.order_by("x", "y") |
sorted(qs.all(), lambda obj: (obj.x, obj.y)) |
qs.filter(x=1) |
[obj for obj in qs.all() if obj.x == 1] |
qs.exclude(x=1) |
[obj for obj in qs.all() if obj.x != 1] |
Зауважте, що пакет nplusone
має перехоплювати всі ці N+1 порушення, тому обов'язково використовуйте його.
Запобігайте отриманню великих полів, що не використовуються, за допомогою defer()
Деякі поля, такі як JSONField
і TextField
, вимагають дорогої обробки для завантаження в об'єкт Python. Це споживає багато пам'яті та сповільнює виконання запитів, особливо при роботі з наборами запитів, що містять кілька тисяч екземплярів або більше. Ви можете використовувати defer()
, щоб запобігти отриманню цих полів і підвищити продуктивність запиту.
class Book(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
pub_date = models.DateField()
notes = models.JSONField()
books = Book.objects.defer("content", "notes")
Крім того, ви можете використовувати метод only()
, щоб явно вказати поля, які ви хочете включити в результат запиту.
books = Book.objects.only("title", "pub_date")
Проте, у ситуаціях, коли ви хочете виключити певні великі поля, використання defer()
часто призводить до коротшого та ефективнішого коду.
Уникайте використання distinct()
на великих полях
Метод distinct()
усуває повторювані об'єкти з набору запитів, порівнюючи всі значення в наборі результатів. При застосуванні до великих полів, таких як JSONField
і TextField
, база даних повинна виконувати дорогі порівняння, що може призвести до уповільнення часу виконання запиту. У PixieBrix один невдалий запит distinct()
, який виконувався лише десятки разів на хвилину, виявився згубним, що призвело до повної зупинки роботи бази даних.
Щоб пом'якшити цю проблему, ви можете обмежити область застосування distinct()
, застосувавши її до підмножини полів. Найкращий варіант - скористатися попередньою підказкою і використати defer()
для повного виключення великих полів з набору результатів:
Book.objects.filter(<filter-that-generates-duplicates>).defer("content", "notes").distinct()
Якщо вам потрібні великі поля і ви використовуєте PostgreSQL, ви можете передати позиційні аргументи, щоб вказати поля, до яких має застосовуватися DISTINCT
, за допомогою distinct(*fields)
. Це вказує базі даних порівнювати лише вказані поля. В ідеалі ви повинні передати поле первинного ключа, але будь-яке унікальне поле буде працювати.
Book.objects.filter(<filter-that-generates-duplicates>).distinct("id")
Нехай вам буде легко виявити та оптимізувати ваші повільні запити.
Коментарі (1)