Индексы и Django ORM | Курс Django ORM урок 5.1
Цель урока
Разобраться с индексами в PostgreSQL: как Django создает их через Meta.indexes, какие типы существуют, как составные и частичные индексы влияют на производительность и когда индекс может навредить.
Необходимые знания
Зачем нужны индексы
Без индекса поиск строки в таблице, это последовательный просмотр (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, и как использовать это для диагностики проблем производительности.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru