Почему не следует вешать индексы на все поля?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Краткий ответ
Индексы на все поля замедляют запросы на запись (INSERT, UPDATE, DELETE), требуют больше памяти, замедляют запуск приложения и усложняют анализ плана выполнения. Индексы — не бесплатны, их нужно выбирать исходя из паттернов чтения и анализа slow queries.
Проблема 1: Замедление операций записи
Каждый индекс нужно обновлять при INSERT, UPDATE, DELETE:
-- Таблица с одним индексом
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255),
age INT,
city VARCHAR(100),
country VARCHAR(100),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX idx_email ON users(email);
-- INSERT с 1 индексом: быстро
INSERT INTO users (...) VALUES (...);
-- Теперь добавим индексы на ВСЕ поля
CREATE INDEX idx_name ON users(name);
CREATE INDEX idx_age ON users(age);
CREATE INDEX idx_city ON users(city);
CREATE INDEX idx_country ON users(country);
CREATE INDEX idx_created_at ON users(created_at);
CREATE INDEX idx_updated_at ON users(updated_at);
-- Теперь одна вставка обновляет 7 структур (первичный ключ + 6 индексов)
INSERT INTO users (...) VALUES (...);
Време вставки растёт линейно с количеством индексов:
import time
import psycopg2
connection = psycopg2.connect('dbname=testdb')
cursor = connection.cursor()
# Тест без индексов
start = time.time()
for i in range(100000):
cursor.execute(
'INSERT INTO users (name, email, age) VALUES (%s, %s, %s)',
(f'user_{i}', f'user_{i}@example.com', i % 100)
)
connection.commit()
time_no_index = time.time() - start
print(f'Time without indexes: {time_no_index:.2f}s') # ~5s
# Тест с 7 индексами
# Результат: ~25s (5x медленнее)
Проблема 2: Использование памяти
Каждый индекс требует оперативную память и дисковое пространство:
-- Проверить размер индексов
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size
FROM pg_indexes
JOIN pg_stat_user_indexes ON indexname = relname
WHERE schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;
-- Пример результата:
-- idx_email: 50 MB
-- idx_name: 45 MB
-- idx_age: 25 MB
-- idx_city: 30 MB
-- idx_country: 28 MB
-- idx_created_at: 40 MB
-- idx_updated_at: 35 MB
-- ИТОГО: ~255 MB + основная таблица
Проблема 3: Загрузка индексов в буфер
Все индексы загружаются в shared buffers при старте:
# При запуске PostgreSQL загружает часто используемые индексы
# Если индексов много, буфер заполняется неиспользуемыми данными
# Пример: 8 GB памяти для buffer pool
# 20 индексов по 100 MB каждый = 2 GB
# Остаётся только 6 GB для горячих данных
Проблема 4: Query Planner путается
Оптимизатор с большим количеством индексов может выбрать неоптимальный план:
-- Сложный запрос с множеством индексов
SELECT *
FROM users
WHERE age > 25
AND city = 'New York'
AND country = 'USA'
AND created_at > NOW() - INTERVAL '1 year';
-- С 4 индексами плаерша может:
-- 1. Использовать индекс на age (плохой выбор, много строк)
-- 2. Использовать индекс на city (лучший выбор, селективнее)
-- 3. Использовать индекс на country (может быть лучше)
-- 4. Использовать индекс на created_at (может быть лучше)
-- 5. Использовать комбинацию (composite index)
-- С 10+ индексами оптимизатор может выбрать неоптимальное сочетание
Проблема 5: Maintenance overhead
Индексы нужно анализировать и поддерживать:
-- VACUUM - очистка мёртвых версий в индексах
VACUUM ANALYZE users;
-- REINDEX - перестроение индексов (может быть медленно)
REINDEX TABLE users;
-- С 1 индексом: быстро
-- С 20 индексами: долго
Правило: индексируй только поля используемые в запросах
# Анализируем slow queries
# Запрос 1: Поиск по email
SELECT * FROM users WHERE email = 'user@example.com';
# Нужен индекс: CREATE INDEX idx_email ON users(email);
# Запрос 2: Поиск по возрасту и городу
SELECT * FROM users WHERE age > 25 AND city = 'NY';
# Лучше: CREATE INDEX idx_age_city ON users(age, city);
# Запрос 3: Редко используемый
SELECT * FROM users WHERE country = 'UK';
# Не индексируем, если используется редко
# Запрос 4: В ORDER BY
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
# Нужен индекс: CREATE INDEX idx_created_at_desc ON users(created_at DESC);
Пример реальной ситуации
import psycopg2
import time
from psycopg2.extras import execute_values
connection = psycopg2.connect('dbname=benchmark')
cursor = connection.cursor()
# Создание таблицы
cursor.execute('''
CREATE TABLE IF NOT EXISTS products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255),
sku VARCHAR(100),
price NUMERIC(10, 2),
category VARCHAR(100),
stock INT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
''')
connection.commit()
# Генерируем 1 миллион товаров
data = []
for i in range(1000000):
data.append((
f'Product {i}',
f'SKU-{i:06d}',
round(10 + (i % 1000) * 0.01, 2),
['Electronics', 'Books', 'Clothing', 'Home'][i % 4],
i % 100,
f'2024-01-01 00:00:00'
))
# Вставка БЕЗ индексов
start = time.time()
execute_values(
cursor,
'INSERT INTO products (name, sku, price, category, stock, created_at) VALUES %s',
data
)
connection.commit()
time_without_index = time.time() - start
print(f'Insert without indexes: {time_without_index:.2f}s') # ~30s
# Добавляем индексы (ВСЕ ПОЛЯ)
cursor.execute('CREATE INDEX idx_name ON products(name)')
cursor.execute('CREATE INDEX idx_sku ON products(sku)')
cursor.execute('CREATE INDEX idx_category ON products(category)')
cursor.execute('CREATE INDEX idx_price ON products(price)')
cursor.execute('CREATE INDEX idx_stock ON products(stock)')
connection.commit()
# Вставка С индексами (воображаемо, на той же таблице)
# time_with_index ~120s (4x медленнее)
Правильный подход: анализ slow queries
import psycopg2
from psycopg2.extras import DictCursor
connection = psycopg2.connect('dbname=mydb')
cursor = connection.cursor(DictCursor)
# 1. Включаем логирование медленных запросов
cursor.execute('SET log_min_duration_statement = 100')
# 2. Выполняем типичные операции
cursor.execute('SELECT * FROM users WHERE email = %s', ('user@example.com',))
# 3. Проверяем план выполнения
cursor.execute('EXPLAIN ANALYZE SELECT * FROM users WHERE email = %s', ('user@example.com',))
for row in cursor:
print(row)
# 4. Если видим Seq Scan (полный скан таблицы), добавляем индекс
# Если видим Index Scan, индекс уже есть и помогает
Стратегия индексирования
Шаг 1: Определить типичные запросы
-- Читаем логи slow queries
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
WHERE mean_time > 100 -- Медленнее 100ms
ORDER BY total_time DESC;
Шаг 2: Создавать индексы только для них
-- Для типичных запросов
CREATE INDEX idx_users_email ON users(email); -- WHERE email = ?
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at DESC); -- WHERE user_id = ? ORDER BY created_at DESC
CREATE INDEX idx_orders_status_date ON orders(status, created_at); -- WHERE status = ? AND created_at > ?
Шаг 3: Мониторить использование
-- Проверить, используется ли индекс
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;
-- Если idx_scan = 0, индекс не используется, можно удалить
DROP INDEX idx_unused_index;
Composite vs Single индексы
-- Плохо: много single индексов
CREATE INDEX idx_age ON users(age);
CREATE INDEX idx_city ON users(city);
CREATE INDEX idx_country ON users(country);
-- Хорошо: один composite индекс
CREATE INDEX idx_age_city_country ON users(age, city, country);
-- Composite индекс используется в WHERE с любым префиксом:
-- WHERE age = ? - использует индекс
-- WHERE age = ? AND city = ? - использует индекс
-- WHERE age = ? AND city = ? AND country = ? - использует индекс
-- WHERE city = ? - НЕ использует (нарушен порядок)
Итоговая рекомендация
# ✅ Правильно
class UserSchema:
# Индексируем только важные поля
id = Column(Integer, primary_key=True) # PRIMARY KEY индекс
email = Column(String, unique=True, index=True) # Часто ищем
username = Column(String(100), index=True) # Часто ищем
created_at = Column(DateTime, index=True) # Часто сортируем
# Остальные поля БЕЗ индексов
first_name = Column(String(100)) # Редко ищем
last_name = Column(String(100)) # Редко ищем
bio = Column(Text) # Не индексируем текстовые поля
# ❌ Неправильно
class BadSchema:
# Индексируем ВСЕ поля - никогда не делай так!
id = Column(Integer, primary_key=True, index=True)
email = Column(String, index=True)
username = Column(String(100), index=True)
first_name = Column(String(100), index=True)
last_name = Column(String(100), index=True)
bio = Column(Text, index=True) # Даже текст!
created_at = Column(DateTime, index=True)
updated_at = Column(DateTime, index=True)
Вывод: Не индексируй все поля. Каждый индекс замедляет запись, требует памяти и усложняет поддержку. Индексируй только поля используемые в WHERE, JOIN, ORDER BY, основываясь на анализе slow queries.