> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Runbook: схема JSON

> Выберите подходящую схему для данных JSON в ClickHouse — типизированные столбцы, гибридный подход, нативный JSON или хранение в String

<Note>
  Тип столбца JSON готов к использованию в продакшне, начиная с ClickHouse 25.3+. Более ранние версии не рекомендуется использовать в продакшне.
</Note>

Ваши данные поступают в формате JSON. ClickHouse предлагает несколько способов их хранения: от полностью типизированных столбцов до хранения в виде необработанного String. Правильный выбор зависит от того, насколько предсказуема ваша схема и нужны ли вам запросы по отдельным полям.

**Область охвата:** Эта страница посвящена выбору схемы для хранения данных JSON. Она не охватывает [форматы ввода/вывода JSON](/docs/ru/reference/formats/JSON/JSON), [функции JSON](/docs/ru/reference/functions/regular-functions/json-functions) или синтаксис запросов. Общие сведения о самом типе столбца JSON см. в разделе [Используйте JSON там, где это уместно](/docs/ru/concepts/best-practices/json-type).

**Предполагается:** Знакомство с [созданием таблиц ClickHouse](/docs/ru/reference/statements/create/table), основами [MergeTree](/docs/ru/reference/engines/table-engines/mergetree-family/mergetree) и синтаксисом типов столбцов.

<div id="quick-decision">
  ## Быстрый выбор
</div>

* **Если** у каждого поля известный стабильный тип и схема меняется редко
  **→** [Типизированные столбцы](#typed-columns)
* **Если** большинство полей стабильны, но какая-то часть данных динамическая или непредсказуемая
  **→** [Гибридная схема (типизированные столбцы + JSON)](#hybrid)
* **Если** вся структура динамическая, а ключи в разных записях то появляются, то исчезают
  **→** [Нативный JSON-столбец](#native-json)
* **Если** динамические поля представляют собой пары ключ-значение с единым типом значений (например, строковые теги или числовые метрики)
  **→** [`Map`](#when-map-fits-better) вместо JSON
* **Если** вы только сохраняете и извлекаете JSON-объект без запросов по отдельным полям
  **→** [Непрозрачное хранение в String](#opaque-storage)

<Note>
  Не путайте JSON как *формат* и JSON как *тип столбца*. Вы можете вставлять данные в формате JSON (через `JSONEachRow` и т. д.) в типизированные столбцы, вообще не используя тип столбца `JSON`. Здесь речь идет о выборе типов столбцов, а не входных форматов.
</Note>

<div id="approach-details">
  ## Подробности о подходе
</div>

<div id="typed-columns">
  ### Типизированные столбцы
</div>

**Когда использовать:** Структура JSON полностью известна на этапе проектирования. Поля и типы не меняются от записи к записи. Даже сложные вложенные структуры (массивы объектов, вложенные словари) можно выразить с помощью типов [`Array`](/docs/ru/reference/data-types/array), [`Tuple`](/docs/ru/reference/data-types/tuple) и [`Nested`](/docs/ru/reference/data-types/nested-data-structures/index).

**Компромиссы:** Для изменения схемы требуется `ALTER TABLE`. Непредусмотренные поля при вставке молча отбрасываются, если схема не была обновлена.

<Accordion title="Настройка, проверка и подводные камни">
  **Настройка**

  ```sql theme={null}
  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)
  ```

  **Проверка**

  ```sql theme={null}
  -- Убедитесь, что типы столбцов соответствуют ожидаемым
  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`](/docs/ru/reference/settings/formats#input_format_skip_unknown_fields) в `0`, если хотите вместо этого получать ошибки.
</Accordion>

***

<div id="hybrid">
  ### Гибридный подход (типизированные столбцы + JSON)
</div>

**Когда использовать:** Базовый набор полей стабилен (временные метки, ID, коды состояния), но часть данных динамическая. Например, пользовательские атрибуты, теги, метаданные или поля расширений, которые различаются от записи к записи.

**Компромиссы:** Максимальная производительность на типизированных столбцах и гибкость в JSON-столбце. При этом JSON-столбец по-прежнему добавляет накладные расходы на вставку и затраты на хранилище для своей динамической части.

<Accordion title="Настройка, проверка и подводные камни">
  **Настройка**

  ```sql theme={null}
  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)
  ```

  **Проверка**

  ```sql theme={null}
  -- Вставьте пример данных и посмотрите, какие пути были определены
  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
  ```

  **Обратите внимание**

  * Используйте [подсказки типов](/docs/ru/reference/data-types/newjson) для JSON-путей, которые известны заранее. Подсказки обходят столбец-дискриминатор и сохраняют путь как обычный типизированный столбец — с той же производительностью и без накладных расходов.
  * Используйте `SKIP` или `SKIP REGEXP` для путей, которые вы никогда не запрашиваете (отладочные метаданные, внутренние ID трассировки), чтобы экономить хранилище и уменьшать число подстолбцов.
  * Устанавливайте `max_dynamic_paths` пропорционально числу различных путей, которые вы действительно запрашиваете. Значение по умолчанию (1024) подходит для большинства случаев. Уменьшите его, если динамическая часть у вас небольшая.
  * Не устанавливайте `max_dynamic_paths` выше 10,000. Высокие значения увеличивают потребление ресурсов и снижают эффективность.

  <Info>
    **Ключи с точками**

    Ключи с точками (например, `http.status_code`) по умолчанию трактуются как вложенные пути, поэтому `{"http.status_code": 200}` хранится так же, как `{"http": {"status_code": 200}}`. Это часто встречается в атрибутах OTel. Используйте подсказки типов, чтобы управлять тем, как хранятся пути с точками, или включите `json_type_escape_dots_in_keys` (25.8+).
  </Info>
</Accordion>

***

<div id="native-json">
  ### Нативный JSON-столбец
</div>

**Когда использовать:** Когда структура действительно непредсказуема и ключи появляются и исчезают от записи к записи. Схемы, создаваемые пользователями, системы плагинов или ингестия в озеро данных, где вы не контролируете исходную схему.

**Компромиссы:** Вставка медленнее, чем в типизированные столбцы. Полное чтение объекта медленнее, чем у String. Дополнительные затраты хранилища на управление подстолбцами. Хорошо подходит для запросов по отдельным полям на конкретных путях.

<Accordion title="Настройка, проверка и подводные камни">
  **Настройка**

  ```sql theme={null}
  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`](/docs/ru/reference/formats/JSON/JSONAsObject) при вставке целых JSON-документов в JSON-столбец. В нём каждая входная строка интерпретируется как полный объект JSON, сопоставленный со столбцом.

  **Проверка**

  ```sql theme={null}
  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`, значения сверх лимита перемещаются в [общую структуру данных](/docs/ru/reference/data-types/newjson#shared-data-structure), что снижает производительность запросов. Отслеживайте это с помощью [`JSONDynamicPaths()`](/docs/ru/reference/data-types/newjson#introspection-functions) и держите лимит ниже 10 000.
  * Каждый динамический путь поддерживает до `max_dynamic_types` (по умолчанию 32) различных типов данных. Если один путь превышает этот предел, дополнительные типы переключаются на общее хранилище Variant. Обычно это несущественно, если только в ваших данных нет сильно различающихся типов для одного и того же поля.
</Accordion>

***

<div id="opaque-storage">
  ### Непрозрачное хранение в String
</div>

**Когда использовать:** JSON-документы хранятся и извлекаются целиком, а затем передаются в приложение, архивируются или отправляются дальше по конвейеру. Без фильтрации по отдельным полям и агрегации внутри ClickHouse.

**Компромиссы:** Самые быстрые вставки и самая простая схема. Запросы по отдельным полям невозможны без разбора во время выполнения (семейство `JSONExtract`), а это плохо масштабируется.

<Accordion title="Настройка, проверка и подводные камни">
  **Настройка**

  ```sql theme={null}
  CREATE TABLE raw_events
  (
      `id`        UInt64,
      `received`  DateTime DEFAULT now(),
      `payload`   String
  )
  ENGINE = MergeTree
  ORDER BY (received)
  ```

  **Проверка**

  ```sql theme={null}
  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-столбцами и выполнить дозагрузку данных. Если есть хоть какая-то вероятность, что вам потребуется обращаться к отдельным полям, лучше сразу выбрать [гибридный подход](#hybrid).
  * Функции `JSONExtract` разбирают строку при каждом запросе. Это приемлемо для разового анализа, но не для панелей мониторинга в продакшн и не для рабочих нагрузок с высоким QPS.
  * Если JSON-полезная нагрузка велика, рассмотрите кодеки сжатия (`ZSTD`) для столбца String — такие данные хорошо сжимаются.
</Accordion>

<div id="comparison">
  ## Сравнение
</div>

| Критерий                           | Типизированные столбцы      | Гибридный                                       | Нативный JSON                         | String                                 |
| ---------------------------------- | --------------------------- | ----------------------------------------------- | ------------------------------------- | -------------------------------------- |
| **Пропускная способность вставки** | Самая высокая               | Высокая                                         | Умеренная                             | Самая высокая                          |
| **Запрос по отдельным полям**      | Самые быстрые               | Быстрые (типизированные); хорошие (JSON с hint) | Хорошие (с hint); медленнее (Dynamic) | Медленные (разбор во время выполнения) |
| **Чтение объекта целиком**         | Быстрое                     | Умеренное                                       | Медленное                             | Самое быстрое                          |
| **Эффективность хранения**         | Лучшая                      | Хорошая                                         | Умеренная                             | Хорошая (хорошо сжимается)             |
| **Гибкость схемы**                 | Отсутствует (`ALTER TABLE`) | Частичная (жёсткое ядро, гибкий tail)           | Полная                                | Полная                                 |
| **Сложность**                      | Низкая                      | Средняя                                         | Средняя–высокая                       | Низкая                                 |

<div id="when-map-fits-better">
  ## Когда лучше подходит Map
</div>

Если ваши динамические поля представляют собой однородные пары «ключ-значение» — то есть все значения имеют один и тот же тип, — [`Map(String, T)`](/docs/ru/reference/data-types/map) проще и эффективнее, чем JSON-столбец. Типичные примеры: строковые теги (`Map(String, String)`), числовые метрики (`Map(String, Float64)`) или feature flags (`Map(String, Bool)`).

```sql theme={null}
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`](/docs/ru/reference/data-types/map#bucketed-map-serialization). Используйте JSON, когда значения имеют смешанные типы или структура содержит вложенные данные, — используйте `Map`, когда это плоские пары ключ-значение с единым типом значений.

<div id="related-resources">
  ## Связанные ресурсы
</div>

* [Используйте JSON там, где это уместно](/docs/ru/concepts/best-practices/json-type) — когда использовать тип столбца JSON, а когда — альтернативы
* [Справочник по типу данных JSON](/docs/ru/reference/data-types/newjson) — полный синтаксис для подсказок типов, SKIP, max\_dynamic\_paths и функций интроспекции
* [Выбор типов данных](/docs/ru/concepts/best-practices/select-data-type) — общие рекомендации по выбору типов данных
* [A New Powerful JSON Data Type for ClickHouse](https://clickhouse.com/blog/a-new-powerful-json-data-type-for-clickhouse) — подробный разбор архитектуры хранения типа JSON
* [Справочник по форматам JSON](/docs/ru/reference/formats/JSON/JSON) — форматы ввода/вывода для данных JSON (JSONEachRow, JSONAsObject и т. д.)
