LowCardinality: словарное кодирование
Столбец country в таблице аналитических событий содержит ~200 уникальных значений, но миллиарды строк. Хранить строку ‘United States’ целиком в каждой из миллиарда строк — расточительство. LowCardinality решает эту проблему через dictionary encoding: повторяющиеся значения хранятся в словаре один раз, а в столбце — только индексы.
Как работает dictionary encoding
Без LowCardinality каждая строка хранит полное значение:
Строка 0: "Chrome" (6 байт + overhead)
Строка 1: "Firefox" (7 байт + overhead)
Строка 2: "Chrome" (6 байт + overhead)
Строка 3: "Safari" (6 байт + overhead)
Строка 4: "Chrome" (6 байт + overhead)
С LowCardinality(String) ClickHouse создаёт словарь и заменяет значения на индексы:
Результат:
- Хранение: вместо 6-15 байт строки — 1-2 байта индекса. Экономия 5-10x по объёму.
- Запросы: фильтрация
WHERE browser = 'Chrome'превращается в поиск индекса 0 в словаре, затем сравнение целых чисел вместо строковое сравнение. Ускорение до 10x на типичных аналитических запросах.
Когда LowCardinality эффективен
Правило: LowCardinality даёт максимальный эффект при менее 10 000 уникальных значений в столбце. Работает хорошо до ~100 000 уникальных значений. Выше этого порога словарь растёт, индексы требуют больше байт, и преимущество теряется.
Хорошие кандидаты для LowCardinality(String):
| Столбец | Уникальных значений | Эффект |
|---|---|---|
| country | ~200 | Отличный |
| browser_name | ~50 | Отличный |
| os_name | ~20 | Отличный |
| http_method | 7 (GET, POST, …) | Отличный |
| city | ~50 000 | Хороший |
| user_agent | миллионы | Плохой (не использовать) |
| миллионы | Плохой (не использовать) |
LowCardinality с более чем 100 000 уникальных значений может работать медленнее обычного String. Словарь перестаёт помещаться в кэш процессора, и dictionary lookup добавляет overhead вместо экономии. Проверьте кардинальность перед применением: SELECT uniq(column) FROM table.
Применение LowCardinality
Задать при создании таблицы:
CREATE TABLE events (
event_date Date,
country LowCardinality(String),
browser LowCardinality(String),
os LowCardinality(String),
url String -- высокая кардинальность, обычный String
) ENGINE = MergeTree()
ORDER BY (event_date, country)
Или изменить тип существующего столбца:
ALTER TABLE events MODIFY COLUMN country LowCardinality(String)
LowCardinality можно применить к нескольким базовым типам: String, FixedString, Date, DateTime, и числовым типам (Int, UInt, Float). Это не только строковая оптимизация.
LowCardinality vs Enum8/Enum16
Enum8 и Enum16 тоже хранят строки как числа, но с существенным отличием:
| Свойство | LowCardinality(String) | Enum8 / Enum16 |
|---|---|---|
| Добавление нового значения | Автоматически | ALTER TABLE … MODIFY COLUMN |
| Максимум значений | ~100 000 (практический) | 256 (Enum8) / 65536 (Enum16) |
| Валидация | Нет (любая строка) | Да (только объявленные) |
| Размер хранения | 1-2 байта (автоматически) | 1 байт (Enum8) / 2 байта (Enum16) |
| ORDER BY эффективность | По словарному порядку | По числовому значению |
Рекомендация: используйте LowCardinality(String) по умолчанию. Enum оправдан только когда нужна строгая валидация значений (например, статус заказа ‘pending’/‘paid’/‘shipped’/‘cancelled’ — новые значения не должны появляться без явного изменения схемы).
Влияние на запросы: числа с примером
Таблица events, 1 миллиард строк, столбец country (200 уникальных стран):
| Метрика | String | LowCardinality(String) | Разница |
|---|---|---|---|
| Размер столбца (несжатый) | ~12 ГБ | ~1.5 ГБ | 8x меньше |
| GROUP BY country | ~3.2 с | ~0.3 с | 10x быстрее |
| WHERE country = ‘RU’ | ~1.8 с | ~0.2 с | 9x быстрее |
Числа приблизительные и зависят от оборудования, но порядок ускорения стабильно воспроизводится.
Ключевые выводы
- LowCardinality заменяет значения индексами — словарь хранит уникальные значения, столбец — 1-2 байта целочисленных индексов. Ускорение запросов до 10x, сокращение хранения в 5-8 раз.
- Оптимальная кардинальность — до 10 000 уникальных значений. Работает до ~100 000. Выше — проверяйте на реальных данных.
- LowCardinality(String) вместо Enum — более гибкий (не нужен ALTER TABLE для новых значений), достаточно эффективный для большинства случаев.
- Проверяйте кардинальность перед применением:
SELECT uniq(column) FROM table. Высокая кардинальность (email, UUID) — противопоказание.