Как определить, какие поля нужно индексировать в таблице?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Определение полей для индексирования в базе данных
Выбор правильных полей для индексирования — критический аспект оптимизации базы данных. За 10+ лет работы я разработал систематический подход к анализу и выбору полей для индексирования на основе нескольких факторов.
Основной критерий: Selectivity (Селективность)
Перый фактор — насколько хорошо индекс сокращает количество проверяемых строк:
-- Низкая селективность (плохой индекс)
SELECT COUNT(*) FROM users WHERE is_active = true;
-- Результат: 95% строк имеют is_active = true
-- Индекс не поможет
-- Высокая селективность (хороший индекс)
SELECT COUNT(*) FROM users WHERE email = 'user@example.com';
-- Результат: 0.001% строк
-- Индекс очень помогает
Правило: индексируйте поля, по которым вы фильтруете и которые имеют высокую уникальность (low cardinality данные плохо индексируются).
Критерии выбора полей для индексирования
1. Частота использования в WHERE
Получите статистику по запросам:
-- PostgreSQL: анализ плана выполнения
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 123 AND created_at > NOW() - INTERVAL '30 days';
-- Если видите Sequential Scan — нужен индекс
-- Если видите Index Scan — уже оптимизировано
Чем чаще поле используется в WHERE, тем больше причин его индексировать.
2. Частота использования в JOIN
-- Часто используются в JOIN'ах
SELECT * FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.created_at > NOW() - INTERVAL '1 year';
-- user_id и id должны быть индексированы (обычно это primary key)
-- created_at может получить индекс если часто используется
3. Количество уникальных значений (Cardinality)
-- Анализ количества уникальных значений
SELECT
COLUMN_NAME,
COUNT(DISTINCT COLUMN_NAME) as unique_count,
COUNT(*) as total_count,
ROUND(100.0 * COUNT(DISTINCT COLUMN_NAME) / COUNT(*), 2) as selectivity_percent
FROM users
GROUP BY COLUMN_NAME;
Правила:
- Высокая кардинальность (>95% уникальных): email, phone, user_id → отличные для индекса
- Средняя (20-95%): status, category_id → может быть индекс
- Низкая (<20%): is_active, gender, country_code → плохие для индекса
4. Размер данных (Data Type)
-- Ориентировочные размеры для индексирования
INT / BIGINT: 4-8 байт → эффективно
VARCHAR(255): до 255 байт → хорошо
VARCHAR(1000+): 1000+ байт → осторожно, увеличивает размер индекса
JSON/TEXT: очень большой → используйте hash индекс или BRIN
TIMESTAMP: 8 байт → эффективно
5. Частота изменений данных
// Поля с частыми изменениями требуют больше IO при вставке/обновлении
// UPDATE users SET last_login = NOW() WHERE id = 123;
// Это обновляет индекс на last_login
// Вес обновления:
// - Часто обновляемое поле с индексом = дополнительные затраты
// - Редко обновляемое поле с индексом = только выгода
Практические примеры индексирования
Пример 1: E-commerce приложение
public class Product {
private Long id; // PRIMARY KEY (индекс автоматический)
private String name; // Часто поиск по имени
private Long categoryId; // Часто фильтруют по категории
private Double price; // Фильтруют по цене
private Boolean isActive; // LOW cardinality, плохо для индекса
private LocalDateTime createdAt; // Сортируют и фильтруют
}
// Стратегия индексирования
-- ОБЯЗАТЕЛЬНО индексировать (high selectivity, часто используются)
CREATE INDEX idx_product_category_id ON products(category_id);
CREATE INDEX idx_product_name ON products(name);
CREATE INDEX idx_product_price ON products(price);
CREATE INDEX idx_product_created_at ON products(created_at DESC);
-- НЕ индексировать (low selectivity, редко используется)
-- is_active (98% true)
-- status (только 5 разных значений)
-- Composite индекс для частых запросов
CREATE INDEX idx_products_category_active_date ON products(
category_id,
is_active,
created_at DESC
) WHERE is_active = true;
Пример 2: Социальная сеть (посты, комментарии)
public class Post {
private Long id;
private Long userId; // JOIN с user, индекс нужен
private String content; // Полнотекстовый поиск, НЕ обычный индекс
private LocalDateTime publishedAt; // Сортировка, индекс нужен
private Integer likeCount; // Часто меняется, индекс плохо помогает
}
public class Comment {
private Long id;
private Long postId; // JOIN с post, индекс критичен
private Long userId; // Часто фильтруют, индекс нужен
private LocalDateTime createdAt; // Сортировка, индекс нужен
}
-- Posts
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_published_at ON posts(published_at DESC);
CREATE INDEX idx_posts_user_published ON posts(user_id, published_at DESC);
-- Comments
CREATE INDEX idx_comments_post_id ON comments(post_id);
CREATE INDEX idx_comments_user_id ON comments(user_id);
CREATE INDEX idx_comments_post_created ON comments(post_id, created_at DESC);
CREATE INDEX idx_comments_user_created ON comments(user_id, created_at DESC);
-- Для полнотекстового поиска (специальный GIN индекс)
CREATE INDEX idx_posts_content_search ON posts USING GIN(
to_tsvector('russian', content)
);
Пример 3: Интернет-магазин с заказами
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL, -- LOW cardinality
total_amount DECIMAL(10,2),
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
-- Часто ищут заказы пользователя
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- Администраторы ищут новые заказы с фильтром по статусу и дате
CREATE INDEX idx_orders_status_created
ON orders(status, created_at DESC)
WHERE status IN ('pending', 'processing');
-- Отчёты по сумме заказов за период
CREATE INDEX idx_orders_created_amount ON orders(created_at DESC, total_amount);
Алгоритм выбора индексов
Шаг 1: Проанализируйте slow queries
# MySQL slow query log
[mysqld]
long_query_time = 1
log-slow-queries = /var/log/mysql/slow-queries.log
# PostgreSQL
ALTER DATABASE mydb SET log_min_duration_statement = 1000; -- 1 second
Шаг 2: Используйте EXPLAIN для каждого запроса
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE user_id = 123
AND created_at > NOW() - INTERVAL '30 days'
ORDER BY created_at DESC;
-- Смотрите на:
-- - Seq Scan vs Index Scan
-- - Actual Rows vs Planned Rows
-- - Execution Time
Шаг 3: Выбирайте индексы с наибольшим ROI
public class IndexSelectionStrategy {
// ROI = (QueryFrequency * AverageScanRowsWithoutIndex / AverageScanRowsWithIndex)
// / (IndexMaintenanceCost + StorageCost)
// Высокий ROI (создавайте индекс):
// - Часто используется (10+ раз в день)
// - Сокращает сканирование с 1000000 до 10 строк
// - Маленькое поле (INT, DATE)
// Низкий ROI (не создавайте индекс):
// - Редко используется (1 раз в месяц)
// - Сокращает сканирование с 100 до 50 строк
// - Большое поле (TEXT, JSON)
}
Проблемы и решения
Проблема: Слишком много индексов
-- Проверка неиспользуемых индексов
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY idx_blks_read DESC;
-- Удаление неиспользуемых
DROP INDEX idx_unused_index;
Проблема: Индекс не используется
-- Иногда нужно намекнуть Optimizer'у
SET enable_seqscan = off; -- Не использовать sequential scan
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
SET enable_seqscan = on; -- Восстановить
-- Или использовать hint (в MySQL)
SELECT /*+ USE_INDEX(users idx_email) */ *
FROM users
WHERE email = 'test@example.com';
Проблема: Размер индекса слишком большой
-- Использовать BRIN для больших таблиц с монотонными данными
CREATE INDEX idx_orders_date_brin ON orders
USING BRIN (created_at);
-- BRIN меньше в 10-100 раз, но медленнее чем B-tree
-- Использовать HASH для некоторых операций
CREATE INDEX idx_status_hash ON orders USING HASH (status);
Заключение
Выбор полей для индексирования — это баланс между:
- Частотой использования — полезен ли индекс
- Селективностью — насколько он сокращает сканирование
- Стоимостью обслуживания — как влияет на INSERT/UPDATE
- Размером — сколько места занимает
Не индексируйте всё подряд — это замедлит вставки и обновления. Индексируйте только поля, которые действительно используются для фильтрации, JOIN'ов и сортировки с высокой селективностью.