Типы skip-индексов
Primary key в ClickHouse (sparse index) позволяет эффективно pruneить гранулы по префиксу ORDER BY ключа. Но что делать, когда фильтр по столбцу, который не входит в ORDER BY? Для этого существуют skip-индексы (data skipping indexes) — вторичные индексы, которые хранят агрегированную информацию о блоках гранул и позволяют пропускать заведомо не подходящие блоки.
Skip-индекс НЕ указывает на конкретную строку. Он говорит: “в этих N гранулах точно нет значения X” или “в этих N гранулах значения лежат в диапазоне [A, B]”. Если skip-индекс определяет, что блок не содержит нужных данных, ClickHouse пропускает его целиком.
GRANULARITY таблицы vs GRANULARITY skip-индекса
Прежде чем создавать skip-индексы, нужно чётко понять два разных значения слова GRANULARITY:
GRANULARITY таблицы (index_granularity = 8192 строк на гранулу) — это количество строк в одной грануле. Это свойство таблицы, определяющее размер минимального блока чтения. Каждая гранула — 8192 строки (по умолчанию).
GRANULARITY skip-индекса (например, GRANULARITY 4) — это количество гранул в одном блоке индекса. Одна запись skip-индекса агрегирует информацию о N гранулах.
GRANULARITY таблицы (index_granularity = 8192 строк на гранулу) — это НЕ то же самое, что GRANULARITY skip-индекса (количество гранул на один блок индекса). GRANULARITY=1 означает запись индекса на каждую гранулу (самый точный). GRANULARITY=4 — одна запись на 4 гранулы (грубее, компактнее).
При index_granularity=8192 и skip-индексе с GRANULARITY=4:
- 1 блок skip-индекса = 4 гранулы = 4 * 8192 = 32 768 строк
- Если индекс определяет, что блок не содержит нужных данных, пропускаются все 32 768 строк
При GRANULARITY=1:
- 1 блок skip-индекса = 1 гранула = 8192 строк
- Максимальная точность, но больший размер индекса
Синтаксис создания skip-индекса
CREATE TABLE events (
event_date Date,
user_id UInt64,
status String,
INDEX idx_status status TYPE set(100) GRANULARITY 2
) ENGINE = MergeTree()
ORDER BY (event_date, user_id);
Также можно добавить skip-индекс к существующей таблице:
ALTER TABLE events ADD INDEX idx_status status TYPE set(100) GRANULARITY 2;
-- Индекс создаётся только для новых данных.
-- Для существующих данных:
ALTER TABLE events MATERIALIZE INDEX idx_status;
Пять типов skip-индексов
ClickHouse поддерживает пять типов skip-индексов. Каждый хранит разную агрегированную информацию о блоке гранул:
1. minmax
Хранит минимальное и максимальное значение столбца в каждом блоке гранул. Идеален для диапазонных запросов на числовых и временных столбцах.
CREATE TABLE metrics (
timestamp DateTime,
host String,
cpu_usage Float32,
INDEX idx_cpu cpu_usage TYPE minmax GRANULARITY 4
) ENGINE = MergeTree()
ORDER BY (host, timestamp);
-- Запрос: ClickHouse пропустит блоки, где max(cpu_usage) < 90
SELECT * FROM metrics WHERE cpu_usage > 90;
Ложных срабатываний нет: если искомое значение вне диапазона [min, max] блока, блок гарантированно не содержит нужных строк.
2. set(N)
Хранит до N уникальных значений столбца в блоке. Подходит для равенственных фильтров на столбцах с ограниченной кардинальностью.
CREATE TABLE logs (
timestamp DateTime,
level String,
message String,
INDEX idx_level level TYPE set(10) GRANULARITY 2
) ENGINE = MergeTree()
ORDER BY timestamp;
-- Запрос: пропустит блоки, где level='ERROR' не встречается
SELECT * FROM logs WHERE level = 'ERROR';
Если в блоке больше N уникальных значений, skip-индекс для этого блока просто не работает (не пропускает его). Ложных срабатываний нет.
3. bloom_filter
Хранит Bloom-фильтр для блока. Подходит для равенственных фильтров и IN на высококардинальных столбцах (user_id, request_id, session_id).
CREATE TABLE events (
event_date Date,
user_id UInt64,
event_type String,
INDEX idx_user user_id TYPE bloom_filter(0.01) GRANULARITY 3
) ENGINE = MergeTree()
ORDER BY event_date;
-- Запрос: Bloom-фильтр проверяет, может ли user_id=12345 быть в блоке
SELECT * FROM events WHERE user_id = 12345;
Ложные срабатывания возможны: Bloom-фильтр может ответить “возможно есть”, когда на самом деле значения нет (false positive). Параметр fp_rate (по умолчанию 0.025) определяет вероятность ложного срабатывания. Чем ниже — тем больше памяти занимает фильтр.
4. ngrambf_v1 (устаревший)
Bloom-фильтр n-грамм для подстрочного поиска. Разбивает текст на подстроки длиной n символов.
CREATE TABLE logs (
timestamp DateTime,
message String,
INDEX idx_message message TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 1
) ENGINE = MergeTree()
ORDER BY timestamp;
-- Подстрочный поиск
SELECT * FROM logs WHERE message LIKE '%timeout%';
ngrambf_v1 считается устаревшим. Для текстового поиска рекомендуется использовать text index (inverted index), который является GA-фичей и работает в 7-10 раз быстрее на холодных запросах.
5. tokenbf_v1 (устаревший)
Bloom-фильтр токенов для поиска по словам. Разбивает текст по не-алфавитно-цифровым разделителям.
CREATE TABLE logs (
timestamp DateTime,
message String,
INDEX idx_tokens message TYPE tokenbf_v1(256, 2, 0) GRANULARITY 1
) ENGINE = MergeTree()
ORDER BY timestamp;
-- Токенный поиск
SELECT * FROM logs WHERE hasToken(message, 'error');
tokenbf_v1 считается устаревшим. Используйте text index вместо него — он обеспечивает детерминированный результат без ложных срабатываний.
Матрица поддерживаемых функций
Не каждый тип skip-индекса поддерживает каждую операцию:
| Функция | minmax | set | bloom_filter |
|---|---|---|---|
= | Да | Да | Да |
!= | Да | Да | Да |
< / > / <= / >= | Да | Нет | Нет |
IN | Да | Да | Да |
LIKE | Нет | Да | Нет |
hasToken | Нет | Нет | Нет |
Для полнотекстового поиска (LIKE '%pattern%' и hasToken()) используйте text index (урок 02) — он заменяет ngrambf_v1 и tokenbf_v1 с лучшей производительностью и без ложных срабатываний.
Проверка эффективности skip-индекса
Чтобы убедиться, что skip-индекс действительно пропускает блоки:
EXPLAIN indexes = 1
SELECT count()
FROM events
WHERE user_id = 12345;
Вывод покажет строку вида:
Skip
Name: idx_user
Description: bloom_filter GRANULARITY 3
Parts: 4/8
Granules: 12/100
Granules: 12/100 означает: из 100 гранул skip-индекс пропустил 88, читаются только 12. Чем меньше отношение — тем эффективнее индекс.
Ключевые выводы
- Skip-индексы — вторичные индексы для столбцов вне ORDER BY. Они не указывают на строку, а говорят “в этом блоке точно нет нужных данных”.
- GRANULARITY таблицы (8192 строки) — не путать с GRANULARITY skip-индекса (количество гранул на блок индекса).
- minmax и set(N) — детерминированные (нет ложных срабатываний). bloom_filter — вероятностный.
- ngrambf_v1 и tokenbf_v1 устарели — используйте text index (урок 02).
- Всегда проверяйте эффективность через
EXPLAIN indexes = 1.