Ваши данные поступают в формате JSON. ClickHouse предлагает несколько способов их хранения: от полностью типизированных столбцов до хранения в виде необработанного String. Правильный выбор зависит от того, насколько предсказуема ваша схема и нужны ли вам запросы по отдельным полям.
Область охвата: Эта страница посвящена выбору схемы для хранения данных JSON. Она не охватывает форматы ввода/вывода JSON, функции JSON или синтаксис запросов. Общие сведения о самом типе столбца JSON см. в разделе Используйте JSON там, где это уместно.
Предполагается: Знакомство с созданием таблиц ClickHouse, основами MergeTree и синтаксисом типов столбцов.
Быстрый выбор
- Если у каждого поля известный стабильный тип и схема меняется редко → Типизированные столбцы
- Если большинство полей стабильны, но какая-то часть данных динамическая или непредсказуемая → Гибридная схема (типизированные столбцы + JSON)
- Если вся структура динамическая, а ключи в разных записях то появляются, то исчезают → Нативный JSON-столбец
- Если динамические поля представляют собой пары ключ-значение с единым типом значений (например, строковые теги или числовые метрики)
→
Mapвместо JSON - Если вы только сохраняете и извлекаете JSON-объект без запросов по отдельным полям → Непрозрачное хранение в String
Подробности о подходе
Типизированные столбцы
Когда использовать: Структура JSON полностью известна на этапе проектирования. Поля и типы не меняются от записи к записи. Даже сложные вложенные структуры (массивы объектов, вложенные словари) можно выразить с помощью типов Array, Tuple и Nested.
Компромиссы: Для изменения схемы требуется ALTER TABLE. Непредусмотренные поля при вставке молча отбрасываются, если схема не была обновлена.
Настройка, проверка и подводные камни
Настройка
CREATE TABLE events
(
`timestamp` DateTime,
`service` LowCardinality(String),
`level` Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
`message` String,
`host` LowCardinality(String),
`duration_ms` UInt32
)
ENGINE = MergeTree
ORDER BY (service, timestamp)Проверка
-- Убедитесь, что типы столбцов соответствуют ожидаемым
DESCRIBE TABLE events FORMAT Vertical
-- Выполните вставку и запрос, чтобы проверить, что схема корректно обрабатывает ваши данные
INSERT INTO events FORMAT JSONEachRow
{"timestamp":"2025-03-19 10:00:00","service":"api","level":"INFO","message":"request handled","host":"node-1","duration_ms":42}
SELECT service, level, duration_ms FROM events WHERE service = 'api'Обратите внимание
- Если вы вставляете JSON-данные с
JSONEachRowи JSON содержит поля, которых нет в схеме, ClickHouse по умолчанию молча их отбрасывает. Установитеinput_format_skip_unknown_fieldsв0, если хотите вместо этого получать ошибки.
Гибридный подход (типизированные столбцы + JSON)
Когда использовать: Базовый набор полей стабилен (временные метки, ID, коды состояния), но часть данных динамическая. Например, пользовательские атрибуты, теги, метаданные или поля расширений, которые различаются от записи к записи.
Компромиссы: Максимальная производительность на типизированных столбцах и гибкость в JSON-столбце. При этом JSON-столбец по-прежнему добавляет накладные расходы на вставку и затраты на хранилище для своей динамической части.
Настройка, проверка и подводные камни
Настройка
CREATE TABLE events
(
`timestamp` DateTime,
`service` LowCardinality(String),
`level` Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
`message` String,
`host` LowCardinality(String),
`duration_ms` UInt32,
`attributes` JSON(
max_dynamic_paths = 256,
`http.status_code` UInt16,
`http.method` LowCardinality(String),
SKIP REGEXP 'debug\..*'
)
)
ENGINE = MergeTree
ORDER BY (service, timestamp)Проверка
-- Вставьте пример данных и посмотрите, какие пути были определены
INSERT INTO events FORMAT JSONEachRow
{"timestamp":"2025-03-19 10:00:00","service":"api","level":"INFO","message":"request handled","host":"node-1","duration_ms":42,"attributes":{"http.status_code":200,"http.method":"GET","user.region":"eu-west","custom_tag":"abc"}}
SELECT JSONAllPathsWithTypes(attributes)
FROM events
FORMAT PrettyJSONEachRowОбратите внимание
- Используйте подсказки типов для JSON-путей, которые известны заранее. Подсказки обходят столбец-дискриминатор и сохраняют путь как обычный типизированный столбец — с той же производительностью и без накладных расходов.
- Используйте
SKIPилиSKIP REGEXPдля путей, которые вы никогда не запрашиваете (отладочные метаданные, внутренние ID трассировки), чтобы экономить хранилище и уменьшать число подстолбцов. - Устанавливайте
max_dynamic_pathsпропорционально числу различных путей, которые вы действительно запрашиваете. Значение по умолчанию (1024) подходит для большинства случаев. Уменьшите его, если динамическая часть у вас небольшая. - Не устанавливайте
max_dynamic_pathsвыше 10,000. Высокие значения увеличивают потребление ресурсов и снижают эффективность.
Нативный JSON-столбец
Когда использовать: Когда структура действительно непредсказуема и ключи появляются и исчезают от записи к записи. Схемы, создаваемые пользователями, системы плагинов или ингестия в озеро данных, где вы не контролируете исходную схему.
Компромиссы: Вставка медленнее, чем в типизированные столбцы. Полное чтение объекта медленнее, чем у String. Дополнительные затраты хранилища на управление подстолбцами. Хорошо подходит для запросов по отдельным полям на конкретных путях.
Настройка, проверка и подводные камни
Настройка
CREATE TABLE dynamic_events
(
`id` UInt64,
`ts` DateTime DEFAULT now(),
`data` JSON(
max_dynamic_paths = 512,
`event_type` LowCardinality(String),
`version` UInt8
)
)
ENGINE = MergeTree
ORDER BY (data.event_type, ts)Используйте формат JSONAsObject при вставке целых JSON-документов в JSON-столбец. В нём каждая входная строка интерпретируется как полный объект JSON, сопоставленный со столбцом.
Проверка
INSERT INTO dynamic_events (id, data) FORMAT JSONEachRow
{"id": 1, "data": {"event_type": "click", "version": 2, "page": "/home", "button_id": "cta-1"}}
{"id": 2, "data": {"event_type": "purchase", "version": 1, "item_id": "SKU-99", "amount": 49.99, "currency": "USD"}}
-- Проверьте, какие пути ClickHouse обнаружил и их типы
SELECT JSONAllPathsWithTypes(data) FROM dynamic_events FORMAT PrettyJSONEachRow
-- Выполните запрос по конкретному пути
SELECT data.page FROM dynamic_events WHERE data.event_type = 'click'На что обратить внимание
- Без подсказок типов ClickHouse определяет тип для каждого пути по первым встретившимся значениям. Если
scoreприходит как"10"(строка) в одной записи и как10(целое число) в другой, для этого пути создаётся столбец-дискриминатор, и запросы становятся медленнее. Добавляйте подсказки для путей с известными типами. - Когда количество путей превышает
max_dynamic_paths, значения сверх лимита перемещаются в общую структуру данных, что снижает производительность запросов. Отслеживайте это с помощьюJSONDynamicPaths()и держите лимит ниже 10 000. - Каждый динамический путь поддерживает до
max_dynamic_types(по умолчанию 32) различных типов данных. Если один путь превышает этот предел, дополнительные типы переключаются на общее хранилище Variant. Обычно это несущественно, если только в ваших данных нет сильно различающихся типов для одного и того же поля.
Непрозрачное хранение в String
Когда использовать: JSON-документы хранятся и извлекаются целиком, а затем передаются в приложение, архивируются или отправляются дальше по конвейеру. Без фильтрации по отдельным полям и агрегации внутри ClickHouse.
Компромиссы: Самые быстрые вставки и самая простая схема. Запросы по отдельным полям невозможны без разбора во время выполнения (семейство JSONExtract), а это плохо масштабируется.
Настройка, проверка и подводные камни
Настройка
CREATE TABLE raw_events
(
`id` UInt64,
`received` DateTime DEFAULT now(),
`payload` String
)
ENGINE = MergeTree
ORDER BY (received)Проверка
INSERT INTO raw_events (id, payload) VALUES
(1, '{"type":"click","page":"/home"}'),
(2, '{"type":"purchase","item":"SKU-99","amount":49.99}')
-- Убедитесь, что данные записываются и читаются без изменений
SELECT payload FROM raw_events WHERE id = 1
-- Убедитесь, что при необходимости поля всё ещё можно разбирать на лету
SELECT JSONExtractString(payload, 'type') AS event_type FROM raw_eventsНа что обратить внимание
- Если требования изменятся и позже вам понадобятся запросы по отдельным полям, придётся создать новую таблицу с типизированными столбцами или JSON-столбцами и выполнить дозагрузку данных. Если есть хоть какая-то вероятность, что вам потребуется обращаться к отдельным полям, лучше сразу выбрать гибридный подход.
- Функции
JSONExtractразбирают строку при каждом запросе. Это приемлемо для разового анализа, но не для панелей мониторинга в продакшн и не для рабочих нагрузок с высоким QPS. - Если JSON-полезная нагрузка велика, рассмотрите кодеки сжатия (
ZSTD) для столбца String — такие данные хорошо сжимаются.
Сравнение
| Критерий | Типизированные столбцы | Гибридный | Нативный JSON | String |
|---|---|---|---|---|
| Пропускная способность вставки | Самая высокая | Высокая | Умеренная | Самая высокая |
| Запрос по отдельным полям | Самые быстрые | Быстрые (типизированные); хорошие (JSON с hint) | Хорошие (с hint); медленнее (Dynamic) | Медленные (разбор во время выполнения) |
| Чтение объекта целиком | Быстрое | Умеренное | Медленное | Самое быстрое |
| Эффективность хранения | Лучшая | Хорошая | Умеренная | Хорошая (хорошо сжимается) |
| Гибкость схемы | Отсутствует (ALTER TABLE) |
Частичная (жёсткое ядро, гибкий tail) | Полная | Полная |
| Сложность | Низкая | Средняя | Средняя–высокая | Низкая |
Когда лучше подходит Map
Если ваши динамические поля представляют собой однородные пары «ключ-значение» — то есть все значения имеют один и тот же тип, — Map(String, T) проще и эффективнее, чем JSON-столбец. Типичные примеры: строковые теги (Map(String, String)), числовые метрики (Map(String, Float64)) или feature flags (Map(String, Bool)).
CREATE TABLE tagged_events
(
`timestamp` DateTime,
`service` LowCardinality(String),
`tags` Map(String, String) -- e.g., {"env": "prod", "region": "us-east-1", "team": "platform"}
)
ENGINE = MergeTree
ORDER BY (service, timestamp)Map поддерживает фильтрацию на уровне ключей (tags['env'] = 'prod'), хранится дешевле, чем JSON, и позволяет избежать накладных расходов на подстолбцы, характерных для типа JSON. Обратите внимание, что поиск по ключу по умолчанию выполняет линейное сканирование map — это нормально для небольших наборов тегов, но для map со 100+ ключами стоит рассмотреть сериализацию with_buckets. Используйте JSON, когда значения имеют смешанные типы или структура содержит вложенные данные, — используйте Map, когда это плоские пары ключ-значение с единым типом значений.
- Используйте JSON там, где это уместно — когда использовать тип столбца JSON, а когда — альтернативы
- Справочник по типу данных JSON — полный синтаксис для подсказок типов, SKIP, max_dynamic_paths и функций интроспекции
- Выбор типов данных — общие рекомендации по выбору типов данных
- A New Powerful JSON Data Type for ClickHouse — подробный разбор архитектуры хранения типа JSON
- Справочник по форматам JSON — форматы ввода/вывода для данных JSON (JSONEachRow, JSONAsObject и т. д.)