Используйте подход на уровне объектаДля разных объектов в рамках одной и той же схемы можно использовать разные методы. Например, для одних объектов лучше всего подходит тип
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_VALUE(body, '$.versions[0].created').
Функции String значительно медленнее (> 10x), чем явные преобразования типов с индексами. Приведённые выше запросы всегда требуют полного сканирования таблицы и обработки каждой строки. Хотя на небольшом наборе данных, подобном этому, такие запросы всё равно будут выполняться быстро, на более крупных наборах данных производительность снизится.
Гибкость этого подхода достигается ценой заметных потерь в производительности и усложнения синтаксиса, поэтому его следует использовать только для очень динамичных объектов в схеме.
Простые 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):
request требуется синтаксис Map, например:
Map для работы с этим типом; они описаны здесь. Если ваши данные не имеют единого типа, можно использовать функции для необходимого приведения типов.
Значения объектов
ТипMap также можно использовать для объектов, содержащих вложенные объекты, если для последних сохраняется согласованность типов.
Предположим, что ключ tags в нашем объекте persons требует согласованной структуры, в которой вложенный объект для каждого tag содержит столбцы name и time. Упрощённый пример такого JSON-документа может выглядеть следующим образом:
Map(String, Tuple(name String, time DateTime)), как показано ниже:
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/.
flatten_nested=0.
Следующий оператор вставляет 10 миллионов строк, поэтому его выполнение может занять несколько минут. При необходимости добавьте LIMIT:
Использование парных массивов
Парные массивы обеспечивают баланс между гибкостью представления JSON в виде String и производительностью более структурированного подхода. Схема остаётся гибкой, так как в корень потенциально можно добавлять новые поля. Однако это требует значительно более сложного синтаксиса запросов и не совместимо со вложенными структурами. В качестве примера рассмотрим следующую таблицу:JSONExtractKeysAndValues:
indexOf, чтобы определить индекс нужного ключа (он должен соответствовать порядку значений). Это позволяет обращаться к столбцу типа Array values, то есть values[indexOf(keys, 'status')]. Для столбца request по-прежнему требуется метод парсинга JSON — в данном случае simpleJSONExtractString.