Skip to main content
Ниже приведены альтернативные подходы к моделированию JSON в ClickHouse. Они описаны здесь для полноты картины: эти подходы использовались до появления типа JSON, поэтому в большинстве сценариев их обычно не рекомендуется применять, а во многих случаях они и вовсе не подходят.
Используйте подход на уровне объектаДля разных объектов в рамках одной и той же схемы можно использовать разные методы. Например, для одних объектов лучше всего подходит тип String, а для других — тип Map. Обратите внимание: если используется тип String, принимать дальнейшие решения о схеме уже не нужно. Кроме того, внутри ключа Map можно вкладывать вложенные объекты, включая String, представляющий JSON, как показано ниже:

Использование типа String

Если объекты очень динамичны, не имеют предсказуемой структуры и содержат произвольные вложенные объекты, следует использовать тип String. Значения можно извлекать на этапе выполнения запроса с помощью JSON-функций, как показано ниже. Обработка данных с использованием структурированного подхода, описанного выше, часто непрактична для пользователей, работающих с динамическим JSON, который либо меняется, либо имеет не до конца понятную схему. Для максимальной гибкости можно просто хранить JSON в виде String, а затем использовать функции для извлечения полей по мере необходимости. Это крайняя противоположность обработке JSON как структурированного объекта. Однако за эту гибкость приходится платить: прежде всего усложняется синтаксис запросов и снижается производительность. Как отмечалось ранее, для исходного объекта person мы не можем гарантировать структуру столбца tags. Мы вставляем исходную строку (включая company.labels, которое пока игнорируем), объявляя столбец Tags как String:
Мы можем выбрать столбец tags и увидеть, что JSON вставлен как строка:
Функции JSONExtract можно использовать для получения значений из этого JSON. Рассмотрим простой пример:
Обратите внимание, что этим функциям требуются и ссылка на столбец tags типа String, и путь в JSON, по которому нужно извлечь значение. Для вложенных путей функции также должны быть вложенными, например JSONExtractUInt(JSONExtractString(tags, 'car'), 'year'), что извлекает значение по пути tags.car.year. Извлечение вложенных путей можно упростить с помощью функций JSON_QUERY и JSON_VALUE. Рассмотрим крайний случай с dataset arxiv, где всё содержимое рассматривается как String.
Для вставки в эту схему нужно использовать формат JSONAsString:
Предположим, мы хотим подсчитать количество опубликованных работ по годам. Сравните следующий запрос, в котором схема задана просто строкой, с ее структурированной версией:
Обратите внимание, что здесь для фильтрации JSON используется выражение XPath, а именно JSON_VALUE(body, '$.versions[0].created'). Функции String значительно медленнее (> 10x), чем явные преобразования типов с индексами. Приведённые выше запросы всегда требуют полного сканирования таблицы и обработки каждой строки. Хотя на небольшом наборе данных, подобном этому, такие запросы всё равно будут выполняться быстро, на более крупных наборах данных производительность снизится. Гибкость этого подхода достигается ценой заметных потерь в производительности и усложнения синтаксиса, поэтому его следует использовать только для очень динамичных объектов в схеме.

Простые JSON-функции

В приведённых выше примерах используется семейство функций JSON*. Они используют полноценный JSON-парсер на основе simdjson, который выполняет строгий разбор и различает одно и то же поле, вложенное на разных уровнях. Эти функции способны работать с JSON, который синтаксически корректен, но плохо отформатирован, например с двойными пробелами между ключами. Также доступен более быстрый и более строгий набор функций. Функции simpleJSON* потенциально обеспечивают более высокую производительность, главным образом за счёт строгих допущений о структуре и формате JSON. В частности:
  • Имена полей должны быть константами
  • Единообразная кодировка имён полей, например simpleJSONHas('{"abc":"def"}', 'abc') = 1, но visitParamHas('{"\\u0061\\u0062\\u0063":"def"}', 'abc') = 0
  • Имена полей должны быть уникальны во всех вложенных структурах. Уровни вложенности не различаются, а сопоставление выполняется без их учёта. Если совпадающих полей несколько, используется первое вхождение.
  • Никаких специальных символов вне строковых литералов. Это касается и пробелов. Следующий пример некорректен и не будет разобран.
В то время как следующий пример будет разобран корректно:
Приведённый выше запрос использует simpleJSONExtractString для извлечения ключа created, исходя из того, что для даты публикации нам нужно только первое значение. В этом случае ограничения функций simpleJSON* оправданы выигрышем в производительности.

Использование типа Map

Если объект используется для хранения произвольных ключей, в основном одного типа, рассмотрите возможность использования типа Map. В идеале количество уникальных ключей не должно превышать нескольких сотен. Тип Map также можно использовать для объектов с вложенными объектами, если их типы однородны. В целом мы рекомендуем использовать тип Map для меток и тегов, например меток подов Kubernetes в данных логов. Хотя Map предоставляет простой способ представления вложенных структур, у него есть несколько существенных ограничений:
  • Все поля должны быть одного типа.
  • Для доступа к подстолбцам требуется специальный синтаксис Map, поскольку поля не существуют как отдельные столбцы. Весь объект и есть столбец.
  • При доступе к подстолбцу загружается всё значение Map, то есть все соседние элементы и их соответствующие значения. Для больших Map это может приводить к существенному снижению производительности.
Ключи StringПри моделировании объектов как Map для хранения имени ключа JSON используется ключ String. Поэтому Map всегда имеет вид Map(String, T), где T зависит от данных.

Примитивные значения

Самый простой способ использовать Map — когда объект содержит значения одного и того же примитивного типа. В большинстве случаев для значения T при этом используется тип String. Рассмотрим JSON с данными о человеке из предыдущего примера, где объект company.labels был определён как динамический. Важно, что мы ожидаем добавления в этот объект только пар ключ-значение типа String. Поэтому его можно объявить как Map(String, String):
Можно вставить исходный объект JSON целиком:
При обращении к этим полям внутри объекта request требуется синтаксис Map, например:
Доступен полный набор функций Map для работы с этим типом; они описаны здесь. Если ваши данные не имеют единого типа, можно использовать функции для необходимого приведения типов.

Значения объектов

Тип Map также можно использовать для объектов, содержащих вложенные объекты, если для последних сохраняется согласованность типов. Предположим, что ключ tags в нашем объекте persons требует согласованной структуры, в которой вложенный объект для каждого tag содержит столбцы name и time. Упрощённый пример такого JSON-документа может выглядеть следующим образом:
Это можно смоделировать с помощью Map(String, Tuple(name String, time DateTime)), как показано ниже:
Использование типа Map в таком случае, как правило, встречается редко и обычно указывает на то, что структуру данных следует переработать так, чтобы динамические имена ключей не содержали вложенных объектов. Например, приведённый выше пример можно преобразовать следующим образом, что позволит использовать Array(Tuple(key String, name String, time DateTime)).

Использование типа Nested

Тип Nested можно использовать для моделирования статических объектов, которые редко меняются, в качестве альтернативы Tuple и Array(Tuple). Как правило, мы рекомендуем не использовать этот тип для JSON, поскольку его поведение часто сбивает с толку. Основное преимущество Nested состоит в том, что подстолбцы можно использовать в ключах сортировки. Ниже приведён пример использования типа Nested для моделирования статического объекта. Рассмотрим следующую простую запись журнала в формате JSON:
Мы можем объявить ключ request как Nested. Как и в случае с Tuple, необходимо указать подстолбцы.

flatten_nested

Параметр flatten_nested определяет поведение типа Nested.

flatten_nested=1

Значение 1 (по умолчанию) не поддерживает произвольную глубину вложенности. В этом случае вложенную структуру данных удобнее всего рассматривать как несколько столбцов Array одинаковой длины. Поля method, path и version фактически представляют собой отдельные столбцы Array(Type) с одним важным ограничением: длина полей method, path и version должна быть одинаковой. Это показано на примере SHOW CREATE TABLE:
Ниже вставляем данные в эту таблицу:
Здесь важно отметить несколько моментов:
  • Нам нужно использовать настройку input_format_import_nested_json, чтобы вставлять JSON как вложенную структуру. Без этого JSON пришлось бы выровнять, то есть:
  • Вложенные поля method, path и version нужно передавать как JSON-массивы, то есть:
К столбцам можно обращаться с помощью точечной нотации:
Обратите внимание: использование Array для подстолбцов означает, что можно задействовать весь спектр функций для работы с массивами, включая оператор ARRAY JOIN, — это полезно, если ваши столбцы содержат несколько значений.

flatten_nested=0

Это допускает произвольный уровень вложенности и означает, что вложенные столбцы остаются единым массивом Tuple — фактически они становятся эквивалентны Array(Tuple). Это предпочтительный и зачастую самый простой способ использовать JSON с Nested. Как показано ниже, для этого достаточно лишь того, чтобы все объекты были представлены в виде списка. Ниже мы заново создаём таблицу и повторно вставляем строку:
Здесь стоит отметить несколько важных моментов:
  • input_format_import_nested_json не требуется для вставки.
  • Тип Nested сохраняется в SHOW CREATE TABLE. По сути, этот столбец представляет собой Array(Tuple(Nested(method LowCardinality(String), path String, version LowCardinality(String))))
  • Поэтому request нужно вставлять как массив, то есть:
К столбцам снова можно обращаться с помощью точечной нотации:

Пример

Более полный пример приведённых выше данных доступен в публичном бакете S3 по адресу: s3://datasets-documentation/http/.
С учётом ограничений и входного формата JSON мы вставляем этот пример набора данных с помощью следующего запроса. Здесь мы устанавливаем flatten_nested=0. Следующий оператор вставляет 10 миллионов строк, поэтому его выполнение может занять несколько минут. При необходимости добавьте LIMIT:
Чтобы выполнять запросы к этим данным, нам нужно обращаться к полям request как к массивам. Ниже приведена сводка по ошибкам и HTTP-методам за фиксированный период времени.

Использование парных массивов

Парные массивы обеспечивают баланс между гибкостью представления JSON в виде String и производительностью более структурированного подхода. Схема остаётся гибкой, так как в корень потенциально можно добавлять новые поля. Однако это требует значительно более сложного синтаксиса запросов и не совместимо со вложенными структурами. В качестве примера рассмотрим следующую таблицу:
Чтобы выполнить вставку в эту таблицу, нужно представить JSON в виде списка ключей и значений. Следующий запрос показывает, как для этого использовать JSONExtractKeysAndValues:
Обратите внимание, что столбец request по-прежнему остаётся вложенной структурой, представленной в виде строки. Мы можем добавлять любые новые ключи в корневой объект. Также в самом JSON могут быть произвольные различия. Чтобы выполнить вставку в нашу локальную таблицу, выполните следующее:
Для запроса к этой структуре необходимо использовать функцию indexOf, чтобы определить индекс нужного ключа (он должен соответствовать порядку значений). Это позволяет обращаться к столбцу типа Array values, то есть values[indexOf(keys, 'status')]. Для столбца request по-прежнему требуется метод парсинга JSON — в данном случае simpleJSONExtractString.
Последнее изменение 3 июля 2026 г.