Learning Platform
Глоссарий Troubleshooting
Урок 08.05 · 25 мин
Средний
NestedFlattenMapJSONEvent DataARRAY JOINSchema Design

Nested, Map и плоские схемы для событий

Event data — один из самых частых случаев в аналитике: клики, покупки, API-вызовы, телеметрия. Каждое событие несёт набор атрибутов (properties), и главный вопрос моделирования: как хранить эти атрибуты?

В ClickHouse четыре основных подхода: Nested, плоская схема (flattened), Map, и JSON type. У каждого свои trade-offs.


Подход 1: Nested type (параллельные массивы)

Nested — специальный тип данных ClickHouse, хранящий вложенные структуры как параллельные массивы одинаковой длины:

CREATE TABLE events_nested (
    event_id UInt64,
    timestamp DateTime,
    event_type LowCardinality(String),
    properties Nested(
        key String,
        value String
    )
) ENGINE = MergeTree()
ORDER BY (event_type, timestamp);

На уровне хранения ClickHouse разворачивает Nested в отдельные столбцы:

  • properties.key — Array(String)
  • properties.value — Array(String)

Эти массивы всегда одинаковой длины (по строке).

Вставка данных

INSERT INTO events_nested VALUES
    (1, '2024-01-15 10:00:00', 'click',
     ['device', 'browser', 'country'],
     ['mobile', 'safari', 'DE']),
    (2, '2024-01-15 10:01:00', 'purchase',
     ['device', 'amount', 'currency'],
     ['desktop', '99.99', 'EUR']);

Чтение: ARRAY JOIN для развёртывания

-- Развернуть Nested в строки (key-value пары)
SELECT event_id, event_type, p.key, p.value
FROM events_nested
ARRAY JOIN properties AS p;

Результат: каждый элемент массива становится отдельной строкой (unnesting). Это аналог UNNEST в PostgreSQL.

flatten_nested: настройка поведения

По умолчанию ClickHouse “распрямляет” Nested в отдельные массивы (flatten_nested=1). Для настоящего вложенного поведения (sub-columns как единая структура):

SET flatten_nested = 0;

С flatten_nested=0 Nested ведёт себя как Array(Tuple(key String, value String)) — единая структура, а не набор независимых массивов.

WARNING

flatten_nested=0 — глобальная или session-level настройка. Перед созданием таблиц с Nested убедитесь, что она установлена. Без неё INSERT в Nested с несколькими полями может давать неожиданные результаты.


Подход 2: Плоская схема (Flattened)

Если набор атрибутов фиксирован и заранее известен, плоская схема — самый производительный вариант:

CREATE TABLE events_flat (
    event_id UInt64,
    timestamp DateTime,
    event_type LowCardinality(String),
    -- Фиксированные атрибуты как отдельные столбцы
    device_type LowCardinality(String),
    browser LowCardinality(String),
    country LowCardinality(String),
    url String,
    amount Decimal64(2) DEFAULT 0,
    currency LowCardinality(String) DEFAULT ''
) ENGINE = MergeTree()
ORDER BY (event_type, timestamp);

Преимущества:

  • Прямой доступ к столбцу (WHERE country = 'DE') без ARRAY JOIN
  • Полная поддержка индексов (primary key, skip indexes) на каждом столбце
  • Оптимальное сжатие: LowCardinality, кодеки применяются к каждому столбцу индивидуально
  • Самая высокая производительность запросов

Недостаток: при добавлении нового атрибута — ALTER TABLE ADD COLUMN. При десятках новых атрибутов в месяц это становится неудобным.


Подход 3: Map(K, V) для переменных атрибутов

Когда набор ключей заранее неизвестен или часто расширяется, Map предлагает гибкость:

CREATE TABLE events_map (
    event_id UInt64,
    timestamp DateTime,
    event_type LowCardinality(String),
    -- Переменные атрибуты в Map
    properties Map(LowCardinality(String), String)
) ENGINE = MergeTree()
ORDER BY (event_type, timestamp);

Вставка:

INSERT INTO events_map VALUES
    (1, '2024-01-15 10:00:00', 'click',
     {'device': 'mobile', 'browser': 'safari', 'country': 'DE'}),
    (2, '2024-01-15 10:01:00', 'purchase',
     {'device': 'desktop', 'amount': '99.99', 'currency': 'EUR',
      'promo_code': 'WINTER24'});

Доступ к ключам:

-- Обращение по ключу
SELECT event_id, properties['device'] AS device
FROM events_map
WHERE properties['country'] = 'DE';

Map хранится как два массива (keys Array(K), values Array(V)) — аналогично Nested, но с удобным синтаксисом доступа по ключу.

TIP

LowCardinality(String) в Map ключах снижает storage overhead, когда набор ключей ограничен (device, browser, country) — словарь LowCardinality переиспользуется.


Подход 4: JSON type (GA 25.3)

Начиная с ClickHouse 25.3 (GA), JSON type хранит semi-structured данные с автоматическим выделением sub-columns:

CREATE TABLE events_json (
    event_id UInt64,
    timestamp DateTime,
    event_type LowCardinality(String),
    properties JSON
) ENGINE = MergeTree()
ORDER BY (event_type, timestamp);

JSON type автоматически обнаруживает подколонки и хранит их в типизированных столбцах (columnar storage). Это сочетание гибкости Map и производительности плоской схемы.

Детальное покрытие JSON type — в Модуле 15 (Phase 60). Здесь упоминаем как перспективный вариант для новых проектов на ClickHouse 25.3+.


Decision framework: как выбрать подход

Выбор модели данных для event properties
NestedNested: хранит данные как параллельные массивы (properties.key, properties.value). Для доступа требуется ARRAY JOIN. Подходит для структурированных вложенных данных с фиксированной схемой ключей и умеренной кардинальностью (до сотен элементов на строку).
FlattenedПлоская схема (Flattened): каждый атрибут -- отдельный столбец. Максимальная производительность запросов: прямой доступ, полная поддержка индексов, оптимальное сжатие. Минус: негибкая. ALTER TABLE ADD COLUMN при каждом новом атрибуте.
MapMap(K, V): key-value пара для переменных атрибутов. Гибкая схема: новые ключи без ALTER TABLE. Запросы по ключу: properties['key']. Без поддержки skip index на конкретных ключах. Хранится как два массива.
JSON (25.3+)JSON type (GA 25.3+): автоматическое обнаружение sub-columns. Сочетает гибкость Map и производительность columnar storage. Доступен с ClickHouse 25.3. Для проектов на 26.3 LTS -- предпочтительный вариант для полностью semi-structured данных.
КритерийNestedFlattenedMapJSON
Схема известна заранееЧастичноДаНетНет
Производительность запросовСредняяВысокаяСредняяВысокая
Гибкость схемыСредняяНизкаяВысокаяОчень высокая
Поддержка skip indexesОграниченаПолнаяНетЧастичная
Индивидуальные кодекиНетДаНетАвтоматические
Рекомендуемый сценарийСтруктурированные вложенныеФиксированная аналитикаПеременные labelsSemi-structured данные

Anti-pattern: Nested с 1000+ элементами на строку

Nested хранится как массивы. При 1000+ элементов на строку:

  • Insert amplification: каждая строка содержит тысячи элементов, объём одного INSERT пропорционален числу элементов
  • ARRAY JOIN explosion: развёртывание строки с 1000 элементами создаёт 1000 строк в результате. При миллионах строк — explosion до миллиардов
  • Отсутствие индексов на элементах: фильтрация по конкретному ключу требует полного сканирования массива

Если event properties содержит сотни уникальных ключей — используйте Map или JSON type, не Nested.


Гибридная схема: flattened + Map

На практике лучший результат даёт комбинация: фиксированные высокочастотные атрибуты как столбцы, а переменные — в Map:

CREATE TABLE events_hybrid (
    event_id UInt64,
    timestamp DateTime,
    event_type LowCardinality(String),
    -- Высокочастотные атрибуты: отдельные столбцы (индексируемые)
    device_type LowCardinality(String),
    country LowCardinality(String),
    -- Переменные атрибуты: Map (гибкие)
    extra Map(LowCardinality(String), String)
) ENGINE = MergeTree()
ORDER BY (event_type, country, timestamp);

Запросы по device_type и country используют primary key и skip indexes. Запросы по extra['promo_code'] — менее эффективны, но не требуют ALTER TABLE при появлении нового атрибута.


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

  1. Nested = параллельные массивы. ARRAY JOIN для развёртывания. Подходит для структурированных вложенных данных с умеренным количеством элементов.
  2. Flattened = максимальная производительность. Каждый атрибут — столбец с индексами и кодеками. Негибкая схема.
  3. Map(K, V) = гибкая key-value схема для переменных атрибутов. Нет индексов на отдельных ключах.
  4. JSON type (GA 25.3) = автоматические sub-columns. Перспективный вариант для semi-structured данных (детали в Модуле 15).
  5. Гибридная схема (flattened + Map) — оптимальный компромисс для production event pipelines.
  6. Anti-pattern: Nested с 1000+ элементами — используйте Map или JSON вместо этого.
Fact и dimension таблицы: нормализованные vs. денормализованные схемы Arrow: columnar memory layout, nested types и List arrays

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Система трекинга событий получает события с произвольным набором properties (UTM-метки, A/B тесты, device info). Набор ключей меняется каждую неделю. Какой тип данных выбрать?

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

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

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

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