Django ORM

Индексы и Django ORM | Курс Django ORM урок 5.1

Индексы и Django ORM | Курс Django ORM урок 5.1
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 3

Цель урока

Разобраться с индексами в PostgreSQL: как Django создает их через Meta.indexes, какие типы существуют, как составные и частичные индексы влияют на производительность и когда индекс может навредить.

Необходимые знания

  • Урок 1.3: Meta options
  • Урок 1.4: миграции
  • Базовое понимание B-tree структур данных

Зачем нужны индексы

Без индекса поиск строки в таблице, это последовательный просмотр (seq scan): PostgreSQL читает каждую строку и проверяет условие. В таблице с миллионом строк поиск по неиндексированному полю займет пропорционально много времени.

Примеры ниже показаны для таблицы с большим количеством данных. В тестовой базе с несколькими десятками записей цифры будут минимальными (cost=0..1-2), потому что планировщик видит маленькую таблицу и разницы нет. На реальных данных картина принципиально другая.

-- Без индекса на slug - seq scan (таблица ~1 млн строк)
EXPLAIN SELECT * FROM shop_product WHERE slug = 'iphone-15';
-- Seq Scan on shop_product  (cost=0..1500 rows=1 width=200)
--   Filter: (slug = 'iphone-15')


-- С индексом - index scan
EXPLAIN SELECT * FROM shop_product WHERE slug = 'iphone-15';
-- Index Scan using shop_product_slug_key on shop_product  (cost=0.4..8.5 rows=1 width=200)
--   Index Cond: (slug = 'iphone-15')

Cost в единицах планировщика: 1500 против 8.5, разница в 175 раз. На большой таблице это разница между запросом, который выполняется секунды, и запросом, который выполняется мгновенно.


Индексы создаваемые Django автоматически

Django создает индексы в трех случаях:

1. Primary key: всегда, автоматически:

CREATE UNIQUE INDEX shop_product_pkey ON shop_product (id);

2. Для поля unique=True:

slug = models.SlugField(unique=True)


CREATE UNIQUE INDEX shop_product_slug_key ON shop_product (slug);

3. ForeignKey всегда автоматически:

category = models.ForeignKey(Category, on_delete=models.PROTECT)


CREATE INDEX shop_product_category_id ON shop_product (category_id);

Индекс на category_id ускоряет выборку всех продуктов категории (WHERE category_id = X). В обратную сторону, поиск категории по её id работает primary key индекс таблицы shop_category.


db_index - одиночный индекс на поле

class Product(models.Model):
    is_active = models.BooleanField(default=True, db_index=True)
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)


CREATE INDEX shop_product_is_active ON shop_product (is_active);
CREATE INDEX shop_product_created_at ON shop_product (created_at);

Когда нужен db_index=True:

  • Поле часто используется в filter() или order_by()
  • Таблица большая (тысячи строк и больше)
  • Поле имеет высокую кардинальность (много уникальных значений)

Когда не нужен:

  • Поле с низкой кардинальностью: is_active с 90% True, индекс почти не поможет при filter(is_active=True)
  • Маленькая таблица - seq scan быстрее чем index scan
  • Поле редко используется в WHERE

Meta.indexes - составные и специальные индексы

Составной индекс

class Meta:
    indexes = [
        models.Index(
            fields=["category", "is_active"],
            name="product_category_active_idx",
        ),
    ]


CREATE INDEX product_category_active_idx ON shop_product (category_id, is_active);

Составной индекс эффективен когда запрос фильтрует по обоим полям:

# Использует составной индекс
Product.objects.filter(category_id=1, is_active=True)


# Тоже используется, "левый префикс" работает
Product.objects.filter(category_id=1)


# НЕ использует, нет category_id в условии
Product.objects.filter(is_active=True)

Правило составного индекса: индекс (A, B) используется для запросов по A и по (A, B), но не для запросов только по B.

Индекс с порядком сортировки

class Meta:
    indexes = [
        models.Index(fields=["-created_at"], name="product_created_desc_idx"),
    ]


CREATE INDEX product_created_desc_idx ON shop_product (created_at DESC);

Полезен для запросов с ORDER BY created_at DESC LIMIT N, PostgreSQL использует индекс в нужном порядке без дополнительной сортировки.


Частичный индекс (PostgreSQL)

Индексирует только строки, удовлетворяющие условию. Если активных товаров 20% от общего числа, индекс в 5 раз меньше и быстрее обычного:

from django.db.models import Q

class Meta:
    indexes = [
        models.Index(
            fields=["price"],
            condition=Q(is_active=True),
            name="product_active_price_idx",
        ),
    ]


CREATE INDEX product_active_price_idx
    ON shop_product (price)
    WHERE is_active = true;

Этот индекс используется только если запрос включает is_active = true:

# Использует частичный индекс
Product.objects.filter(is_active=True, price__gt=500)

# НЕ использует, нет условия is_active=True
Product.objects.filter(price__gt=500)

UniqueConstraint как индекс

from django.db.models import UniqueConstraint

class Meta:
    constraints = [
        UniqueConstraint(
            fields=["product", "user"],
            name="unique_review_per_user",
        ),
    ]

UniqueConstraint автоматически создает уникальный индекс, отдельный Index добавлять не нужно.

Частичный UniqueConstraint

# Уникальность только среди активных записей (PostgreSQL)
UniqueConstraint(
    fields=["email"],
    condition=Q(is_active=True),
    name="unique_active_email",
)


CREATE UNIQUE INDEX unique_active_email
    ON users (email)
    WHERE is_active = true;

Как индексы влияют на запись

Индексы ускоряют SELECT, но замедляют INSERT, UPDATE, DELETE, при каждом изменении данных нужно обновить все индексы в таблице.

Для таблицы с 5 индексами каждый INSERT фактически делает 6 операций записи (1 в таблицу + 5 в индексы).

Последствия:

  • bulk_create() на таблице с индексами медленнее чем без них
  • При массовом импорте данных иногда выгодно временно удалить индексы, импортировать, воссоздать

Проверка использования индексов через EXPLAIN

Подробно разберем в уроке 5.2. Вкратце:

docker compose exec db psql -U postgres -d ormcourse


EXPLAIN SELECT * FROM shop_product WHERE is_active = true AND price > 500;


Seq Scan on shop_product  (cost=0.00..1.21 rows=10 width=102)
  Filter: (is_active AND (price > '500'::numeric))

"Seq Scan" = последовательный просмотр = индекс не используется.

На тестовой базе с десятками строк PostgreSQL выберет seq scan даже после добавления индекса, планировщик видит маленькую таблицу и знает, что читать её целиком дешевле. На таблице с сотнями тысяч строк после добавления индекса картина другая:

-- Иллюстративный пример для большой таблицы
Index Scan using shop_product_is_acti_price_idx on shop_product  (cost=0.28..8.50 rows=3 width=200)
  Index Cond: (is_active = true AND price > 500)

"Index Scan" = индекс используется. Планировщик переключается на него, когда это выгоднее seq scan.


Практическое задание

1) Посмотрите через psql (\d shop_product) какие индексы уже существуют на таблице shop_product. Какие созданы Django автоматически?

2) Добавьте в Meta составной индекс на (is_active, price). Сделай миграцию.
Проверьте через \d shop_product что индекс появился.

3) Добавьте частичный индекс на price только для активных продуктов. Через EXPLAIN проверьте, использует ли его запрос filter(is_active=True, price__gt=500)?

Перед проверкой через EXPLAIN создайте достаточно данных, на маленькой таблице PostgreSQL всегда выбирает seq scan, независимо от индексов:

   from shop.models import Product, Category
   import random

   category = Category.objects.first()
   products = [
       Product(
           name=f'Product {i}',
           slug=f'product-{i}',
           price=random.randint(100, 2000),
           is_active=random.choice([True, False]),
           category=category,
       )
       for i in range(10000)
   ]
   Product.objects.bulk_create(products)

4) Выполните в psql (с теми же 10 000 записями):

   EXPLAIN SELECT * FROM shop_product WHERE slug = 'iphone-15';
   EXPLAIN SELECT * FROM shop_product WHERE description LIKE '%phone%';

Для какого из них используется индекс и почему?


Возможные ошибки

Индекс на FK когда он уже есть

# Django уже создает индекс на ForeignKey - db_index=True избыточен
category = models.ForeignKey(Category, on_delete=models.PROTECT, db_index=True)

Индекс на поле с низкой кардинальностью

# is_active имеет только два значения, индекс малополезен для filter(is_active=True)
is_active = models.BooleanField(db_index=True)
# Лучше: частичный индекс на другое поле с condition=Q(is_active=True)

Слишком много индексов

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

Не указан name у Index

# Django сгенерирует имя, но оно может быть обрезано если длинное
models.Index(fields=["category", "is_active", "price"])

# Лучше явно
models.Index(fields=["category", "is_active", "price"], name="product_cat_active_price_idx")

Связь со следующим уроком

В уроке 5.2 разберем EXPLAIN ANALYZE в PostgreSQL, как читать план запроса, что значат Seq Scan и Index Scan, Rows и Cost, и как использовать это для диагностики проблем производительности.


<< Урок 4.4

Урок 5.2 >>


Подписывайтесь на мой Telegram канал

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

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы