Вложенные данные и Dremel
Проблема вложенных данных
Parquet хранит данные по колонкам. Для плоской таблицы это просто: каждая колонка — массив значений. Но реальные данные часто вложенные: JSON с массивами, struct внутри struct, repeated groups.
Как сохранить {"name": "Alice", "phones": ["+49...", "+1..."]} в колоночном формате, не потеряв структуру?
Ответ: Dremel encoding — алгоритм из одноимённой статьи Google (2010), который разбирает (shred) вложенные записи на плоские колонки с дополнительными уровнями, а затем собирает (assemble) обратно.
Вложенная запись (JSON)
Плоские колонки
Definition Levels
Definition level отвечает на вопрос: «На каком уровне вложенности значение определено (не null)?»
Для каждого поля в схеме вычисляется max definition level — количество optional/repeated полей на пути от корня к полю. Значение definition level говорит, до какого уровня путь определён:
def = max_def→ значение присутствуетdef < max_def→ null на уровнеdef + 1
Схема
Required поля не добавляют к definition level — они не могут быть null. Поле doc_id (required) имеет max_def = 0, и ему вообще не нужны definition levels.
Repetition Levels
Repetition level отвечает на вопрос: «На каком уровне вложенности начинается новый элемент?»
rep = 0→ новая запись (новый корневой record)rep = 1→ новый элемент в repeated group уровня 1rep = 2→ новый элемент в repeated group уровня 2- И так далее…
Repetition level нужен только для repeated полей. Required и optional поля не повторяются.
Column: links.forward
Shredding: запись → плоские колонки
Record shredding — процесс разбиения вложенной записи на плоские колонки с definition/repetition levels. Это происходит при записи Parquet-файла.
Алгоритм обходит каждую запись по дереву схемы, вычисляя def/rep levels для каждого листового поля:
Вложенная запись (input)
Входная запись — вложенная структура. Может быть JSON, Avro record, Spark Row — любой формат с optional/repeated полями.Обход дерева схемы (DFS)
Рекурсивный обход схемы. Для каждого листового поля: пройти путь от корня, посчитать количество определённых optional/repeated уровней → def level. Определить, на каком уровне повторяется → rep level.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
FSM Assembly Engine
Конечный автомат читает значения из всех leaf-колонок синхронно. rep=0 → начинает новую запись. rep=1 → добавляет элемент в текущий repeated group. def < max_def → вставляет null на нужном уровне.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).
Глубоко вложенные схемы (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, а когда 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 для редких полей.
Используйте VARIANT для landing-zone данных (raw events, webhook payloads), где схема непредсказуема. Используйте Dremel для curated-слоя (Silver/Gold), где схема стабильна и важна максимальная производительность колоночных запросов. Часто оба слоя сосуществуют в одном lakehouse.
Ключевые выводы
- Dremel encoding позволяет Parquet хранить вложенные данные в колоночном формате без потери структуры
- Definition levels кодируют «на каком уровне значение определено (не null)» — от 0 до max_def
- Repetition levels кодируют «на каком уровне начинается повторение» — от 0 (новая запись) до max_rep
- Shredding разбирает записи при записи, assembly собирает при чтении — оба используют DFS по дереву схемы
- Levels кодируются RLE — эффективно для регулярных структур, где паттерны повторяются
- Max levels вычисляются из схемы: optional добавляет +1 def, repeated добавляет +1 def и +1 rep
- VARIANT logical type (Parquet 2.11, март 2025) — альтернатива Dremel для schema-flexible JSON; shredded VARIANT даёт predicate pushdown по горячим полям