> Что делать, если после добавления индекса селективность низкая и запрос остается медленным? (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: InDrive

Стек: Go

> Пример ответа

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

Низкая селективность индекса означает, что индекс не сужает выборку достаточно сильно - например, при селективности 50% оптимизатор скорее выберет full scan. Нужно проверить статистику, состав индекса, типы данных и план запроса. Часто помогает покрывающий индекс, переписывание запроса, изменение порядка колонок в составном индексе или использование partial index. Если индекс действительно бесполезен - его нужно удалить, чтобы не тратить ресурсы на запись и память.

Подробное объяснение

Селективность - это отношение количества уникальных значений колонки к общему числу строк. Низкая селективность (например, 0.1-0.5) означает, что индекс вернет большой процент таблицы, и оптимизатор может отказаться от него в пользу sequential scan - это дешевле по числу page reads.

Причины низкой селективности:

  • колонка с малым числом уникальных значений (boolean, enum с 2-3 значениями)
  • составной индекс, где первая колонка низкоселективна, а высокоселективная стоит позже
  • устаревшая статистика - планировщик не знает реальное распределение данных
  • несоответствие типов: индекс по varchar, а в запросе передается число - индекс не используется
  • функциональные выражения: WHERE lower(name) = ... без functional index

Что делать:

  1. Проверить план через EXPLAIN ANALYZE - убедиться, что индекс вообще используется, и посмотреть оценку rows.
  2. Обновить статистику (ANALYZE в PostgreSQL, UPDATE STATISTICS в SQL Server).
  3. Перестроить индекс - иногда он фрагментирован.
  4. Изменить порядок колонок в составном индексе - высокоселективные колонки должны идти первыми.
  5. Добавить покрывающий индекс (INCLUDE / covering index) - чтобы не было table lookup.
  6. Использовать partial index для часто фильтруемого значения.
  7. Переписать запрос: добавить дополнительные условия, изменить JOIN order, использовать EXISTS вместо IN.
  8. Рассмотреть другие структуры: BRIN для больших таблиц с коррелированным распределением, hash index для равенства.

Важно понимать: индекс - не панацея. Если запрос фильтрует 30% таблицы, full scan может быть быстрее. Иногда правильное решение - не индекс, а изменение схемы (денормализация, партиционирование) или кэширование.

На практике

Типичный сценарий: есть таблица orders с колонкой status (значения: new, paid, shipped, cancelled). Запрос SELECT * FROM orders WHERE status = 'new' - селективность низкая, если 40% заказов в статусе new. Индекс по status бесполезен.

Что делаем:

  • если запрос всегда фильтрует по status = 'new' - создаем partial index: CREATE INDEX idx_orders_new ON orders(id) WHERE status = 'new'
  • если запросы разные - добавляем в индекс другие колонки: CREATE INDEX idx_orders_status_created ON orders(status, created_at) - тогда для WHERE status = 'new' ORDER BY created_at индекс полезен
  • если нужен только id - делаем covering index: CREATE INDEX idx_orders_status_covering ON orders(status) INCLUDE (id, total)

Еще пример: составной индекс (city, status) - если запросы всегда фильтруют по status, а city редко, индекс бесполезен. Меняем порядок на (status, city).

Для Go-разработчика важно: не пытаться решить проблему на уровне приложения (например, добавить time.Sleep или искусственное ограничение), а работать с БД. Также стоит проверить, не создает ли ORM (GORM, sqlx) запросы с неявными приведениями типов.

Пример кода

SQL
-- Плохой индекс: низкая селективность
CREATE INDEX idx_orders_status ON orders(status);
-- Partial index для конкретного значения
CREATE INDEX idx_orders_new ON orders(id, created_at) WHERE status = 'new';
-- Покрывающий индекс
CREATE INDEX idx_orders_status_covering ON orders(status) INCLUDE (total, created_at);
-- Составной индекс с правильным порядком
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
-- Проверка плана
EXPLAIN ANALYZE SELECT id, total FROM orders WHERE status = 'new' AND created_at > now() - interval '1 day';
GO
// В Go: не подменяем проблему БД кодом
// Плохо: вытащить все и фильтровать в памяти
rows, _ := db.Query("SELECT * FROM orders WHERE status = 'new'")
// фильтруем created_at в Go
// Хорошо: пусть БД делает свою работу
rows, _ := db.Query(`
SELECT id, total FROM orders
WHERE status = 'new' AND created_at > now() - interval '1 day'
`)

Как отвечать на собеседовании

Начни с определения селективности и почему она важна. Затем перечисли шаги диагностики: EXPLAIN, статистика, структура индекса. Обязательно упомяни, что низкая селективность - не всегда проблема: иногда full scan быстрее. Покажи понимание trade-off между скоростью чтения и стоимостью поддержки индекса. Приведи конкретный пример с partial index или covering index. В конце скажи, что решение зависит от паттерна запросов и распределения данных - нет универсального ответа.

Что проверяет интервьюер

  • понимание того, как оптимизатор принимает решение об использовании индекса
  • знание внутреннего устройства индексов (B-tree, page reads, cost model)
  • умение читать план запроса и видеть проблему
  • способность предложить несколько вариантов решения с обоснованием
  • понимание trade-off: индекс ускоряет SELECT, но замедляет INSERT/UPDATE
  • знание специфики конкретной СУБД (PostgreSQL, MySQL, etc.)

Типичные ошибки

  • предложение "просто добавить индекс" без анализа селективности
  • игнорирование статистики - сразу пересоздание индекса
  • непонимание разницы между селективностью колонки и селективностью индекса
  • утверждение, что индекс всегда ускоряет запрос
  • забывание про covering index, когда нужен только id
  • предложение использовать индекс для колонки с 2-3 уникальными значениями без дополнительных условий
  • отсутствие проверки через EXPLAIN после изменений

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью