> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-fix-nav-issues.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Работа с JSON в ClickHouse

> Распространённые способы работы с JSON-данными, реплицированными из MongoDB в ClickHouse через ClickPipes

В этом руководстве описаны распространённые способы работы с JSON-данными, реплицированными из MongoDB в ClickHouse через ClickPipes.

Предположим, мы создали коллекцию `t1` в MongoDB для отслеживания заказов клиентов:

```javascript theme={null}
db.t1.insertOne({
  "order_id": "ORD-001234",
  "customer_id": 98765,
  "status": "completed",
  "total_amount": 299.97,
  "order_date": new Date(),
  "shipping": {
    "method": "express",
    "city": "Seattle",
    "cost": 19.99
  },
  "items": [
    {
      "category": "electronics",
      "price": 149.99
    },
    {
      "category": "accessories",
      "price": 24.99
    }
  ]
})
```

Коннектор MongoDB CDC реплицирует документы MongoDB в ClickHouse, используя встроенный тип данных JSON. В реплицируемой таблице `t1` в ClickHouse будет содержаться следующая строка:

```shell theme={null}
Row 1:
──────
_id:                "68a4df4b9fe6c73b541703b0"
doc:                {"_id":"68a4df4b9fe6c73b541703b0","customer_id":"98765","items":[{"category":"electronics","price":149.99},{"category":"accessories","price":24.99}],"order_date":"2025-08-19T20:32:11.705Z","order_id":"ORD-001234","shipping":{"city":"Seattle","cost":19.99,"method":"express"},"status":"completed","total_amount":299.97}
_peerdb_synced_at:  2025-08-19 20:50:42.005000000
_peerdb_is_deleted: 0
_peerdb_version:    0
```

<div id="table-schema">
  ## Схема таблицы
</div>

Для реплицируемых таблиц используется следующая стандартная схема:

```shell theme={null}
┌─name───────────────┬─type──────────┐
│ _id                │ String        │
│ doc                │ JSON          │
│ _peerdb_synced_at  │ DateTime64(9) │
│ _peerdb_version    │ Int64         │
│ _peerdb_is_deleted │ Int8          │
└────────────────────┴───────────────┘
```

* `_id`: Первичный ключ из MongoDB
* `doc`: Документ MongoDB, реплицированный в типе данных JSON
* `_peerdb_synced_at`: Указывает, когда строка была синхронизирована в последний раз
* `_peerdb_version`: Отслеживает версию строки; увеличивается при обновлении или удалении строки
* `_peerdb_is_deleted`: Указывает, удалена ли строка

<div id="replacingmergetree-table-engine">
  ### Движок таблицы ReplacingMergeTree
</div>

ClickPipes сопоставляет коллекции MongoDB с ClickHouse с помощью семейства движков таблиц `ReplacingMergeTree`. В этом движке обновления представляются как вставки более новой версии (`_peerdb_version`) документа для данного первичного ключа (`_id`), что позволяет эффективно обрабатывать обновления, замены и удаления как версионированные вставки.

`ReplacingMergeTree` удаляет дубликаты асинхронно в фоновом режиме. Чтобы гарантировать отсутствие дубликатов для одной и той же строки, используйте [модификатор `FINAL`](/ru/reference/statements/select/from#final-modifier). Например:

```sql theme={null}
SELECT * FROM t1 FINAL;
```

<div id="handling-deletes">
  ### Обработка удалений
</div>

Удаления из MongoDB передаются как новые строки, помеченные как удалённые в столбце `_peerdb_is_deleted`. Обычно такие строки исключают с помощью фильтра в запросах:

```sql theme={null}
SELECT * FROM t1 FINAL WHERE _peerdb_is_deleted = 0;
```

Вы также можете создать политику на уровне строк, чтобы автоматически исключать удалённые строки, вместо того чтобы указывать фильтр в каждом запросе:

```sql theme={null}
CREATE ROW POLICY policy_name ON t1
FOR SELECT USING _peerdb_is_deleted = 0;
```

<div id="querying-json-data">
  ## Запросы к данным JSON
</div>

Вы можете напрямую обращаться к полям JSON, используя точечную нотацию:

```sql title="Query" theme={null}
SELECT
    doc.order_id,
    doc.shipping.method
FROM t1;
```

```shell title="Result" theme={null}
┌-─doc.order_id─┬─doc.shipping.method─┐
│ ORD-001234    │ express             │
└───────────────┴─────────────────────┘
```

При запросе *полей вложенных объектов* с использованием точечной нотации обязательно добавляйте оператор [`^`](/ru/reference/data-types/newjson#reading-json-sub-objects-as-sub-columns):

```sql title="Query" theme={null}
SELECT doc.^shipping as shipping_info FROM t1;
```

```shell title="Result" theme={null}
┌─shipping_info──────────────────────────────────────┐
│ {"city":"Seattle","cost":19.99,"method":"express"} │
└────────────────────────────────────────────────────┘
```

<div id="dynamic-type">
  ### Тип Dynamic
</div>

В ClickHouse каждое поле в JSON имеет тип `Dynamic`. Тип Dynamic позволяет ClickHouse хранить значения любых типов, не зная тип заранее. Это можно проверить с помощью функции `toTypeName`:

```sql title="Query" theme={null}
SELECT toTypeName(doc.customer_id) AS type FROM t1;
```

```shell title="Result" theme={null}
┌─type────┐
│ Dynamic │
└─────────┘
```

Чтобы узнать фактические типы данных поля, можно воспользоваться функцией `dynamicType`. Обратите внимание: у одного и того же имени поля в разных строках могут быть разные типы данных:

```sql title="Query" theme={null}
SELECT dynamicType(doc.customer_id) AS type FROM t1;
```

```shell title="Result" theme={null}
┌─type──┐
│ Int64 │
└───────┘
```

[Обычные функции](/ru/reference/functions/regular-functions/regular-functions-index) работают с типом Dynamic так же, как и с обычными столбцами:

**Пример 1: Парсинг даты**

```sql title="Query" theme={null}
SELECT parseDateTimeBestEffortOrNull(doc.order_date) AS order_date FROM t1;
```

```shell title="Result" theme={null}
┌─order_date──────────┐
│ 2025-08-19 20:32:11 │
└─────────────────────┘
```

**Пример 2: Условная логика**

```sql title="Query" theme={null}
SELECT multiIf(
    doc.total_amount < 100, 'less_than_100',
    doc.total_amount < 1000, 'less_than_1000',
    '1000+') AS spendings
FROM t1;
```

```shell title="Result" theme={null}
┌─spendings──────┐
│ less_than_1000 │
└────────────────┘
```

**Пример 3: операции с Array**

```sql title="Query" theme={null}
SELECT length(doc.items) AS item_count FROM t1;
```

```shell title="Result" theme={null}
┌─item_count─┐
│          2 │
└────────────┘
```

<div id="field-casting">
  ### Приведение типов полей
</div>

[Агрегатные функции](/ru/reference/functions/aggregate-functions/combinators) в ClickHouse не работают с типом Dynamic напрямую. Например, если попытаться применить функцию `sum` непосредственно к типу Dynamic, возникнет следующая ошибка:

```sql theme={null}
SELECT sum(doc.shipping.cost) AS shipping_cost FROM t1;
-- DB::Exception: Недопустимый тип Dynamic аргумента для агрегатной функции sum. (ILLEGAL_TYPE_OF_ARGUMENT)
```

Чтобы использовать агрегатные функции, приведите поле к соответствующему типу с помощью функции `CAST` или синтаксиса `::`:

```sql title="Query" theme={null}
SELECT sum(doc.shipping.cost::Float32) AS shipping_cost FROM t1;
```

```shell title="Result" theme={null}
┌─shipping_cost─┐
│         19.99 │
└───────────────┘
```

<Note>
  Приведение из типа Dynamic к базовому типу данных (определяемому с помощью `dynamicType`) выполняется очень быстро, поскольку ClickHouse уже хранит значение внутри в этом базовом типе.
</Note>

<div id="flattening-json">
  ## Преобразование JSON в плоский вид
</div>

<div id="normal-view">
  ### Обычное представление
</div>

Вы можете создавать обычные представления над JSON-таблицей, чтобы инкапсулировать логику разворачивания, приведения типов и преобразования, а затем запрашивать данные как в реляционной таблице. Обычные представления легковесны, поскольку хранят только сам запрос, а не исходные данные. Например:

```sql theme={null}
CREATE VIEW v1 AS
SELECT
    CAST(doc._id, 'String') AS object_id,
    CAST(doc.order_id, 'String') AS order_id,
    CAST(doc.customer_id, 'Int64') AS customer_id,
    CAST(doc.status, 'String') AS status,
    CAST(doc.total_amount, 'Decimal64(2)') AS total_amount,
    CAST(parseDateTime64BestEffortOrNull(doc.order_date, 3), 'DATETIME(3)') AS order_date,
    doc.^shipping AS shipping_info,
    doc.items AS items
FROM t1 FINAL
WHERE _peerdb_is_deleted = 0;
```

У этого представления будет следующая схема:

```shell theme={null}
┌─name────────────┬─type───────────┐
│ object_id       │ String         │
│ order_id        │ String         │
│ customer_id     │ Int64          │
│ status          │ String         │
│ total_amount    │ Decimal(18, 2) │
│ order_date      │ DateTime64(3)  │
│ shipping_info   │ JSON           │
│ items           │ Dynamic        │
└─────────────────┴────────────────┘
```

Теперь можно выполнять запросы к представлению так же, как к денормализованной таблице:

```sql theme={null}
SELECT
    customer_id,
    sum(total_amount)
FROM v1
WHERE shipping_info.city = 'Seattle'
GROUP BY customer_id
ORDER BY customer_id DESC
LIMIT 10;
```

<div id="refreshable-materialized-view">
  ### Refreshable materialized view
</div>

Вы можете создавать [Refreshable Materialized Views](/ru/concepts/features/materialized-views/refreshable-materialized-view), которые позволяют по расписанию выполнять запрос для дедупликации строк и сохранять результаты в плоскую целевую таблицу. При каждом обновлении по расписанию целевая таблица заменяется последними результатами запроса.

Ключевое преимущество этого метода в том, что запрос с ключевым словом `FINAL` выполняется только один раз — во время обновления, поэтому в последующих запросах к целевой таблице использовать `FINAL` уже не нужно.

Недостаток в том, что данные в целевой таблице остаются актуальными лишь до степени, соответствующей времени последнего обновления. Во многих сценариях интервалы обновления от нескольких минут до нескольких часов обеспечивают хороший баланс между актуальностью данных и производительностью запросов.

```sql theme={null}
CREATE TABLE flattened_t1 (
    `_id` String,
    `order_id` String,
    `customer_id` Int64,
    `status` String,
    `total_amount` Decimal(18, 2),
    `order_date` DateTime64(3),
    `shipping_info` JSON,
    `items` Dynamic
)
ENGINE = ReplacingMergeTree()
PRIMARY KEY _id
ORDER BY _id;

CREATE MATERIALIZED VIEW rmv REFRESH EVERY 1 HOUR TO flattened_t1 AS
SELECT 
    CAST(doc._id, 'String') AS _id,
    CAST(doc.order_id, 'String') AS order_id,
    CAST(doc.customer_id, 'Int64') AS customer_id,
    CAST(doc.status, 'String') AS status,
    CAST(doc.total_amount, 'Decimal64(2)') AS total_amount,
    CAST(parseDateTime64BestEffortOrNull(doc.order_date, 3), 'DATETIME(3)') AS order_date,
    doc.^shipping AS shipping_info,
    doc.items AS items
FROM t1 FINAL
WHERE _peerdb_is_deleted = 0;
```

Теперь вы можете обращаться к таблице `flattened_t1` напрямую, без модификатора `FINAL`:

```sql theme={null}
SELECT
    customer_id,
    sum(total_amount)
FROM flattened_t1
WHERE shipping_info.city = 'Seattle'
GROUP BY customer_id
ORDER BY customer_id DESC
LIMIT 10;
```

<div id="incremental-materialized-view">
  ### Incremental materialized view
</div>

Если вам нужен доступ к развёрнутым столбцам в реальном времени, вы можете создать [Incremental Materialized Views](/ru/concepts/features/materialized-views/incremental-materialized-view). Если в вашей таблице часто происходят обновления, не рекомендуется использовать модификатор `FINAL` в materialized view, так как каждое обновление будет запускать слияние. Вместо этого дедупликацию данных можно выполнять во время выполнения запроса, создав обычное представление поверх materialized view.

```sql theme={null}
CREATE TABLE flattened_t1 (
    `_id` String,
    `order_id` String,
    `customer_id` Int64,
    `status` String,
    `total_amount` Decimal(18, 2),
    `order_date` DateTime64(3),
    `shipping_info` JSON,
    `items` Dynamic,
    `_peerdb_version` Int64,
    `_peerdb_synced_at` DateTime64(9),
    `_peerdb_is_deleted` Int8
)
ENGINE = ReplacingMergeTree()
PRIMARY KEY _id
ORDER BY _id;

CREATE MATERIALIZED VIEW imv TO flattened_t1 AS
SELECT 
    CAST(doc._id, 'String') AS _id,
    CAST(doc.order_id, 'String') AS order_id,
    CAST(doc.customer_id, 'Int64') AS customer_id,
    CAST(doc.status, 'String') AS status,
    CAST(doc.total_amount, 'Decimal64(2)') AS total_amount,
    CAST(parseDateTime64BestEffortOrNull(doc.order_date, 3), 'DATETIME(3)') AS order_date,
    doc.^shipping AS shipping_info,
    doc.items,
    _peerdb_version,
    _peerdb_synced_at,   
    _peerdb_is_deleted
FROM t1;

CREATE VIEW flattened_t1_final AS
SELECT * FROM flattened_t1 FINAL WHERE _peerdb_is_deleted = 0;
```

Теперь вы можете выполнить запрос к представлению `flattened_t1_final` следующим образом:

```sql theme={null}
SELECT
    customer_id,
    sum(total_amount)
FROM flattened_t1_final
AND shipping_info.city = 'Seattle'
GROUP BY customer_id
ORDER BY customer_id DESC
LIMIT 10;
```
