Learning Platform
Глоссарий Troubleshooting
Урок 06.03 · 20 мин
Продвинутый
SelectivitySkip IndexEXPLAINData LocalityPerformance

Анализ селективности

Skip-индексы — мощный инструмент, но они не бесплатны. Создание skip-индекса на каждый столбец “на всякий случай” — распространённая ошибка, которая увеличивает время вставки и размер данных без выигрыша в запросах. В этом уроке мы разберём, когда skip-индексы действительно помогают, когда вредят, и как это измерить.


Когда skip-индексы помогают

Skip-индекс эффективен, когда выполняются три условия одновременно:

1. Фильтр отсекает большую долю гранул

Skip-индекс пропускает блоки гранул, где нужных данных нет. Если ваш фильтр отсекает 90% данных — skip-индекс пропустит большинство блоков. Если фильтр отсекает 5% — почти все блоки всё равно будут прочитаны.

-- Хороший кандидат: status='ERROR' встречается в 1% строк
-- Skip-индекс пропустит ~99% блоков
SELECT * FROM logs WHERE status = 'ERROR';

-- Плохой кандидат: status='OK' встречается в 95% строк
-- Skip-индекс пропустит максимум 5% блоков -- почти бесполезен
SELECT * FROM logs WHERE status = 'OK';

2. Хорошая data locality (значения сгруппированы)

Skip-индекс агрегирует информацию по блоку гранул. Если значение “ERROR” равномерно размазано по всем гранулам (в каждой грануле хотя бы один “ERROR”), то skip-индекс не сможет исключить ни одну гранулу, даже если “ERROR” составляет всего 1% строк.

Если же значения сгруппированы (например, все ошибки произошли в определённый временной интервал), skip-индекс эффективно исключит гранулы без ошибок.

TIP

Data locality напрямую связана с ORDER BY таблицы. Столбцы, коррелирующие с ORDER BY ключом (например, error_code коррелирует с timestamp — ошибки кластеризуются по времени), показывают лучшую data locality для skip-индексов.

3. Запросы предсказуемы и повторяются

Skip-индекс имеет смысл, если определённый паттерн фильтрации используется регулярно. Создавать skip-индекс для ad-hoc запроса, выполняемого раз в месяц, нерационально — overhead хранения и вставки перевесит выигрыш.


Когда skip-индексы вредят

Случай 1: случайное распределение данных

Если значения столбца распределены случайно по гранулам, skip-индекс не сможет исключить ни одну гранулу. Например, UUID или случайный hash в каждой грануле будет содержать разные значения — minmax покажет огромный диапазон, set(N) переполнится, bloom_filter будет срабатывать на каждом блоке.

-- Плохо: random_id распределён случайно по гранулам
-- minmax: min=1, max=1000000 в каждом блоке → ничего не пропускает
INDEX idx_random random_id TYPE minmax GRANULARITY 4

Случай 2: overhead превышает выигрыш

Каждый skip-индекс добавляет:

  • Время вставки: вычисление индекса при каждом INSERT
  • Хранение: файл индекса на каждый part
  • Merge overhead: пересчёт индекса при слиянии parts

Если запросы с фильтром по столбцу выполняются редко или сканируют большинство данных, overhead индекса превысит экономию на I/O.

Случай 3: слишком мелкая GRANULARITY

GRANULARITY=1 создаёт запись индекса на каждую гранулу — максимальная точность, но и максимальный размер индекса. Для таблиц с миллиардами строк это может означать миллионы записей skip-индекса, что само по себе замедляет обработку запросов.

Случай 4: skip-индекс на столбце, уже в ORDER BY

Если столбец входит в ORDER BY ключ, primary.idx уже обеспечивает pruning гранул для этого столбца. Добавлять skip-индекс бесполезно — primary key уже делает ту же работу.

-- Бессмысленно: event_date уже первый столбец ORDER BY
CREATE TABLE events (
    event_date Date,
    user_id UInt64,
    INDEX idx_date event_date TYPE minmax GRANULARITY 4
) ENGINE = MergeTree()
ORDER BY (event_date, user_id);

Мониторинг: EXPLAIN indexes=1

Главный инструмент для оценки эффективности skip-индекса — EXPLAIN с параметром indexes = 1:

EXPLAIN indexes = 1
SELECT count()
FROM events
WHERE status = 'ERROR';

Пример вывода:

Expression ((Project names + Projection))
  Aggregating
    Expression (Before GROUP BY)
      Filter (WHERE)
        ReadFromMergeTree (default.events)
        Indexes:
          PrimaryKey
            Keys: event_date
            Condition: true
            Parts: 8/8
            Granules: 100/100
          Skip
            Name: idx_status
            Description: set GRANULARITY 2
            Parts: 3/8
            Granules: 12/100

Как читать вывод:

  • PrimaryKey Parts: 8/8, Granules: 100/100 — primary key не отфильтровал ничего (100 из 100 гранул)
  • Skip Name: idx_status, Parts: 3/8, Granules: 12/100 — skip-индекс сократил чтение с 100 до 12 гранул. Пропущено 88 гранул — индекс работает эффективно.
WARNING

Если вывод показывает Granules: 100/100 после skip-индекса — индекс не помогает. Либо данные распределены случайно, либо выбран неподходящий тип индекса, либо GRANULARITY слишком крупная.


Мониторинг: system.part_log

Для мониторинга overhead skip-индексов используйте system.part_log:

-- Время merge с skip-индексами
SELECT
    table,
    event_type,
    duration_ms,
    rows,
    size_in_bytes
FROM system.part_log
WHERE table = 'events'
  AND event_type = 'MergeParts'
ORDER BY event_time DESC
LIMIT 10;

Если duration_ms значительно выросла после добавления skip-индексов, overhead может быть неоправданным.


Практический алгоритм принятия решения

Перед созданием skip-индекса задайте себе четыре вопроса:

  1. Столбец входит в ORDER BY? Если да — skip-индекс не нужен, primary key уже pruneит.
  2. Фильтр высокоселективен? Если нет (фильтр пропускает большинство строк) — skip-индекс бесполезен.
  3. Данные имеют data locality? Если значения равномерно размазаны по гранулам — skip-индекс не сможет ничего пропустить.
  4. Запрос выполняется регулярно? Если нет — overhead хранения не окупится.

Если все четыре ответа положительные — создайте skip-индекс. Выбор типа:

  • Диапазонные фильтры (>, <, BETWEEN) -> minmax
  • Равенство на low-cardinality -> set(N)
  • Равенство/IN на high-cardinality -> bloom_filter
  • Текстовый поиск -> text (не ngrambf_v1/tokenbf_v1)

Ключевые выводы

  1. Skip-индекс помогает, когда: фильтр высокоселективен, данные имеют хорошую data locality, запрос повторяется регулярно.
  2. Skip-индекс вредит при случайном распределении данных, слишком мелкой GRANULARITY, или на столбце, уже входящем в ORDER BY.
  3. EXPLAIN indexes = 1 — основной инструмент для проверки: Granules: 12/100 означает 88% пропущенных гранул.
  4. Если Granules: X/X (X = total), skip-индекс не работает — удалите его.
  5. Всегда проверяйте эффективность после создания индекса, а не предполагайте.
Selectivity в PostgreSQL: как оптимизатор оценивает WHERE Parquet: column statistics, min/max pruning и row group skipping

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 4. Столбец session_id (UUID) содержит случайные значения, равномерно распределённые по гранулам. Создан skip-индекс INDEX idx_session session_id TYPE bloom_filter GRANULARITY 2. EXPLAIN показывает: Granules: 500/500. Почему индекс не пропускает ни одной гранулы?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7