Learning Platform
Глоссарий Troubleshooting
Урок 16.02 · 40 мин
Продвинутый
Lance v2Container FormatAdaptive EncodingMini-BlockProtobufPage LayoutFooterColumn MetadataStructural EncodingEncoding Pipeline

Lance v2 Format

В предыдущем уроке мы разобрали архитектуру Lance на уровне dataset’ов: фрагменты, manifest’ы, version log. Теперь спустимся на уровень отдельного data file — формат Lance v2, опубликованный в апреле 2025 (arxiv 2504.15247).

Lance v2 — это радикально другой подход к колоночному хранению по сравнению с Parquet. Его ключевая идея: контейнерный формат без встроенной системы типов и фиксированного набора кодировок. Вместо этого — protobuf “any” messages, адаптивные structural encodings и extensible encoding pipeline.

NOTE

Parquet определяет фиксированный набор из ~10 кодировок (PLAIN, RLE, DICTIONARY, DELTA_BINARY_PACKED и др.) и жёсткую иерархию типов. Добавление новой кодировки в Parquet требует изменения спецификации формата — процесс, занимающий месяцы через Apache governance. Lance v2 позволяет добавлять произвольные кодировки без изменения спецификации.

Философия контейнерного формата

Lance v2 называет себя “columnar container format” — формат-контейнер, который хранит данные, но не навязывает, как они закодированы:

Container Format: Lance v2 vs Parquet

Parquet (фиксированный)

Parquet: формат определяет систему типов (INT32, INT64, BYTE_ARRAY, FIXED_LEN_BYTE_ARRAY, FLOAT, DOUBLE, BOOLEAN) и фиксированный набор кодировок. Reader обязан поддерживать все кодировки — иначе файл нечитаем.
Типы7 физических типов + logical type annotations. Каждый логический тип маппится на физический: TIMESTAMP → INT64, DECIMAL → FIXED_LEN_BYTE_ARRAY. Система типов зафиксирована в спецификации — расширение через Thrift metadata.
Кодировки~10 кодировок: PLAIN, RLE, DICTIONARY, DELTA_BINARY_PACKED, DELTA_BYTE_ARRAY, DELTA_LENGTH_BYTE_ARRAY, RLE_DICTIONARY, BYTE_STREAM_SPLIT. Новая кодировка = изменение Thrift-спецификации + обновление всех reader'ов.

Lance v2 (контейнер)

Lance v2: контейнер не знает о типах данных и кодировках. Кодировки хранятся как protobuf 'any' messages — самоописывающие бинарные блобы. Reader интерпретирует encoding на лету.
ТипыНет встроенной системы типов. Используется Arrow schema — формат наследует типы из Arrow (50+ типов: int8-int64, float16-float64, utf8, binary, list, struct, map, dictionary, duration, interval...). Arrow schema = единственный контракт.
КодировкиКодировки — protobuf 'any' messages. Каждая колонка хранит описание своей кодировки как бинарный blob. Новая кодировка = новый protobuf message. Старые reader'ы пропускают неизвестные кодировки (graceful degradation).

Это даёт три конкретных преимущества:

Преимущества контейнерной модели
РасширяемостьДобавить новую кодировку — написать protobuf message + encoder/decoder. Не нужно менять спецификацию формата. Не нужно обновлять все reader'ы. Старые reader'ы просто не используют новую кодировку — graceful degradation.
АдаптивностьWriter выбирает оптимальную кодировку для каждого chunk данных на основе статистики: cardinality, null ratio, value distribution. Разные chunks одной колонки могут использовать разные кодировки — mini-block vs all-null vs dictionary.
Forward CompatibilityReader v1 может читать файл, записанный writer v3 — он просто игнорирует unknown encoding types и читает данные через fallback (PLAIN). Parquet: если reader не знает кодировку — файл нечитаем.

Encoding Pipeline

Lance v2 использует двухступенчатый encoding pipeline: сначала logical encoding (компрессия значений), затем structural encoding (физический layout на диске):

Encoding Pipeline: logical → structural

Arrow Array (входные данные)

Входные данные: Arrow array (например, Int64Array из 1M значений). Encoding pipeline обрабатывает каждую колонку независимо. Pipeline запускается при записи фрагмента.
Шаг 1: Logical Encoding
DictionaryЕсли cardinality низкая (< 50% уникальных значений) — dictionary encoding. Значения заменяются на индексы в словаре. Словарь хранится отдельно. Аналог Parquet DICTIONARY, но с per-chunk решением.
DeltaЕсли значения монотонно растут (timestamps, sequence IDs) — delta encoding. Хранятся разницы между соседними значениями. Меньший диапазон → лучшее bitpacking downstream.
PlainFallback: если ни одна специализированная кодировка не даёт выигрыша — plain encoding. Значения хранятся как есть в бинарном формате Arrow.
Шаг 2: Structural Encoding
Mini-BlockОсновной structural encoding: данные разбиваются на mini-blocks фиксированного размера. Каждый mini-block содержит: заголовок (encoding type, compressed size, row count) + compressed data. Обеспечивает random access: offset строки = block_index * block_size + offset_in_block.
All-NullОптимизация для sparse колонок: если все значения в chunk — null, то хранится только маркер ALL_NULL. Нет data bytes. При чтении: вернуть null array указанной длины. Для ML: missing features, sparse embeddings.

Encoded Data + Protobuf Descriptor

Результат: закодированные данные + protobuf descriptor кодировки. Descriptor содержит: тип logical encoding, тип structural encoding, параметры (block size, dictionary size). Этот descriptor хранится в column metadata файла.

Adaptive Structural Encoding

Ключевое нововведение v2 — адаптивный выбор structural encoding на уровне chunk’ов:

Adaptive Encoding: решение per-chunk

Chunk (~4096 строк)

Encoder анализирует каждый chunk (по умолчанию ~4096 строк) и выбирает оптимальный structural encoding. Решение принимается на основе простых эвристик: null ratio, data width, value distribution.
анализ
null_ratio = 100%Все строки в chunk — null. Используем ALL_NULL: 0 байт данных, только маркер в metadata. Для sparse колонок (90%+ null) — колоссальная экономия.
Фиксированная ширинаЗначения фиксированного размера (int32, int64, float32, float64): MINI_BLOCK encoding. Данные разбиваются на блоки размером = row_count * value_width. Random access: offset = row_index * value_width. O(1).
Переменная ширинаСтроки, бинарные данные (variable-width): MINI_BLOCK + offsets array. Дополнительный массив offset'ов для каждой строки. Random access: offset_array[row_index] → start, offset_array[row_index+1] → end.
TIP

Адаптивность per-chunk означает, что одна колонка может использовать разные structural encodings в разных chunk’ах. Например, колонка description в первых 4096 строках — 100% null (ALL_NULL), в следующих 4096 — заполнена (MINI_BLOCK + offsets). Parquet выбирает encoding per-page, но из фиксированного набора.

Page Layout

Lance v2 data file организован как последовательность pages — но page в Lance v2 отличается от page в Parquet:

Структура Lance v2 data file

Lance v2 Data File

Lance v2 data file: один файл для одной или нескольких колонок в рамках фрагмента. Формат: data pages → column metadata → global metadata → footer. Чтение начинается с footer (как в Parquet), но layout принципиально другой.
Data PagesПоследовательность data pages: каждый page содержит один chunk данных одной колонки. Page = structural encoding header + encoded data. Размер page варьируется (в отличие от Parquet, где page фиксирован ~64KB).
Column MetadataМетаданные каждой колонки: массив protobuf descriptors для каждого chunk. Descriptor содержит: тип encoding, offset page в файле, compressed size, row count. Позволяет найти нужный chunk за O(1).
FooterФиксированный размер (16 байт): magic bytes + version + offset к column metadata section. Reader начинает с footer → column metadata → выборочные data pages. Минимальный I/O для metadata read.

Protobuf-based Column Metadata

Каждая колонка описывается protobuf message, содержащим информацию о всех chunk’ах:

Column Metadata: protobuf descriptors

Column Metadata (protobuf)

Column metadata — центральный элемент self-describing формата. Содержит полное описание всех кодировок всех chunk'ов. Reader, получив column metadata, знает как декодировать каждый байт data section без дополнительных запросов.
Chunk 0 DescriptorОписание первого chunk: logical_encoding = DICTIONARY (cardinality 15%), structural_encoding = MINI_BLOCK (fixed-width indices), page_offset = 0, compressed_size = 4096, num_rows = 4096, dictionary_size = 150.
Chunk 1 DescriptorОписание второго chunk: logical_encoding = PLAIN (high cardinality), structural_encoding = ALL_NULL (100% null). Нет data bytes — только descriptor. При чтении: вернуть null array длиной 4096.
Chunk N DescriptorКаждый chunk может использовать свою комбинацию logical + structural encoding. Protobuf 'any' позволяет хранить произвольные параметры: block_size, dictionary, compression_codec, custom_params.

Protobuf “any” message для encoding descriptor:

message ColumnEncoding {
 // Logical encoding type
 google.protobuf.Any logical_encoding = 1;
 // Structural encoding type
 google.protobuf.Any structural_encoding = 2;
 // Page offset in data file
 uint64 page_offset = 3;
 // Compressed size in bytes
 uint64 compressed_size = 4;
 // Number of rows in chunk
 uint32 num_rows = 5;
 // Column statistics
 ColumnStats stats = 6;
}

message DictionaryEncoding {
 uint32 dictionary_size = 1;
 uint32 index_bits = 2;
 bytes dictionary_data = 3;
}

message MiniBlockEncoding {
 uint32 block_size = 1;
 uint32 value_width = 2;
 CompressionCodec compression = 3;
}
WARNING

Protobuf any — механизм расширяемости. Если reader v1.0 встречает encoding type, добавленный в v2.0, он видит google.protobuf.Any с неизвестным type URL. Поведение: пропустить chunk, вернуть null array (или fallback к PLAIN, если доступен). Парсер не падает — формат forward-compatible.

Mini-Block Encoding

Mini-block — основной structural encoding, обеспечивающий O(1) random access:

Mini-Block: random access vs Parquet page

Parquet: чтение строки #42

Parquet page: ~64KB, содержит множество значений. Для доступа к строке N нужно: загрузить page → декодировать ВСЕ значения в page → найти значение #N. Decode — обязателен (RLE, DICTIONARY требуют последовательной обработки).
1. Найти pageRow group index → page index: column_chunk.offset_index (если есть) или sequential scan. Для 10GB файла с 1000 row groups — O(log N) lookup.
2. Загрузить pageЗагрузить 64KB page с диска. Даже если нужна одна строка — читаем весь page. I/O amplification: 64KB ради 8 байт (одно int64 значение).
3. Декодировать pageДекодировать ВСЕ значения в page: распаковать RLE, разыменовать dictionary, debit-pack. CPU cost: O(page_size), даже если нужна одна строка. Для 1000 random reads = 1000 × page decode.

Lance: чтение строки #42

Lance mini-block: данные разбиты на блоки фиксированного размера. Каждый блок — самостоятельный. Для доступа к строке N: вычислить block_index + offset_in_block, загрузить один блок, прочитать значение.
1. Вычислить offsetАрифметика: block_index = row_index / block_size, offset_in_block = row_index % block_size. Для fixed-width типов: byte_offset = block_index * block_bytes + offset_in_block * value_width. O(1), нет I/O.
2. Загрузить блокЗагрузить один mini-block (обычно ~256 байт - 4KB). На порядки меньше, чем Parquet page. Для fixed-width: можно загрузить только одно значение (8 байт для int64).
3. Прочитать значениеДля plain encoding: прямое чтение байтов. Для dictionary: разыменовать один индекс. Нет page-level decode. CPU cost: O(1). Для 1000 random reads = 1000 × O(1) = O(1000).

Именно это обеспечивает заявленный 100x выигрыш в random access: вместо загрузки + декодирования 64KB page ради одной строки — прямой доступ к байтам значения.

v1 → v2: эволюция формата

Lance v1 (2022-2024) использовал другой подход к колоночному хранению. v2 — переработка на основе опыта продакшена:

Эволюция: v1 → v2

Lance v1 (2022-2024)

Lance v1: каждая колонка = отдельный файл внутри фрагмента. Encoding зафиксирован при создании dataset. Нет adaptive encoding per-chunk. Footer в каждом column file.
Ограничения v11) Много файлов: N колонок = N файлов per fragment. Object storage: high metadata overhead (LIST operations). 2) Фиксированная кодировка: выбор при создании, не адаптируется к данным. 3) Нет per-chunk решений: вся колонка кодируется одинаково.

Lance v2 (2025+)

Lance v2: все колонки в одном файле (или группах). Adaptive encoding per-chunk. Protobuf 'any' для расширяемости. Оптимизирован для object storage: один GET per read (no LIST).
Улучшения v21) Один файл per fragment: минимальные metadata operations на object storage. 2) Adaptive encoding: кодировка выбирается per-chunk на основе статистик. 3) Protobuf 'any': произвольные кодировки без изменения формата. 4) Mini-block: O(1) random access.

Сравнение Encoding-подходов

Как Lance v2 encoding pipeline соотносится с подходами Parquet и Apache Arrow:

Encoding: Parquet vs Lance v2 vs Arrow
АспектКлючевые различия в подходах к кодированию данных между тремя форматами.
ParquetApache Parquet: фиксированные кодировки, Thrift metadata, page-level granularity. Стандарт аналитического хранения с 2013 года.
Lance v2Lance v2: extensible кодировки, protobuf metadata, chunk-level adaptive. Оптимизирован для random access и ML workloads.
ArrowApache Arrow: in-memory формат, нет кодировок (данные в raw format). Оптимизирован для CPU cache и SIMD. Zero-copy IPC.
Набор кодировокКоличество и расширяемость доступных кодировок.
~10 кодировок, зафиксированных в Thrift спецификации. Добавление новой требует обновления спецификации и всех реализаций.
Неограниченный: protobuf 'any' позволяет произвольные кодировки. Writer определяет encoding, reader интерпретирует descriptor.
Нет кодировок: данные хранятся в raw формате. Значение int64 = 8 байт. Массив из 1M int64 = 8MB. Сжатие — не цель Arrow.
GranularityУровень принятия решений о кодировании: page, chunk, column, file.
Per-page: каждый page (~64KB) кодируется одинаково. Разные pages одной колонки могут использовать разные кодировки, но решение — при записи, не адаптивное.
Per-chunk (~4096 строк): каждый chunk анализируется и кодируется оптимально. Разные chunks одной колонки — разные кодировки на основе данных.
N/A: Arrow хранит данные как raw arrays. Нет выбора кодировки — всегда plain.
Random accessСкорость доступа к произвольной строке по индексу.
Page decode: O(page_size). Нужно загрузить и декодировать весь page. Для 64KB page с int64: декодировать ~8000 значений ради одного.
O(1): mini-block encoding позволяет вычислить byte offset арифметически. Для fixed-width: загрузить value_width байт напрямую.
O(1): массив в памяти, доступ по индексу. Лучший возможный random access — но Arrow не предназначен для persistent storage.
Forward compatВозможность читать файлы, записанные более новой версией writer'а.
Ограниченная: reader обязан поддерживать все encoding types в файле. Неизвестная кодировка = ошибка чтения. Parquet решает это медленным обновлением спецификации.
Встроенная: protobuf 'any' позволяет graceful degradation. Неизвестный encoding → пропустить chunk или fallback к PLAIN. Файл остаётся читаемым.
Стабильный: Arrow IPC формат фиксирован, нет кодировок → нет проблемы forward compatibility.

Write Path: выбор кодировки

При записи данных Lance v2 writer проходит через pipeline выбора кодировки для каждого chunk каждой колонки:

Write Path: encoding selection pipeline

Chunk: 4096 строк

Входной chunk: ~4096 строк одной колонки. Writer анализирует данные перед кодированием: вычисляет null ratio, cardinality, value range, width uniformity.
анализ
Statisticsnull_ratio: доля null значений (0.0 - 1.0). cardinality: количество уникальных значений / total. value_range: max - min для числовых. is_sorted: монотонно ли возрастают значения. avg_length: средняя длина строковых значений.
решение
null = 100%Если все значения null → ALL_NULL structural encoding. Нет data bytes, только descriptor. Экономия: chunk_size × value_width байт. Для sparse ML features — типичный случай.
cardinality < 50%Низкая cardinality → dictionary encoding + mini-block для indices. Dictionary хранится inline в descriptor. Для categorical features, enum-like columns.
is_sorted = trueМонотонные данные → delta encoding + mini-block. Дельты обычно маленькие → лучшее сжатие downstream. Для timestamps, auto-increment IDs, sequence numbers.
defaultFallback: plain encoding + mini-block. Данные хранятся как есть, но в mini-block формате для random access. Для high-cardinality строк, binary данных, embedding vectors.

Read Path: lazy decode

При чтении Lance v2 применяет lazy decoding — данные декодируются только когда запрошены:

Read Path: lazy decode
SELECT name, embedding WHERE id = 42Запрос: SELECT name, embedding FROM dataset WHERE id = 42. Reader: не декодировать все колонки — только name и embedding. Не декодировать все строки — только id = 42.
1. Footer → Column MetadataЗагрузить footer (16 байт) → offset к column metadata. Загрузить column metadata для колонок id, name, embedding (только нужные колонки). Один I/O request.
2. Find chunkИз column metadata для 'id': найти chunk, содержащий строку 42. Chunk stats (min/max) позволяют пропустить chunks где id=42 невозможен. Binary search по chunk ranges.
3. Decode only neededЗагрузить и декодировать только: chunk колонки 'id' (для поиска строки), mini-block колонки 'name' (для значения), mini-block колонки 'embedding' (для значения). Не декодировать остальные chunks и колонки.

Arrow RecordBatch (zero-copy)

Результат: Arrow RecordBatch с одной строкой, двумя колонками. Zero-copy transfer в Pandas/DuckDB/PyTorch. Суммарный I/O: ~3 mini-block reads вместо full page decodes.
NOTE

Lazy decode — ключ к производительности. В Parquet reader загружает и декодирует целый page даже если нужна одна строка. Lance v2 mini-block encoding позволяет загрузить и декодировать только нужный блок — часто это десятки байт вместо десятков килобайт.

Итоги

Lance v2 — формат, построенный на принципе “контейнер не навязывает содержимое”:

Ключевые принципы Lance v2
ContainerНет встроенных типов, нет фиксированных кодировок. Protobuf 'any' для extensibility. Arrow schema для типизации. Формат — контейнер, не диктатор.
AdaptiveКодировка выбирается per-chunk на основе данных. Разные chunks одной колонки — разные кодировки. ALL_NULL для sparse, DICTIONARY для categorical, DELTA для monotonic.
Random AccessMini-block encoding: O(1) доступ к строке. Нет page decode. Offset вычисляется арифметически. 100x быстрее Parquet для random reads.
Forward CompatUnknown encoding → graceful skip. Старый reader читает файл нового writer. Нет breaking changes при добавлении кодировок. Протокол evolution без координации.

В следующем уроке мы разберём ML-специфичные возможности Lance: 100x random access через sliceable encodings, встроенный векторный поиск (IVF-PQ, HNSW), мультимодальные данные и Python API.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Lance v2 reader (version 1.0) встречает data file, записанный writer version 3.0, который использует новую кодировку 'ZigZag-SIMD' (неизвестную reader v1.0). Что произойдёт при чтении?

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

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

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

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