Оптимизация запросов Django - defer, only, exclude

В этой статье рассматриваются различия между наборами запросов Django defer(), only(), и методами exclude().


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

Чтобы свести к минимуму объем данных, извлекаемых при работе с Django, вы можете использовать методы defer(), only(), и exclude().

Давайте узнаем о них и о том, как использовать их на практике!

Содержимое

Введение в проект

На протяжении всей статьи мы будем работать над веб-приложением для недвижимости. Вы можете клонировать проект из репозитория на GitHub и следовать за ним или просто прочитать статью.

Чтобы настроить проект локально, следуйте инструкциям в файле README.md.

Веб-приложение имеет две модели: Property и Location:

# estates/models.py

class Location(models.Model):
    city = models.CharField(max_length=128)
    state = models.CharField(max_length=128)
    country = models.CharField(max_length=32)
    zip_code = models.CharField(max_length=32)
    # ...


class Property(models.Model):
    name = models.CharField(max_length=256)
    description = models.TextField()
    property_type = models.CharField(max_length=20, choices=PROPERTY_TYPES)
    location = models.ForeignKey(Location, on_delete=models.CASCADE)
    square_feet = models.PositiveIntegerField()
    bedrooms = models.PositiveSmallIntegerField()
    bathrooms = models.PositiveSmallIntegerField()
    has_garage = models.BooleanField(default=False)
    has_balcony = models.BooleanField(default=False)
    # ...

Обе модели имеют несколько полей и содержат метод to_json(), который преобразует все атрибуты модели в словарь Python.

Веб-приложение предоставляет простой API со следующими конечными точками:

  1. / возвращает сокращенный сериализованный список всех свойств
  2. /<int:id>/ возвращает все сведения о конкретном объекте недвижимости
  3. /<int:id>/amenities/ возвращает информацию об удобствах конкретного объекта недвижимости

Начальный тест

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

Чтобы сгенерировать отчеты о производительности, мы можем обратиться к конечным точкам веб-приложения:

  1. http://127.0.0.1:8000/
  2. http://127.0.0.1:8000/1/
  3. http://127.0.0.1:8000/1/amenities/

Вы заметите, что ответ в виде списка и в представлении удобств содержит подмножество атрибутов свойства, в то время как подробное представление содержит все атрибуты свойства.

Затем перейдите к http://127.0.0.1:8000/silk/requests/ и просмотрите только что созданные отчеты.

В моем случае просмотр списка занимает 765 миллисекунд (301 миллисекунды из которых тратятся на запросы), в то время как две другие занимают 11 миллисекунд (~1 по запросам).

Рассматривая SQL-запросы, мы видим, что все три из них выдают аналогичный запрос:

-- List query
SELECT * FROM "estates_property"
INNER JOIN "estates_location" ON (
    "estates_property"."location_id" = "estates_location"."id"
)

-- Detail query
SELECT * FROM "estates_property"
INNER JOIN "estates_location" ON (
    "estates_property"."location_id" = "estates_location"."id"
)
WHERE "estates_property"."id" = 1

-- Amenities query
SELECT * FROM "estates_property"
WHERE "estates_property"."id" = 1

Во всех трех представлениях отображаются все поля свойств, хотя в представлениях "удобства" и "список" отображается лишь небольшая их часть. Давайте исправим это!

defer()

Вы можете использовать метод defer() при запросе моделей Django, чтобы "исключить" определенные поля, которые не требуются для вашего конкретного запроса. Например, в listview вы можете отложить все поля, которые вы не отображаете (например,, description, created_at, и updated_at).

Этот метод наиболее эффективен при переносе полей, содержащих много данных (например, текстовых полей), или полей, которые требуют дорогостоящей обработки для преобразования в объекты Python (например, JSON).

defer() работает на уровне атрибутов. По сути, это изменяет SQL следующим образом:

-- No defer(): The query fetches all the columns
SELECT * FROM some_table;

-- Using defer(): The query fetches all the columns except the deferred ones
SELECT column_1, column_2, ... column_n FROM some_table;

На практике мы могли бы отложить наши properties_list_view() Наборы запросов description атрибут:

properties = Property.objects.select_related("location").defer(
    "description"
)

Мы используем select_related(), чтобы избежать запроса N+1. Более подробная информация в этой статье.

Если мы снова протестируем конечную точку, то увидим, что запрос SQL SELECT больше не содержит атрибута description. Кроме того, время запроса сократилось с 765 миллисекунд до 184 миллисекунд. Это примерно в 4 раза больше!

only()

Метод only() является полной противоположностью методу defer().

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

Опять же, это работает на уровне атрибутов и аналогичным образом изменяет базовый SQL:

-- No only(): The query fetches all the columns
SELECT * FROM some_table;

-- Using only(): The query fetches only the provided subset of columns
SELECT only_column1, only_column2, ... only_column_n FROM some_table;

Чтобы проверить это в действии, мы можем изменить properties_list_view Набор запросов следующим образом:

properties = Property.objects.select_related("location").only(
    "id", "name", "location", "price"
)

После тестирования мы можем увидеть еще одно небольшое улучшение. Время выполнения запроса сократилось с 184 миллисекунд до примерно 154 миллисекунд.

Если вы хотите получить только определенные поля связанной модели, вы можете использовать разделитель поиска __ в Django. Например: location__city будет отображаться только поле города местоположения.

Кроме того, вы можете выполнить несколько вызовов в defer() и only() в рамках одного запроса.

exclude()

Метод exclude() является противоположностью метода filter(). Он возвращает объекты, которые не соответствуют заданным параметрам поиска. В то время как предыдущие два метода работают на уровне столбцов, этот работает на уровне строк.

Например:

# Fetches all the apartments
apartments = Property.objects.filter(property_type=PROPERTY_TYPE_APARTMENT)

# Fetches all the properties that aren't apartments
non_apartments = Property.objects.exclude(property_type=PROPERTY_TYPE_APARTMENT)

Выдает следующий SQL-код:

-- Using filter(): The query fetches all the apartments
SELECT * FROM "estates_property" WHERE ("estates_property"."property_type" = AP)

-- Using exclude(): The query fetches all properties that aren't apartments
SELECT * FROM "estates_property" WHERE NOT ("estates_property"."property_type" = AP)

Так же, как и в случае с filter(), вы можете создавать условия. Например, чтобы получить объекты, которые не являются земельными участками и имеют размер более 1000 футов, вы могли бы сделать это:

big_buildings = (
    Property.objects.
        exclude(property_type=PROPERTY_TYPE_LAND, square_feet__lt=1000)
)

Подводный камень метода defer() и only()

Неправильное использование defer() и only() может привести к проблемам с производительностью, а не к улучшению.

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

На данный момент наш property_amenities_view выглядит так:

def property_amenities_view(request, id):
    property = Property.objects.only(
        "id", "has_garage", "has_balcony", "has_basement", "has_pool"
    ).get(id=id)

    return JsonResponse({
        "id": property.id,
        "has_garage": property.has_garage,
        "has_balcony": property.has_balcony,
        "has_basement": property.has_basement,
        "has_pool": property.has_pool,
    })

В представлении используется only(), чтобы избежать извлечения каких-либо ненужных полей из базы данных.

Базовый SQL выглядит примерно так:

SELECT "estates_property"."id",
       "estates_property"."has_garage",
       "estates_property"."has_balcony",
       "estates_property"."has_basement",
       "estates_property"."has_pool"
FROM "estates_property" WHERE "estates_property"."id" = 1

Затем, через некоторое время, мы передумываем и решаем добавить bedrooms и bathrooms к ответу в формате JSON, поскольку они также технически удобны:

def property_amenities_view(request, id):
    property = Property.objects.only(
        "id", "has_garage", "has_balcony", "has_basement", "has_pool"
    ).get(id=id)

    return JsonResponse({
        "id": property.id,
        "bedrooms": property.bedrooms,       # new
        "bathrooms": property.bathrooms,     # new
        "has_garage": property.has_garage,
        "has_balcony": property.has_balcony,
        "has_basement": property.has_basement,
        "has_pool": property.has_pool,
    })

Сделав это, мы только что ввели два дополнительных запроса. Поскольку мы не включили bedrooms и bathrooms в подмножество only(), Django теперь извлекает дополнительные поля отдельно.

Базовый SQL теперь выглядит следующим образом:

-- This query fetches the only() field subset
SELECT "estates_property"."id",
       "estates_property"."has_garage",
       "estates_property"."has_balcony",
       "estates_property"."has_basement",
       "estates_property"."has_pool"
FROM "estates_property" WHERE "estates_property"."id" = 1

-- An extra query for fetching the bedrooms
SELECT "estates_property"."id",
       "estates_property"."bedrooms"
FROM "estates_property" WHERE "estates_property"."id" = 1

-- An extra query for fetching the bathrooms
SELECT "estates_property"."id",
       "estates_property"."bathrooms"
FROM "estates_property" WHERE "estates_property"."id" = 1

Помните, что при рефакторинге кода вы должны быть осторожны с only() и exclude().

Вы также можете защититься от появления дополнительных SQL-запросов при работе с этими представлениями в будущем, используя django_assert_num_queries. Подробнее об этом читайте в статье Автоматизация тестирования производительности в Django.

Заключение

Чтобы ускорить выполнение запросов в Django, вам следует попробовать извлекать минимальный объем данных из базы данных. Это относится как к строкам (объектам модели), так и к столбцам (атрибутам модели).

Два метода, которые позволяют вам контролировать, какие атрибуты модели будут выбраны, - это defer() и only(). Эти два параметра оказывают наиболее существенное влияние при исключении атрибутов, содержащих много данных или требующих дорогостоящей обработки.

С другой стороны, чтобы сделать исключения на уровне объекта, вы можете использовать exclude().

Дополнительные советы по оптимизации Django смотрите здесь:

Вернуться на верх