ClickHouse хорошо работает не только с простыми колонками, но и с полуструктурированными данными: JSON-событиями, массивами тегов, вложенными атрибутами и текстовыми свойствами. Главное — не превращать каждую таблицу в безразмерный JSON-склад, а выбирать между обычными колонками, JSON, Array и Nested по задаче

- JSON-формат и JSON-тип — разные вещи
- Когда использовать тип JSON
- Таблица с JSON и Array
- Array: массивы в ClickHouse
- arrayMap и arrayFilter
- ARRAY JOIN: раскрыть массив в строки
- Nested: когда нужен вложенный набор колонок
- Текстовые поля и поиск
- Частые ошибки
- Как выбрать между JSON, Array и отдельной таблицей
- Контроль размера после ARRAY JOIN
- Что еще учесть при работе с JSON и массивами
- Когда JSON лучше распарсить заранее?
- Почему ARRAY JOIN нужно проверять на размере результата?
- Как это связано с загрузкой данных?
JSON-формат и JSON-тип — разные вещи
В ClickHouse можно загружать данные в формате JSONEachRow. Это формат обмена: каждая строка файла — JSON-объект. Но это не означает, что колонка в таблице должна иметь тип JSON. Часто правильнее распарсить JSON в обычные типизированные колонки
{"event_time":"2026-05-19 10:00:00","user_id":101,"event_type":"view","amount":0}
{"event_time":"2026-05-19 10:01:00","user_id":101,"event_type":"purchase","amount":990}
Эти строки можно вставить в таблицу с обычными колонками: DateTime, UInt64, LowCardinality(String), Decimal
Когда использовать тип JSON
Тип JSON имеет смысл, когда структура действительно динамическая: у событий разные ключи, поля появляются и исчезают, заранее перечислить все колонки сложно. Например, пользовательские свойства события, произвольные параметры интеграций или логи с разными схемами
Если поле стабильно и часто используется в фильтрах, вынесите его в отдельную колонку. Так запросы будут проще, а схема понятнее
Таблица с JSON и Array
CREATE TABLE demo.event_json
(
event_time DateTime,
user_id UInt64,
event_type LowCardinality(String),
tags Array(String),
payload JSON
)
ENGINE = MergeTree
ORDER BY (toDate(event_time), event_type, user_id);
tags — это массив строк. payload — динамический JSON-объект. При такой схеме основные аналитические поля остаются нормальными колонками, а нестабильные свойства уходят в JSON
Array: массивы в ClickHouse
Array(T) хранит несколько значений одного типа. Все элементы должны иметь общий тип: нельзя нормально смешивать строки, числа и даты в одном массиве без явной модели
SELECT
['seo', 'clickhouse', 'database'] AS tags,
length(tags) AS tag_count,
has(tags, 'clickhouse') AS has_clickhouse;
arrayMap и arrayFilter
SELECT
tags,
arrayMap(x -> lowerUTF8(x), tags) AS normalized_tags,
arrayFilter(x -> x != '', tags) AS clean_tags
FROM demo.event_json;
Функции массивов позволяют преобразовывать значения без отдельной нормализующей таблицы. Но если массивы становятся главным объектом аналитики, проверьте, не нужна ли отдельная таблица связей
ARRAY JOIN: раскрыть массив в строки
ARRAY JOIN размножает строку по элементам массива. Если у события три тега, после ARRAY JOIN получится три строки
SELECT
user_id,
event_type,
tag
FROM demo.event_json
ARRAY JOIN tags AS tag
WHERE tag != '';
Это мощно, но опасно для больших массивов. Запрос может внезапно прочитать тысячу исходных строк и получить сотни тысяч строк после раскрытия
Nested: когда нужен вложенный набор колонок
Nested полезен, когда у записи есть повторяющаяся группа связанных полей. Например, у заказа несколько товаров, и для каждого товара нужны sku, quantity, price. Это уже не просто массив строк, а массив структур
CREATE TABLE demo.orders
(
order_id UInt64,
created_at DateTime,
items Nested
(
sku String,
quantity UInt16,
price Decimal(12, 2)
)
)
ENGINE = MergeTree
ORDER BY (toDate(created_at), order_id);
Текстовые поля и поиск
Для простых проверок можно использовать LIKE или строковые функции. Но если вы постоянно ищете по одному и тому же признаку внутри текста, лучше вынести этот признак в отдельную колонку. ClickHouse развивается в сторону текстового поиска, но модель данных все равно остается важной
Частые ошибки
- Хранить предсказуемую схему в одном JSON-поле вместо нормальных колонок.
- Смешивать несовместимые типы в массиве.
- Использовать
ARRAY JOIN, не оценивая рост числа строк. - Считать JSON-тип обязательным для загрузки JSONEachRow.
- Искать структурированные параметры через
LIKEв сыром тексте.
Как выбрать между JSON, Array и отдельной таблицей
| Ситуация | Лучший выбор |
|---|---|
| Поля известны заранее и часто фильтруются | Обычные типизированные колонки |
| Небольшой список тегов у события | Array(String) |
| Динамические свойства с разными ключами | JSON |
| Много связанных сущностей с собственной аналитикой | Отдельная таблица связей |
| Повторяющаяся группа полей внутри записи | Nested или отдельная таблица, зависит от запросов |
Главный критерий — не красота схемы, а будущие запросы. Если аналитик постоянно спрашивает "топ тегов за день", массив с ARRAY JOIN может быть удобен. Если нужны сложные связи между объектами, отдельная таблица будет понятнее
Контроль размера после ARRAY JOIN
SELECT
count() AS source_rows,
sum(length(tags)) AS rows_after_array_join_estimate
FROM demo.event_json;
Этот простой расчет заранее показывает, во сколько раз может вырасти результат после раскрытия массива. Если оценка слишком большая, добавьте фильтр по дате, типу события или длине массива до ARRAY JOIN
Что еще учесть при работе с JSON и массивами
Когда JSON лучше распарсить заранее?
Если поле часто используется в WHERE, GROUP BY, JOIN или materialized view. Вынесенная типизированная колонка обычно проще для запросов и быстрее для аналитики; общий подход к типам разобран в материале Типы данных ClickHouse
Почему ARRAY JOIN нужно проверять на размере результата?
Он размножает строки. Если в каждой строке десятки элементов массива, отчет может резко стать тяжелее. Перед тем как подключать такой запрос к дашборду, проверьте кардинальность и подумайте, не нужна ли отдельная таблица или materialized view
Как это связано с загрузкой данных?
Формат JSONEachRow часто используют уже на этапе вставки, но сама загрузка и выбор формата — отдельная тема. Если нужно собрать полный путь от файла до запроса, посмотрите урок ClickHouse Client, INSERT INTO и первые SELECT-запросы



