> Что такое селективность индекса и почему не всегда индекс используется? (Go)

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

Компании: Сбер, Employcity

Стек: Go

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

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

Селективность индекса - это доля строк таблицы, которые отбирает запрос при использовании индекса. Высокая селективность означает, что индекс отбирает мало строк (например, 1-5%), низкая - много (например, 50% и более). Индекс не используется, когда оптимизатор считает, что чтение таблицы целиком (full scan) дешевле, чем чтение индекса плюс обращения к таблице. Это происходит при низкой селективности, малом размере таблицы или когда запрос возвращает большую часть строк.

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

Селективность вычисляется как отношение количества уникальных значений в колонке к общему количеству строк. Например, колонка id с уникальными значениями имеет селективность 1.0 (максимальная), а колонка is_active с двумя значениями - около 0.5 (низкая).

Оптимизатор оценивает стоимость плана выполнения:

  • при использовании индекса - стоимость чтения индексных страниц + стоимость random access к таблице (для каждого найденного rowid);
  • при full scan - стоимость последовательного чтения всех страниц таблицы.

Если индекс отбирает 30% строк, random access к 30% страниц таблицы часто дороже, чем последовательное чтение всех страниц. Поэтому оптимизатор выбирает full scan.

Также индекс не используется, если:

  • колонка участвует в выражении или функции (например, WHERE year(created_at) = 2023);
  • в запросе используется LIKE '%text%' (префиксный wildcard);
  • оптимизатор считает таблицу слишком маленькой (меньше нескольких страниц);
  • индекс не покрывает запрос, и требуется много обращений к таблице.

В PostgreSQL и MySQL оптимизатор принимает решение на основе статистики (гистограммы, количество страниц, плотность данных). Если статистика устарела, решение может быть неоптимальным.

На практике

Для backend-разработчика на Go важно понимать, что селективность - это не свойство индекса, а свойство пары "индекс + конкретный запрос". Один и тот же индекс может быть высокоселективным для одного запроса и низкоселективным для другого.

Практические следствия:

  • не создавать индексы на колонках с малым количеством уникальных значений (булевы, enum с 2-3 значениями);
  • для составных индексов ставить первой колонку с наибольшей селективностью;
  • использовать EXPLAIN для проверки, использует ли запрос индекс;
  • в Go-приложениях следить за тем, чтобы параметры запросов не приводили к неожиданным full scan (например, передача пустого фильтра).

Пример: запрос WHERE status = 'active' при 90% активных записей не будет использовать индекс по status - это нормально. А вот WHERE user_id = ? при миллионе пользователей - будет.

Пример кода

SQL
-- таблица с низкой селективностью колонки status
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status TEXT NOT NULL,
user_id BIGINT NOT NULL
);
-- индекс на status - низкая селективность, часто не используется
CREATE INDEX idx_orders_status ON orders(status);
-- индекс на user_id - высокая селективность, используется
CREATE INDEX idx_orders_user ON orders(user_id);
-- запрос, который не будет использовать idx_orders_status
EXPLAIN SELECT * FROM orders WHERE status = 'completed';
-- вероятно, Seq Scan, если completed составляет большую часть строк
-- запрос, который будет использовать idx_orders_user
EXPLAIN SELECT * FROM orders WHERE user_id = 12345;
-- Index Scan, так как user_id уникален для большинства записей

В Go-коде это проявляется так:

GO
// плохо: фильтр по низкоселективной колонке
rows, _ := db.Query("SELECT * FROM orders WHERE status = $1", "completed")
// хорошо: фильтр по высокоселективной колонке
rows, _ := db.Query("SELECT * FROM orders WHERE user_id = $1 AND status = $2", userID, "completed")

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

Начни с определения селективности как доли строк, отбираемых запросом. Затем объясни, что оптимизатор сравнивает стоимость index scan и full scan. Приведи пример: колонка с двумя значениями - индекс бесполезен, колонка с уникальными значениями - полезен. Упомяни, что решение зависит от статистики и размера таблицы. Если спросят про Go - скажи, что разработчик должен проверять планы запросов через EXPLAIN и не полагаться на индексы, которые "вроде бы должны работать". Заверши практическим советом: для составных индексов первая колонка - самая селективная.

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

  • понимание разницы между селективностью индекса и селективностью колонки;
  • знание факторов, влияющих на выбор плана (стоимость random access, размер таблицы, статистика);
  • умение объяснить, почему WHERE status = 'active' при 90% активных строк не использует индекс;
  • понимание роли EXPLAIN в диагностике;
  • способность связать теорию с практикой backend-разработки (Go, SQL).

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

  • утверждение, что индекс не используется "потому что он плохой" - индекс не бывает плохим, он бывает неподходящим для запроса;
  • игнорирование стоимости random access - даже при селективности 10% index scan может быть дороже full scan;
  • забывание про покрывающие индексы (covering index) - если индекс покрывает запрос, он может использоваться даже при низкой селективности;
  • утверждение, что индекс всегда ускоряет запрос - для маленьких таблиц full scan быстрее;
  • отсутствие упоминания статистики и устаревших ANALYZE данных.

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

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