← Назад к вопросам

Почему не следует вешать индексы на все поля?

2.0 Middle🔥 161 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Краткий ответ

Индексы на все поля замедляют запросы на запись (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.