ClickHouseMiddleTechnical

В чём разница между типами данных Array, Nested и Map в ClickHouse?

Array — одномерный массив одного типа; Nested — синтаксический сахар над несколькими параллельными массивами; Map — ключ-значение с O(n) доступом. Выбор зависит от семантики данных и паттерна чтения.

Array, Nested и Map в ClickHouse

ClickHouse поддерживает три основных составных типа для хранения повторяющихся структур в одной строке. Они похожи по назначению, но сильно различаются по реализации и производительности.

Array(T)

Хранит произвольное число элементов одного типа. Это первоклассный тип: он хранится в одном файле .bin и поддерживает богатый набор функций:

CREATE TABLE events (
    user_id  UInt64,
    tags     Array(String),
    scores   Array(Float32)
) ENGINE = MergeTree()
ORDER BY user_id;

INSERT INTO events VALUES (1, ['ml', 'python', 'data'], [0.9, 0.7, 0.85]);

-- Работа с элементами
SELECT
    user_id,
    tags[1]                      AS first_tag,   -- 1-based index
    has(tags, 'python')          AS has_python,
    arrayFilter(x -> x > 0.8, scores) AS high_scores,
    arrayMap(x -> x * 100, scores)     AS pct_scores,
    length(tags)                 AS tag_count
FROM events;

-- ARRAY JOIN разворачивает массив в строки
SELECT user_id, tag
FROM events
ARRAY JOIN tags AS tag;

Nested

Nested — синтаксический сахар: объявление нескольких колонок как «вложенной таблицы». На диске каждая колонка вложенной структуры хранится отдельным файлом (как самостоятельный Array), но ClickHouse гарантирует, что их длины совпадают и предоставляет удобный синтаксис обращения.

CREATE TABLE orders (
    order_id   UInt64,
    items      Nested(
        product_id  UInt32,
        quantity    UInt16,
        price       Decimal(10, 2)
    )
) ENGINE = MergeTree()
ORDER BY order_id;

-- Вставка: каждая "колонка" Nested — отдельный массив
INSERT INTO orders VALUES (42, [101, 202], [3, 1], [9.99, 49.99]);

-- Обращение через точку
SELECT
    order_id,
    items.product_id,
    items.quantity,
    arraySum(items.price)   AS total
FROM orders;

-- ARRAY JOIN по Nested-колонкам (join все колонки синхронно)
SELECT order_id, pid, qty, prc
FROM orders
ARRAY JOIN
    items.product_id AS pid,
    items.quantity   AS qty,
    items.price      AS prc;

Map(K, V)

Map(String, UInt64) хранит набор пар ключ-значение. Внутри реализован как два параллельных массива (ключей и значений). Доступ по ключу — O(n), никакого хеш-индекса нет.

CREATE TABLE page_views (
    session_id  UInt64,
    properties  Map(String, String)
) ENGINE = MergeTree()
ORDER BY session_id;

INSERT INTO page_views VALUES
    (1, {'browser': 'Chrome', 'os': 'Linux', 'country': 'RU'});

-- Обращение по ключу
SELECT
    session_id,
    properties['browser']      AS browser,
    mapKeys(properties)        AS keys,
    mapValues(properties)      AS vals,
    mapContains(properties, 'os') AS has_os
FROM page_views;

-- Фильтрация по значению Map
SELECT count()
FROM page_views
WHERE properties['country'] = 'RU';

Сравнительная таблица

  • Array: один тип элементов, богатые функции (arrayMap, arrayFilter, arraySum), лучше всего для однородных наборов значений.
  • Nested: несколько связанных массивов одной длины, семантика «таблица внутри строки», удобен для нормализованных данных (позиции заказа, события сессии).
  • Map: произвольные ключи (динамическая схема), O(n) доступ, подходит для properties/labels когда набор ключей заранее неизвестен.

Подводные камни

  • Nested — это НЕ JOIN: при ARRAY JOIN items.product_id, items.price нужно перечислять все нужные колонки явно, иначе другие колонки Nested не синхронизируются.
  • Map — O(n) поиск: для горячих запросов с фильтрацией по ключу Map работает медленнее отдельной колонки; добавьте BLOOM_FILTER или замените на реальную колонку.
  • Array индексируется с 1: arr[0] в ClickHouse вернёт дефолтное значение, а не первый элемент — частая ошибка для разработчиков, привыкших к 0-based индексам.
  • Nested не поддерживает DEFAULT и ALTER ADD COLUMN: нельзя добавить новую колонку в существующий Nested-тип без пересоздания таблицы.
  • Map(String, String) vs LowCardinality: ключи Map хранятся как полные строки; если ключи повторяются, используйте Map(LowCardinality(String), String) для экономии памяти.
  • ARRAY JOIN создаёт декартово произведение: если джойнить два независимых массива (ARRAY JOIN a, ARRAY JOIN b), результат будет декартовым произведением — используйте ARRAY JOIN обоих массивов в одном JOIN.
  • Глубина вложенности: Array(Array(T)) возможен, но операции с ним ограничены; не все функции поддерживают многомерные массивы.

Common mistakes

  • Объяснять array, nested и map как OLTP-механику row-store базы вместо аналитической колоночной модели ClickHouse.
  • Путать primary key ClickHouse с уникальным constraint из PostgreSQL или MySQL.
  • Игнорировать parts, merges, ORDER BY, sparse index и стоимость маленьких вставок.
  • Предлагать синтаксис или транзакционное поведение, которого в ClickHouse нет.

What the interviewer is testing

  • Кандидат объясняет array, nested и map через реальный механизм ClickHouse, а не общими словами.
  • Приводит корректный SQL или диагностический запрос для этой СУБД.
  • Называет ограничения, версионные отличия или эксплуатационные последствия.
  • Связывает ответ с проектированием приложения, производительностью, надежностью или безопасностью.

Sources

Related topics