INCLUDE-индексы для устранения Table Scan и оптимизации SELECT.
Покрывающий индекс — это когда все данные запроса находятся в индексе. Table Scan не требуется — это максимальная производительность.
Покрывающий индекс (covering index) — это индекс, содержащий все данные, необходимые для выполнения запроса.
CREATE INDEX idx_users_email ON users(email);
-- Запрос
SELECT email, name, created_at FROM users WHERE email = 'test@example.com';Что происходит:
idx_users_emailemail = 'test@example.com'name и created_at из таблицыДва чтения: индекс + таблица = дороже
CREATE INDEX idx_users_covering ON users(email) INCLUDE (name, created_at);
-- Тот же запрос
SELECT email, name, created_at FROM users WHERE email = 'test@example.com';Что происходит:
idx_users_coveringemail = 'test@example.com'name и created_at из индексаОдно чтение: только индекс = быстрее
Когда PostgreSQL может выполнить запрос только по индексу, используется Index Only Scan.
CREATE INDEX idx_orders_covering ON orders(user_id, status) INCLUDE (total, created_at);
-- Запрос
SELECT user_id, status, total, created_at
FROM orders
WHERE user_id = 123 AND status = 'pending';
-- План выполнения
Index Only Scan using idx_orders_covering on orders
Index Cond: (user_id = 123 AND status = 'pending')Преимущества:
EXPLAIN (ANALYZE, BUFFERS)
SELECT user_id, status, total, created_at
FROM orders
WHERE user_id = 123 AND status = 'pending';Ищите в выводе:
Index Only Scan — хорошоBuffers: shared hit=... — сколько страниц прочитаноHeap Fetch — требуется чтение таблицы (плохо)CREATE INDEX index_name ON table_name (key_column) INCLUDE (non_key_column);| Ключевые колонки | INCLUDE колонки |
|---|---|
| Участвуют в поиске | Не участвуют в поиске |
| Индексируются (сортируются) | Хранятся в листовых узлах без сортировки |
| Могут использоваться для WHERE, JOIN, ORDER BY | Только для SELECT |
| Влияют на структуру B-дерева | Не влияют на структуру |
CREATE INDEX idx_reports ON reports(department_id, report_date)
INCLUDE (total_amount, status, manager_id);Использование:
department_id, report_date — для WHERE, JOIN, ORDER BYtotal_amount, status, manager_id — только для SELECT-- ✅ Эффективно: поиск по ключевым, данные из INCLUDE
SELECT total_amount, status
FROM reports
WHERE department_id = 5 AND report_date = '2026-01-01';
-- ❌ Неэффективно: попытка поиска по INCLUDE
SELECT total_amount, status
FROM reports
WHERE status = 'approved'; -- status не в ключевых колонках!-- Частый запрос в приложении
SELECT email, name, avatar_url
FROM users
WHERE email = ?;
-- Покрывающий индекс
CREATE INDEX idx_users_auth ON users(email) INCLUDE (name, avatar_url);-- Ежедневный отчёт
SELECT
DATE(created_at) as date,
COUNT(*) as orders_count,
SUM(amount) as total_amount
FROM orders
WHERE created_at BETWEEN ? AND ?
GROUP BY DATE(created_at);
-- Индекс для отчёта
CREATE INDEX idx_orders_report ON orders(created_at)
INCLUDE (amount);-- Частый JOIN
SELECT o.id, o.total, u.name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at > '2026-01-01';
-- Индексы
CREATE INDEX idx_orders_date ON orders(created_at) INCLUDE (user_id, total);
CREATE INDEX idx_users_covering ON users(id) INCLUDE (name, email);CREATE INDEX idx_composite ON orders(user_id, status, total, created_at);Проблемы:
CREATE INDEX idx_covering ON orders(user_id, status) INCLUDE (total, created_at);Преимущества:
| Сценарий | Составной | Покрывающий |
|---|---|---|
| WHERE по всем колонкам | ✅ Да | ❌ Нет |
| ORDER BY по всем колонкам | ✅ Да | ❌ Нет |
| Только SELECT по дополнительным колонкам | ⚠️ Работает, но избыточен | ✅ Идеально |
| Размер индекса критичен | ❌ Нет | ✅ Да |
Некоторые типы данных нельзя использовать в INCLUDE:
Проверяйте документацию для вашей версии PostgreSQL.
-- ❌ Ошибка: UNIQUE требует, чтобы все колонки были ключевыми
CREATE UNIQUE INDEX idx_unique ON users(email) INCLUDE (name);
-- ✅ Правильно: все колонки уникальности в ключевых
CREATE UNIQUE INDEX idx_unique ON users(email, name);INCLUDE-колонки увеличивают размер индекса:
-- Таблица
CREATE TABLE products (
id BIGSERIAL,
sku VARCHAR(50),
name VARCHAR(200),
description TEXT,
price DECIMAL,
stock INTEGER
);
-- Индекс без INCLUDE
CREATE INDEX idx_products_sku ON products(sku);
-- Размер: ~2 MB для 100K записей
-- Индекс с INCLUDE
CREATE INDEX idx_products_covering ON products(sku)
INCLUDE (name, price, stock);
-- Размер: ~8 MB для 100K записейПравило: Включайте только часто используемые колонки.
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...;Ищите:
Index Only Scan — хорошоHeap Fetch: 0 — отлично (таблица не читалась)Heap Fetch: N — N раз читалась таблица (проверьте VACUUM)SELECT
indexrelname,
idx_scan,
idx_tup_read,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE indexrelname LIKE '%covering%';Если Heap Fetch > 0, возможно нужен VACUUM:
-- Visibility map устарела
VACUUM orders;
-- После VACUUM Index Only Scan должен работать лучше-- Частые запросы к товарам
SELECT name, price, image_url, stock
FROM products
WHERE category_id = ? AND brand_id = ?;
-- Покрывающий индекс
CREATE INDEX idx_products_filter
ON products(category_id, brand_id)
INCLUDE (name, price, image_url, stock);-- Лента новостей
SELECT post_id, author_id, content, likes_count, created_at
FROM posts
WHERE author_id = ?
ORDER BY created_at DESC
LIMIT 20;
-- Покрывающий индекс
CREATE INDEX idx_posts_feed
ON posts(author_id, created_at DESC)
INCLUDE (content, likes_count);-- История транзакций
SELECT
transaction_id,
amount,
currency,
status,
created_at
FROM transactions
WHERE account_id = ?
AND created_at BETWEEN ? AND ?
ORDER BY created_at DESC;
-- Покрывающий индекс
CREATE INDEX idx_transactions_history
ON transactions(account_id, created_at DESC)
INCLUDE (amount, currency, status);Для сложных отчётов:
CREATE MATERIALIZED VIEW mv_daily_reports AS
SELECT
DATE(created_at) as date,
COUNT(*) as orders_count,
SUM(amount) as total_amount
FROM orders
GROUP BY DATE(created_at);
CREATE INDEX idx_mv_date ON mv_daily_reports(date);Для очень больших таблиц:
-- Партиционирование по датам + покрывающие индексы на партициях
CREATE TABLE orders (
...
) PARTITION BY RANGE (created_at);Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.