Learning Platform
Глоссарий Troubleshooting
Урок 03.06 · 35 мин
Продвинутый
ParquetDremelNested DataDefinition LevelsRepetition LevelsShreddingAssembly

Вложенные данные и Dremel

Проблема вложенных данных

Parquet хранит данные по колонкам. Для плоской таблицы это просто: каждая колонка — массив значений. Но реальные данные часто вложенные: JSON с массивами, struct внутри struct, repeated groups.

Как сохранить {"name": "Alice", "phones": ["+49...", "+1..."]} в колоночном формате, не потеряв структуру?

Ответ: Dremel encoding — алгоритм из одноимённой статьи Google (2010), который разбирает (shred) вложенные записи на плоские колонки с дополнительными уровнями, а затем собирает (assemble) обратно.

Вложенная запись → плоские колонки

Вложенная запись (JSON)

Record 0Запись с required name и repeated phones. Два телефона — repeated group с двумя элементами.
Record 1Запись без телефонов — phones отсутствует (null). Dremel должен закодировать отсутствие repeated group.

Плоские колонки

Column: nameПростая required колонка. Не нуждается в definition/repetition levels — значение всегда присутствует.
Column: phonesRepeated колонка. Содержит 3 значения (2 от Alice, 0 от Bob), но нужны def/rep levels, чтобы понять группировку.
+ Definition LevelsПоказывают, на каком уровне вложенности значение определено. 0 = top-level null, 1 = list есть, но элемент null, 2 = значение присутствует.
+ Repetition LevelsПоказывают, на каком уровне повторяется элемент. 0 = новая запись, 1 = новый элемент в том же repeated group.

Definition Levels

Definition level отвечает на вопрос: «На каком уровне вложенности значение определено (не null)?»

Для каждого поля в схеме вычисляется max definition level — количество optional/repeated полей на пути от корня к полю. Значение definition level говорит, до какого уровня путь определён:

  • def = max_def → значение присутствует
  • def < max_def → null на уровне def + 1
Вычисление Definition Levels

Схема

message Document {
required string doc_id; // max_def = 0
optional group links { // +1 (optional)
repeated string forward; // +1 (repeated) → max_def = 2
}
}
def = 0links = null. Вся optional группа отсутствует. Нет смысла проверять forward — родительский уровень null.
def = 1links определён (не null), но forward пуст — repeated group существует, но в нём 0 элементов.
def = 2Полный путь определён: links не null, forward содержит значение. Это единственный level, при котором фактически хранится строковое значение.
max_def = 2Max definition level для links.forward = 2 (1 optional + 1 repeated). Вычисляется один раз из схемы.
TIP

Required поля не добавляют к definition level — они не могут быть null. Поле doc_id (required) имеет max_def = 0, и ему вообще не нужны definition levels.

Repetition Levels

Repetition level отвечает на вопрос: «На каком уровне вложенности начинается новый элемент?»

  • rep = 0 → новая запись (новый корневой record)
  • rep = 1 → новый элемент в repeated group уровня 1
  • rep = 2 → новый элемент в repeated group уровня 2
  • И так далее…

Repetition level нужен только для repeated полей. Required и optional поля не повторяются.

Repetition Levels на примере
Record 0: { doc_id: "d1", links: { forward: ["url_a", "url_b", "url_c"] } }
Record 1: { doc_id: "d2", links: null }
Record 2: { doc_id: "d3", links: { forward: ["url_d"] } }

Column: links.forward

url_aПервое значение Record 0. rep=0 → новая запись. def=2 → значение присутствует.
url_bВторой элемент в forward[] Record 0. rep=1 → повторение на уровне repeated field. def=2 → значение есть.
url_cТретий элемент в forward[] Record 0. rep=1 → ещё один элемент в том же массиве.
nullRecord 1: links=null. rep=0 → новая запись. def=0 → null на уровне 0 (optional group отсутствует). Значение не хранится.
url_dRecord 2: один элемент в forward[]. rep=0 → новая запись. def=2 → значение присутствует.

Shredding: запись → плоские колонки

Record shredding — процесс разбиения вложенной записи на плоские колонки с definition/repetition levels. Это происходит при записи Parquet-файла.

Алгоритм обходит каждую запись по дереву схемы, вычисляя def/rep levels для каждого листового поля:

Shredding: вложенная запись → колонки + levels

Вложенная запись (input)

Входная запись — вложенная структура. Может быть JSON, Avro record, Spark Row — любой формат с optional/repeated полями.

Обход дерева схемы (DFS)

Рекурсивный обход схемы. Для каждого листового поля: пройти путь от корня, посчитать количество определённых optional/repeated уровней → def level. Определить, на каком уровне повторяется → rep level.
Шаг 1: def levelДля каждого значения: считаем, сколько optional/repeated предков определены (не null). Если значение есть → def = max_def. Если null → def = уровень, на котором путь обрывается.
Шаг 2: rep levelДля каждого значения: на каком уровне repeated field это значение — первый элемент (rep=parent) или продолжение (rep=current_level). Для первого значения записи → rep = 0.
Шаг 3: записьТройка (value, def, rep) дописывается в буфер колонки. Def/rep levels кодируются RLE (Run-Length Encoding) — эффективно для повторяющихся паттернов.
NOTE

Definition и repetition levels кодируются RLE (Run-Length Encoding) на каждой странице. Для данных с регулярной структурой (все записи одной формы) RLE сжимает levels почти до нуля — например, 50 000 одинаковых def=2, rep=0 кодируются парой байт.

Assembly: плоские колонки → записи

Record assembly — обратный процесс при чтении. Из плоских колонок с def/rep levels воссоздаётся оригинальная вложенная структура.

Алгоритм использует конечный автомат (finite state machine), где:

  • Каждый leaf-field — это reader, который выдаёт следующее значение + def/rep levels
  • Repetition level определяет, когда закрывать текущий repeated group и начинать новый
  • Definition level определяет, какие промежуточные узлы создавать как null
Assembly: колонки + levels → вложенная запись
Column: doc_idПростая required колонка. Значения читаются последовательно. Нет def/rep levels (required field).
Column: links.forwardКолонка с def/rep levels. Значения и null перемешаны в одном потоке.
Def/Rep levelsПары (rep, def) определяют границы записей и наличие значений. Это ключ к восстановлению структуры.

FSM Assembly Engine

Конечный автомат читает значения из всех leaf-колонок синхронно. rep=0 → начинает новую запись. rep=1 → добавляет элемент в текущий repeated group. def < max_def → вставляет null на нужном уровне.
Record 0Три значения forward с rep=0,1,1. rep=0 начинает запись, rep=1 добавляет элементы в тот же массив.
Record 1rep=0 → новая запись, def=0 → links = null. Значение forward не хранится — null на уровне optional group.
Record 2rep=0 → новая запись, def=2 → значение есть. Один элемент forward.

Max levels из схемы

Max definition и repetition levels вычисляются один раз из схемы Parquet при открытии файла. Правила:

Тип поляВлияние на max_defВлияние на max_rep
required+0+0
optional+1+0
repeated+1+1

Пример для глубоко вложенной схемы:

message AddressBook {
 required string owner; // max_def=0, max_rep=0
 repeated group contacts { // +1 def, +1 rep
 required string name; // max_def=1, max_rep=1
 optional group phone_info { // +1 def, +0 rep
 repeated string numbers; // +1 def, +1 rep → max_def=3, max_rep=2
 }
 }
}

Поле contacts.phone_info.numbers имеет max_def=3 (repeated + optional + repeated) и max_rep=2 (repeated + repeated). Это означает: 4 возможных definition levels (0, 1, 2, 3) и 3 возможных repetition levels (0, 1, 2).

WARNING

Глубоко вложенные схемы (5+ уровней) приводят к высоким max levels, что увеличивает количество бит на значение для RLE encoding levels. На практике большинство Parquet-схем имеют max_def ≤ 3. Если вложенность глубже — стоит подумать о денормализации.

Альтернатива Dremel: VARIANT logical type (Parquet 2.11+)

Dremel хорош для известных, стабильных схем с прогнозируемой вложенностью. Но реальные JSON-данные часто schema-flexible: события телеметрии, webhook payloads, log-events — каждая запись может иметь свой набор полей. Зашиывать такой JSON через Dremel требует либо «фиксации» схемы (теряется гибкость), либо очень глубокой схемы со всеми возможными полями (большой overhead).

Parquet 2.11 (март 2025) добавил VARIANT logical type — сжатая бинарная репрезентация semi-structured данных, аналог BSON / Snowflake VARIANT, но интегрированная с колоночным хранением Parquet.

VARIANT в Parquet: метаданные + значение
Schema (logical type)VARIANT аннотирует не примитив, а group из двух required binary полей. metadata содержит словарь ключей объектов, value — закодированное дерево.
metadata bufferСодержит версию формата и упорядоченный словарь строковых ключей объекта. Ключи дедуплицируются — при повторах одного и того же ключа в разных записях платим только за индекс.
value bufferБинарное дерево: header byte (тип + variant info) → payload. Типы: NULL, boolean, int8/16/32/64, float, decimal, date, timestamp, string, binary, object, array.

Когда выбирать VARIANT, а когда Dremel

КритерийDremel (Definition/Repetition Levels)VARIANT
Известная стабильная схемаДа — оптимальноНет — overhead
Schema-flexible JSON (event-streams)Плохо — раздутая схемаДа — оптимально
Колоночное чтение поля без чтения остальногоДа (можно прочитать одну колонку)Частично (shredded VARIANT)
Predicate pushdown по полюПолный (через Statistics + ColumnIndex)Через shredded VARIANT (Parquet 2.11+)
Размер на диске для regular dataМинимальный+10–20% overhead
Записать запись с новым полем без миграции схемыНет — нужно altered schemaДа — просто добавить ключ

Shredded VARIANT (расширение в Parquet 2.11+) — гибрид: «горячие» поля (часто запрашиваемые) выделяются в отдельные типизированные колонки с их собственными Statistics и ColumnIndex; «холодные» хранятся в общем VARIANT-блобе. Это даёт predicate pushdown по горячим путям без потери flexibility для редких полей.

TIP

Используйте VARIANT для landing-zone данных (raw events, webhook payloads), где схема непредсказуема. Используйте Dremel для curated-слоя (Silver/Gold), где схема стабильна и важна максимальная производительность колоночных запросов. Часто оба слоя сосуществуют в одном lakehouse.

Ключевые выводы

  1. Dremel encoding позволяет Parquet хранить вложенные данные в колоночном формате без потери структуры
  2. Definition levels кодируют «на каком уровне значение определено (не null)» — от 0 до max_def
  3. Repetition levels кодируют «на каком уровне начинается повторение» — от 0 (новая запись) до max_rep
  4. Shredding разбирает записи при записи, assembly собирает при чтении — оба используют DFS по дереву схемы
  5. Levels кодируются RLE — эффективно для регулярных структур, где паттерны повторяются
  6. Max levels вычисляются из схемы: optional добавляет +1 def, repeated добавляет +1 def и +1 rep
  7. VARIANT logical type (Parquet 2.11, март 2025) — альтернатива Dremel для schema-flexible JSON; shredded VARIANT даёт predicate pushdown по горячим полям
ClickHouse: JSON / Variant / Dynamic типы

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое 'shredding' (разбиение) в контексте Dremel-кодирования Parquet?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7