Learning Platform
Глоссарий Troubleshooting
Урок 03.04 · 20 мин
Средний
EnumUUIDIPv4IPv6DecimalNestedMapTuple

Специализированные типы данных

Помимо числовых типов и строк, 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')
ТипРазмерМаксимум значенийПрименение
Enum81 байт256Статусы, категории, флаги
Enum162 байта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
WARNING

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 символов). Нативные типы значительно компактнее:

Сравнение типов для хранения IP-адресов
String: 11-15+ байтString: хранит IP как текст. '192.168.1.1' = 11 байт + length prefix. '255.255.255.255' = 15 байт + length prefix. Нет валидации -- можно записать 'not-an-ip'. Сравнение строковое, медленное.
IPv4: 4 байтаIPv4: хранит адрес как UInt32 (4 байта). Автоматическая валидация -- ClickHouse отклонит невалидный IP. Нативные функции: IPv4NumToString, IPv4StringToNum, IPv4CIDRToRange. Сравнение числовое, быстрое.
IPv6: 16 байтIPv6: хранит адрес как FixedString(16) (16 байт). Поддерживает и IPv6, и IPv4-mapped адреса (::ffff:192.168.1.1). Нативные функции: IPv6NumToString, IPv6StringToNum. Занимает столько же, сколько UUID.

Экономия на строку: 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
)
TIP

Настройка 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 байтEnum8: 1 байт (Int8 внутри). До 256 значений. Строгая валидация -- ClickHouse отклонит неизвестное значение. Требует ALTER TABLE для добавления нового значения. Используйте для закрытых множеств: статус заказа, тип события.
Enum16: 2 байтаEnum16: 2 байта (Int16 внутри). До 65536 значений. Та же строгая валидация, что и Enum8. Редко нужен -- если более 256 значений, рассмотрите LowCardinality(String) вместо Enum16.
UUID: 16 байтUUID: 16 байт (128 бит). Бинарное хранение вместо 36-байтной строки. Не использовать как ведущий столбец ORDER BY -- случайность UUID v4 уничтожает locality в sparse index.
IPv4: 4 байтаIPv4: 4 байта (UInt32 внутри). Экономия ~10 байт по сравнению со String. Нативные функции IPv4StringToNum, IPv4CIDRToRange. Валидация при вставке.
IPv6: 16 байтIPv6: 16 байт (FixedString(16) внутри). Поддерживает IPv4-mapped адреса. Нативные функции IPv6StringToNum, IPv6NumToString.
Decimal64: 8 байтDecimal64(S): 8 байт. До 18 значащих цифр. S -- количество знаков после запятой. Используйте для денежных сумм, где Float64 теряет точность.
Nested: Array-параNested: синтаксический сахар. Разворачивается в параллельные Array-столбцы (при FLATTEN_NESTED=1). Nested(name String, value String) = name Array(String) + value Array(String).
Map: key-valueMap(K,V): ключ-значение. Медленнее выделенных столбцов (key lookup на каждую строку). Подходит для metadata, tags, HTTP headers -- данных с непредсказуемой схемой.

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

  1. Enum8 (1 байт) для закрытых множеств — строгая валидация, но требует ALTER TABLE для новых значений. Если значения часто добавляются, используйте LowCardinality(String).
  2. UUID не ведущий столбец ORDER BY — случайность UUID v4 уничтожает sparse index locality. Используйте UUID для данных, не для сортировки.
  3. IPv4 (4 байта) вместо String (~13 байт) — экономия ~9 ГБ на миллиард строк. Нативные функции для CIDR-фильтрации и преобразований.
  4. Decimal для денег, Float для метрик — Decimal64(2) гарантирует точность до копейки. Float64 теряет значащие цифры.
  5. Nested — параллельные массивы, не вложенные документы. Map — для semi-structured данных. Tuple — для фиксированных составных значений.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Инженер использует UUID как первый столбец ORDER BY для таблицы с 1 миллиардом строк. Как это влияет на sparse index?

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

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

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

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