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

Что будешь делать, если планировщик не использует индекс при поиске записи

1.0 Junior🔥 171 комментариев
#Soft Skills и карьера

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

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

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

Отладка проблемы: Планировщик не использует индекс

Систематический подход к решению

Это типичная и критичная для production проблема. Когда запрос выполняется полным сканированием таблицы (Seq Scan) вместо индекса, это может замедлить систему в 1000x раз. Вот как я диагностирую и решаю эту проблему.

Шаг 1: Подтверждение проблемы с EXPLAIN

Сначала смотрю план запроса

// Пример: ищем пользователя по email
SELECT * FROM users WHERE email = 'test@example.com';

// Диагностика:
EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM users WHERE email = 'test@example.com';

// Результат может быть:
Seq Scan on users (cost=0.00..35000.00 rows=1000000)
  Filter: (email = 'test@example.com')
  Buffers: shared hit=10000 read=5000

Вывод: Полное сканирование (Seq Scan) вместо индекса (Index Scan)

// Что хотим видеть:
Index Scan using idx_user_email on users (cost=0.29..8.30 rows=1)
  Index Cond: (email = 'test@example.com')
  Buffers: shared hit=4

Шаг 2: Проверка, существует ли индекс

Смотрю, какие индексы есть на таблице

// В PostgreSQL
SELECT 
    indexname,
    indexdef
FROM pg_indexes
WHERE tablename = 'users';

// В MySQL
SHOW INDEXES FROM users;
SHOW CREATE TABLE users;

// Возможные результаты:
// 1. Индекса нет вообще → создаём
// 2. Индекс есть, но условие не подходит → переделаем условие
// 3. Индекс есть, но статистика устарела → ANALYZE
// 4. Индекс неправильного типа → перестроим

Шаг 3: Типичные причины и решения

Причина 1: Индекса просто нет

// Была таблица
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255),
    name VARCHAR(255),
    created_at TIMESTAMP
    // Нет индекса на email!
);

// Запрос медленный
SELECT * FROM users WHERE email = 'test@example.com'; // Seq Scan

// Решение: создаём индекс
CREATE INDEX idx_user_email ON users(email);

// Проверяем
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
// Теперь: Index Scan

Причина 2: Условие WHERE не может использовать индекс

// Есть индекс на email
CREATE INDEX idx_user_email ON users(email);

// Но запрос использует функцию
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';
// ← Seq Scan, потому что индекс на LOWER(email)

// Решение 1: Создаём индекс на функцию
CREATE INDEX idx_user_email_lower ON users(LOWER(email));
// Теперь запрос с LOWER будет использовать индекс

// Решение 2: Нормализуем данные
ALTER TABLE users ADD COLUMN email_normalized VARCHAR(255) GENERATED ALWAYS AS (LOWER(email));
CREATE INDEX idx_user_email_norm ON users(email_normalized);

// Решение 3: Переделываем запрос (если возможно)
SELECT * FROM users WHERE email = LOWER('test@example.com');
// Все равно может быть Seq Scan, потому что LOWER меняет условие

Причина 3: Тип данных в условии не совпадает

// Таблица
CREATE TABLE users (
    id BIGINT, // INTEGER
    email VARCHAR
);
CREATE INDEX idx_user_id ON users(id);

// Запрос с неправильным типом
SELECT * FROM users WHERE id = '123'; // String, не INTEGER
// ← Может быть Seq Scan, потому что нужна конвертация

// Решение: правильный тип
SELECT * FROM users WHERE id = 123; // INTEGER
// ← Теперь Index Scan

// В Java (Spring Data JPA)
@Query("SELECT u FROM User u WHERE u.id = ?1")
User findById(Long id); // ← Правильно

// Плохо
@Query("SELECT u FROM User u WHERE u.id = CAST(?1 AS TEXT)")
User findById(Long id);

Причина 4: Условие WHERE использует OR

// Есть индексы на email и phone
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_phone ON users(phone);

// Запрос с OR может быть Seq Scan
SELECT * FROM users WHERE email = 'test@example.com' OR phone = '1234567890';
// ← Может быть Seq Scan

// Решение 1: Composite index (если часто ищем по обоим полям)
CREATE INDEX idx_email_phone ON users(email, phone);
// Может помочь в некоторых случаях

// Решение 2: Используем UNION вместо OR
SELECT * FROM users WHERE email = 'test@example.com'
UNION
SELECT * FROM users WHERE phone = '1234567890';
// ← Обе части используют индексы

// Решение 3: Check statistics (может быть, планировщик думает, что нужен Seq Scan)
ANALYZE users;

// Решение 4: Force index hint (если планировщик совсем неправ)
-- PostgreSQL не поддерживает INDEX HINT напрямую
-- Но можно использовать:
SET enable_seqscan = off; -- Только для этой session
SELECT * FROM users WHERE email = 'test' OR phone = '123';
SET enable_seqscan = on;

Причина 5: Статистика БД устарела

// Планировщик использует статистику для выбора плана
// Если статистика устарела, может выбрать неправильный индекс

// Диагностика
EXPLAIN SELECT * FROM large_table WHERE id = 1;
// Вывод: expected rows=500000, actual rows=1
// ← Статистика неправильная!

// Решение: ANALYZE обновляет статистику
ANALYZE large_table;

// Или для конкретного столбца
ANALYZE large_table(id);

// После этого план должен быть правильный

Шаг 4: Поиск неправильного плана (когда индекс есть, но не используется)

// Иногда планировщик имеет устаревшие/неправильные статистические данные
// и выбирает Seq Scan вместо индекса, хотя индекс есть

// Детальный анализ
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) 
SELECT * FROM users WHERE email = 'test@example.com';

// Посмотрим:
// - Filter (какой фильтр применяется)
// - Rows (сколько рядов планировщик думает, что будет)
// - Actual (сколько рядов на самом деле)

// Если:
// - estimated rows: 500000
// - actual rows: 1
// → Статистика неправильная, нужна ANALYZE

Шаг 5: Проверка, подходит ли индекс

// Индекс может быть создан, но быть неправильного типа

// 1. B-tree индекс (default)
CREATE INDEX idx_standard ON users(email); // B-tree
// Хорош для: =, <, >, BETWEEN

// 2. Hash индекс (только = )
CREATE INDEX idx_hash ON users(email) USING hash;
// Хорош для: только точных совпадений =

// 3. BRIN индекс (для очень больших таблиц с сортировкой)
CREATE INDEX idx_brin ON events(created_at) USING brin;
// Хорош для: range queries на больших отсортированных таблицах

// 4. GiST/GIN (для сложных типов)
CREATE INDEX idx_fulltext ON documents USING gin(to_tsvector(content));
// Хорош для: полнотекстовый поиск, массивы, JSON

// Если используешь WHERE email LIKE 'test%'
// B-tree индекс будет использован (хорош)
// Но если WHERE email LIKE '%test%'
// Индекс может не помочь, нужен полнотекстовый поиск

Шаг 6: В коде Java

// Если проблема на уровне приложения
@Service
@AllArgsConstructor
public class UserService {
    private final UserRepository repository;
    
    // ПЛОХО: может быть Seq Scan
    public User findByEmail(String email) {
        return repository.findAll().stream()
            .filter(u -> u.getEmail().equals(email))
            .findFirst()
            .orElse(null);
    }
    
    // ХОРОШО: используем БД для фильтрации
    @Query("SELECT u FROM User u WHERE u.email = ?1")
    public User findByEmail(String email);
    
    // ЕЩЁ ЛУЧШЕ: Spring находит метод автоматически
    public User findByEmail(String email);
    // Spring Data JPA создаст запрос SELECT u FROM User u WHERE u.email = ?1
}

// А в БД проверяем индекс
CREATE INDEX idx_user_email ON users(email);

Полный checklist отладки

public class IndexDebugChecklist {
    public static void debugIndexIssue(String table, String column) {
        // 1. Проверить план запроса
        // EXPLAIN SELECT * FROM table WHERE column = value;
        
        // 2. Проверить наличие индекса
        // SELECT * FROM pg_indexes WHERE tablename = 'table';
        
        // 3. Обновить статистику
        // ANALYZE table;
        
        // 4. Проверить, используется ли индекс
        // EXPLAIN SELECT * FROM table WHERE column = value;
        
        // 5. Если всё ещё Seq Scan:
        //    - Проверить условие WHERE (может быть функция?)
        //    - Проверить тип данных
        //    - Проверить размер таблицы
        //    - Может быть, Seq Scan эффективнее для маленькой таблицы
        
        // 6. Создать индекс если нужно
        // CREATE INDEX idx_name ON table(column);
        
        // 7. ANALYZE снова
        // ANALYZE table;
        
        // 8. Проверить план ещё раз
        // EXPLAIN ANALYZE SELECT * FROM table WHERE column = value;
    }
}

Реальный пример из production

// Проблема: запрос SELECT * FROM orders WHERE user_id = 123 работает 3 секунды

// 1. EXPLAIN показал Seq Scan
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
// Seq Scan on orders, time: 3000ms

// 2. Проверил индексы
SELECT * FROM pg_indexes WHERE tablename = 'orders';
// Результат: индекс на id, но не на user_id

// 3. Создал индекс
CREATE INDEX idx_orders_user_id ON orders(user_id);

// 4. ANALYZE
ANALYZE orders;

// 5. Проверил ещё раз
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
// Index Scan using idx_orders_user_id, time: 50ms ← 60x быстрее!

Итоговый алгоритм

public class IndexOptimizationAlgorithm {
    
    /**
     * Когда планировщик не использует индекс
     */
    public void fixIndexUsage() {
        // 1. Подтвердить проблему
        explainQuery(); // EXPLAIN показывает Seq Scan?
        
        // 2. Проверить индекс
        checkIndexExists(); // Индекс есть?
        
        // 3. Обновить статистику
        analyzeTable(); // ANALYZE
        
        // 4. Проверить условие
        validateWhereClause(); // Нет ли функций? Типы совпадают?
        
        // 5. Если индекса нет
        createIndex(); // CREATE INDEX
        
        // 6. Если индекс неправильного типа
        recreateIndex(); // DROP и CREATE с правильным USING
        
        // 7. Проверить результат
        explainQueryAgain(); // Теперь Index Scan?
    }
}