Специализированные типы данных
Помимо числовых типов и строк, ClickHouse предлагает специализированные типы, оптимизированные для конкретных данных. Каждый из них экономит хранение, добавляет валидацию или открывает доступ к специфическим функциям.
Enum8 и Enum16: типовая безопасность
Enum хранит строковые значения как целые числа, но в отличие от LowCardinality, обеспечивает строгую валидацию — ClickHouse отклонит INSERT с неизвестным значением.
CREATE TABLE orders (
status Enum8('pending' = 1, 'paid' = 2, 'shipped' = 3, 'cancelled' = 4)
) ENGINE = MergeTree()
ORDER BY tuple()
-- Работает
INSERT INTO orders VALUES ('paid')
-- Ошибка: Unknown element 'refunded' for enum
INSERT INTO orders VALUES ('refunded')
| Тип | Размер | Максимум значений | Применение |
|---|---|---|---|
| Enum8 | 1 байт | 256 | Статусы, категории, флаги |
| Enum16 | 2 байта | 65 536 | Большие справочники |
Главный недостаток: добавление нового значения требует ALTER TABLE:
ALTER TABLE orders MODIFY COLUMN status
Enum8('pending'=1, 'paid'=2, 'shipped'=3, 'cancelled'=4, 'refunded'=5)
Если значения добавляются часто, используйте LowCardinality(String) вместо Enum.
UUID: 16 байт вместо 36
Тип UUID хранит 128-битный идентификатор в 16 байт (бинарно), а не как строку из 36 символов (‘550e8400-e29b-41d4-a716-446655440000’):
-- String UUID: ~36 байт + length overhead
-- UUID тип: ровно 16 байт
SELECT generateUUIDv4() AS id, toTypeName(id) AS type
UUID как первый столбец ORDER BY — антипаттерн для аналитических таблиц. UUID v4 генерируется случайно, что уничтожает locality данных в sparse index. Гранулы не могут быть эффективно пропущены, потому что соседние UUID не коррелируют по времени или по пользователю.
Сравнение ORDER BY с UUID и без:
-- Плохо: UUID первым в ORDER BY
-- Sparse index не может пропускать гранулы -- полный scan
CREATE TABLE events (
id UUID,
event_date Date,
user_id UInt64
) ENGINE = MergeTree()
ORDER BY (id, event_date)
-- Хорошо: event_date первым, UUID только для уникальности
CREATE TABLE events (
id UUID,
event_date Date,
user_id UInt64
) ENGINE = MergeTree()
ORDER BY (event_date, user_id, id)
UUID полезен как столбец данных (для дедупликации, внешних ссылок), но не как ведущий столбец сортировки.
IPv4 и IPv6: нативная экономия
Хранение IP-адреса как String расходует ~11-15 байт на значение (‘192.168.1.1’ = 11 символов, ‘255.255.255.255’ = 15 символов). Нативные типы значительно компактнее:
Экономия на строку: IPv4 (4 байта) вместо String (~13 байт в среднем) = ~9 байт. При 1 миллиарде строк = ~9 ГБ экономии на одном столбце.
Нативные функции для работы с IP:
-- Преобразование строки в IPv4
SELECT IPv4StringToNum('192.168.1.1') AS ip_num
-- Результат: 3232235777
-- Обратное преобразование
SELECT IPv4NumToString(3232235777) AS ip_str
-- Результат: '192.168.1.1'
-- CIDR-фильтрация
SELECT * FROM logs
WHERE IPv4CIDRToRange(ip, 24).1 = IPv4StringToNum('10.0.0.0')
Decimal: точные десятичные вычисления
Decimal типы хранят числа с фиксированной точностью без потери значащих цифр. Параметр S определяет количество знаков после запятой:
| Тип | Размер | Всего цифр | Диапазон (при S=2) | Применение |
|---|---|---|---|---|
| Decimal32(S) | 4 байта | 9 | до 9 999 999.99 | Цены в рублях |
| Decimal64(S) | 8 байт | 18 | до 9 999 999 999 999 999.99 | Финансовые суммы |
| Decimal128(S) | 16 байт | 38 | до 10^36 | Криптовалюта (18 decimals) |
| Decimal256(S) | 32 байта | 76 | до 10^74 | Научные вычисления |
Операции между Decimal и Float запрещены — ClickHouse требует явного приведения типа. Это защищает от случайной потери точности.
Nested: синтаксический сахар для параллельных массивов
Nested выглядит как вложенная структура, но на самом деле это параллельные массивы:
CREATE TABLE events (
event_date Date,
tags Nested(
name String,
value String
)
) ENGINE = MergeTree()
ORDER BY event_date
ClickHouse разворачивает Nested в два отдельных столбца:
tags.name— Array(String)tags.value— Array(String)
INSERT INTO events VALUES (
'2024-01-15',
['region', 'env'], -- tags.name
['eu-west', 'production'] -- tags.value
)
Настройка FLATTEN_NESTED=1 (по умолчанию) разворачивает Nested в отдельные Array-столбцы. При FLATTEN_NESTED=0 Nested хранится как единый Array(Tuple(name String, value String)), что сохраняет связь между полями. Проверьте текущее значение: SELECT getSetting('flatten_nested').
Map и Tuple
Map(K, V) — ключ-значение пары для semi-structured данных:
CREATE TABLE events (
metadata Map(String, String)
) ENGINE = MergeTree()
ORDER BY tuple()
INSERT INTO events VALUES ({'source': 'api', 'version': '2.1'})
SELECT metadata['source'] FROM events
Map медленнее выделенных столбцов (key lookup на каждую строку), но подходит для столбцов с непредсказуемой схемой (теги, метаданные, HTTP-заголовки).
Tuple(T1, T2, …) — фиксированный набор типов:
-- Tuple как тип столбца
CREATE TABLE geo (
location Tuple(latitude Float64, longitude Float64)
) ENGINE = MergeTree()
ORDER BY tuple()
-- Tuple в AggregateFunction (в AggregatingMergeTree)
-- Состояние агрегатной функции хранится как Tuple внутри
Tuple используется для составных значений с фиксированной структурой и как внутренний формат для состояний агрегатных функций.
Сводная таблица специализированных типов
Ключевые выводы
- Enum8 (1 байт) для закрытых множеств — строгая валидация, но требует ALTER TABLE для новых значений. Если значения часто добавляются, используйте LowCardinality(String).
- UUID не ведущий столбец ORDER BY — случайность UUID v4 уничтожает sparse index locality. Используйте UUID для данных, не для сортировки.
- IPv4 (4 байта) вместо String (~13 байт) — экономия ~9 ГБ на миллиард строк. Нативные функции для CIDR-фильтрации и преобразований.
- Decimal для денег, Float для метрик — Decimal64(2) гарантирует точность до копейки. Float64 теряет значащие цифры.
- Nested — параллельные массивы, не вложенные документы. Map — для semi-structured данных. Tuple — для фиксированных составных значений.