> Какой индекс построить для запроса с условиями на несколько столбцов (Go)

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

Компании: Ozon

Стек: Go

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

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

Для запроса с условиями на несколько столбцов оптимальный вариант - композитный (составной) B-tree индекс, где порядок столбцов определяется селективностью и паттерном запросов. Сначала ставятся столбцы с условиями равенства (=), затем - с диапазонными условиями (>, <, BETWEEN). Если все условия равенства - порядок менее критичен, но важно учитывать покрытие запроса индексом (covering index) для избежания table lookup.

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

Композитный индекс работает как упорядоченная структура по всем включённым столбцам. Ключевое правило - левосторонний префикс: индекс эффективен, если условия в запросе покрывают столбцы слева направо без пропусков. Например, индекс (a, b, c) эффективен для WHERE a = ? AND b = ?, но не для WHERE b = ? AND c = ?.

Порядок столбцов определяется:

  1. Селективность - более селективные столбцы (с большим количеством уникальных значений) ставятся первыми при условиях равенства, чтобы быстрее сузить диапазон сканирования.
  2. Тип условия - столбцы с = всегда идут раньше столбцов с диапазоном, потому что после первого диапазонного условия индекс перестаёт быть эффективным для последующих столбцов.
  3. Сортировка - если запрос содержит ORDER BY, индекс может покрыть сортировку, если порядок столбцов в индексе совпадает с порядком сортировки.

Для WHERE a = ? AND b BETWEEN ? AND ? индекс (a, b) идеален: сначала точное совпадение по a, затем диапазон по b - это один последовательный скан листьев.

Если запрос выбирает только столбцы, входящие в индекс, можно использовать covering index - тогда не потребуется обращение к таблице (index-only scan). Это особенно важно в Go-приложениях с высоким RPS, где каждый лишний disk read критичен.

Для OR-условий композитный индекс обычно не помогает - лучше использовать UNION ALL или отдельные индексы (в PostgreSQL - bitmap OR).

На практике

В PostgreSQL и MySQL синтаксис одинаков: CREATE INDEX idx_name ON table (col1, col2). В Go-приложениях миграции обычно выполняются через golang-migrate или goose.

Практический подход:

  • Сначала соберите реальные запросы из продакшена (через pg_stat_statements или slow query log).
  • Для каждого запроса определите паттерн условий: равенство, диапазон, сортировка.
  • Сгруппируйте запросы по общим префиксам столбцов - один индекс может обслуживать несколько запросов.
  • Проверяйте план через EXPLAIN ANALYZE: ищите Index Scan вместо Seq Scan, обращайте внимание на Rows Removed by Index Recheck.

Для Go-сервисов с типичным CRUD: если есть таблица orders с фильтрацией по user_id и created_at, индекс (user_id, created_at) покроет запросы "все заказы пользователя за период" и "последние заказы пользователя".

Пример кода

SQL
-- Таблица заказов
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Запрос: все заказы пользователя по статусу за период
-- WHERE user_id = ? AND status = ? AND created_at BETWEEN ? AND ?
CREATE INDEX idx_orders_user_status_created
ON orders (user_id, status, created_at);
-- Запрос: последние заказы пользователя (сортировка)
-- WHERE user_id = ? ORDER BY created_at DESC LIMIT 10
-- Этот же индекс работает, так как created_at - последний столбец

В Go-коде запрос остаётся стандартным:

GO
rows, err := db.Query(ctx, `
SELECT id, status, created_at
FROM orders
WHERE user_id = $1 AND status = $2 AND created_at BETWEEN $3 AND $4
ORDER BY created_at DESC
`, userID, status, from, to)

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

Начните с главного: композитный B-tree индекс, порядок столбцов - сначала равенство, потом диапазон. Затем объясните левосторонний префикс и почему нельзя пропускать столбцы. Приведите пример из реальной практики: таблица заказов, индекс (user_id, created_at). Упомяните covering index как оптимизацию для частых запросов. Если спросят про OR - честно скажите, что композитный индекс не поможет, нужен bitmap OR или UNION. Завершите упоминанием EXPLAIN ANALYZE для проверки.

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

  • Понимание внутренней структуры B-tree и принципа левостороннего префикса.
  • Умение определять порядок столбцов на основе типов условий (равенство vs диапазон).
  • Знание trade-off: лишние индексы замедляют запись, поэтому один композитный индекс лучше нескольких одиночных.
  • Практический опыт: умение читать план запроса и видеть, использует ли СУБД индекс.
  • Понимание covering index и index-only scan.

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

  • Ставить столбцы в порядке, указанном в WHERE, без учёта селективности и типа условия.
  • Создавать отдельные индексы на каждый столбец - СУБД сможет использовать только один из них для одного запроса (кроме bitmap-сканов).
  • Игнорировать ORDER BY - если индекс не покрывает сортировку, будет дополнительная сортировка.
  • Забывать про стоимость поддержки индекса при INSERT/UPDATE - каждый индекс замедляет запись.
  • Считать, что индекс с диапазоном по первому столбцу эффективен для последующих - это не так, после первого диапазона остальные столбцы бесполезны.

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

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