Learning Platform
Глоссарий Troubleshooting
Урок 16.01 · 40 мин
Продвинутый
LanceLanceDBFragmentManifestVersion LogDeletion VectorTime TravelAppend-OnlyArrow-NativeRandom Access

Lance Architecture

Parquet стал де-факто стандартом колоночного хранения, но его архитектура 2013 года имеет фундаментальные ограничения для современных workload’ов: произвольный доступ к строкам требует полного чтения page, нет встроенного векторного поиска, нет нативного версионирования данных. Lance — это Rust-based, Arrow-native формат, разработанный компанией LanceDB специально для ML/AI workload’ов, где эти ограничения критичны.

Проект развивается с 2022 года. Текущий формат — Lance v2 (апрель 2025, arxiv 2504.15247). В продакшене у LanceDB Cloud на петабайтных масштабах — multimodal GenAI компании, платформы для self-driving cars, e-commerce рекомендательные системы. Экосистема: PyArrow, Pandas, DuckDB, Polars, PyTorch.

NOTE

В Модуле 07 мы разобрали Apache Arrow — in-memory columnar формат с zero-copy. Lance строится на Arrow как фундаменте: все данные при чтении возвращаются как Arrow RecordBatch без копирования. Это ключевое отличие от Parquet, который при чтении декодирует данные из собственного формата в Arrow.

Почему Parquet недостаточен для ML

Parquet оптимизирован для аналитических scan’ов: sequential read колонок с predicate pushdown. Но ML-pipeline’ы требуют другой паттерн доступа:

Паттерны доступа: Analytics vs ML

Analytics Workload

Аналитические запросы: SELECT columns FROM table WHERE predicate. Последовательное чтение колонок, предикаты отсекают row groups. Parquet идеален для этого паттерна — columnar layout + statistics = эффективный scan.
ДоступSequential scan: читаем колонку целиком или по row groups. Predicate pushdown через min/max statistics, bloom filters. I/O паттерн — большие последовательные чтения (128KB+ pages).

Parquet = идеален

Parquet оптимален: columnar layout минимизирует I/O, dictionary encoding сжимает повторы, page-level statistics обеспечивают пропуск ненужных данных.

ML / AI Workload

ML workload: обучение модели требует случайных батчей строк (random mini-batches), vector search для retrieval (nearest neighbor queries), версионирование датасетов для воспроизводимости экспериментов.
ДоступRandom access: выбрать строки 42, 7891, 3 — три произвольные позиции. В Parquet нужно: найти row group → декодировать page → найти строку. Для каждой строки — отдельный I/O + декодирование целого page.

Parquet = неэффективен

Parquet неэффективен: random access требует чтения + декодирования целого page (64KB) ради одной строки. Нет нативного векторного индекса. Нет нативного версионирования.

Три конкретные проблемы Parquet для ML:

  1. Random access = full page decode. Parquet хранит данные в pages (~64KB). Чтобы прочитать строку #42, нужно найти row group, загрузить page, декодировать весь page (dictionary → RLE → значение). Для mini-batch из 1000 случайных строк — потенциально 1000 page read + decode. Lance решает это через sliceable encodings — прямой доступ к offset строки без декодирования page.

  2. Нет векторного поиска. ML-pipeline’ы требуют nearest-neighbor search по embedding-векторам (CLIP, sentence-transformers). Parquet не хранит векторные индексы — нужен отдельный vector database (Pinecone, Weaviate). Lance встраивает IVF-PQ и HNSW-индексы прямо в data fragments.

  3. Нет версионирования. Воспроизводимость ML-экспериментов требует возврата к конкретной версии датасета. Parquet — stateless файл. Lance добавляет append-only transaction log с version snapshots — аналог Delta Lake, но на уровне формата.

Фрагментная модель хранения

Lance организует данные в фрагменты — самодостаточные единицы хранения размером ~64 МБ. Каждый фрагмент содержит подмножество строк датасета:

Фрагментная модель Lance

Lance Dataset (директория)

Lance dataset — директория на файловой системе (или object storage). Содержит data fragments, manifest'ы, transaction log и опционально — векторные индексы. Формат самоописывающий — всё необходимое для чтения внутри директории.
Fragment 0Первый фрагмент: содержит строки 0-N (по умолчанию ~1M строк или ~64MB данных). Внутри — columnar data files в формате Lance v2. Каждая колонка хранится отдельно или группами для оптимизации I/O.
Fragment 1Второй фрагмент: следующий блок строк. Фрагменты не перекрываются по строкам — каждая строка принадлежит ровно одному фрагменту. ID фрагмента + offset внутри фрагмента = глобальный row ID.
Fragment NПоследний фрагмент: может быть меньше 64MB. При append новых данных создаётся новый фрагмент (или дополняется существующий). Фрагменты иммутабельны после создания — update работает через deletion vectors.

Каждый фрагмент содержит:

Структура фрагмента Lance

Fragment (единица хранения)

Fragment — единица I/O и параллелизма. Чтение и запись работают на уровне фрагментов: один reader обрабатывает один фрагмент, параллелизм = количество фрагментов. Фрагменты могут храниться на разных storage devices.
Data FilesColumnar data в формате Lance v2. Каждый data file содержит одну или несколько колонок. Формат: adaptive structural encodings (mini-block или all-null), footer с metadata. Не Parquet — собственный бинарный формат, оптимизированный для random access.
Deletion VectorБитовая маска удалённых строк. Вместо физического удаления — soft delete: бит = 1 означает 'строка удалена'. При чтении фильтруется на лету. При компакции — физическое удаление. Экономит перезапись данных при update/delete.
ANN IndexОпциональный векторный индекс на уровне фрагмента. Поддерживает IVF-PQ (Inverted File + Product Quantization) и HNSW (Hierarchical Navigable Small World). Хранится рядом с данными — не в отдельной системе.
TIP

Фрагментная модель — прямая аналогия с FileGroup в Hudi (Модуль 13). Но в Hudi FileGroup содержит base file + log files (MOR), а Lance fragment — это колоночные данные + deletion vector + опциональный ANN индекс. Фрагменты в Lance иммутабельны — update создаёт новый deletion vector, не новый data file.

Manifest и Version Log

Lance использует двухуровневую metadata-структуру: manifest описывает текущее состояние датасета, а version log хранит историю всех manifest’ов:

Manifest → Version Log: двухуровневая metadata

Version Log (append-only)

Version Log — append-only журнал всех версий датасета. Каждая запись содержит: номер версии, timestamp, ссылку на manifest, тип операции (append/delete/merge/create_index). Аналог transaction log в Delta Lake.
Version 1Первая версия: создание датасета. Manifest содержит список начальных фрагментов, schema, количество строк. Тип операции: CREATE.
Version 2Вторая версия: append новых данных. Manifest содержит 3 старых + 1 новый фрагмент. Старые фрагменты не копируются — manifest ссылается на них. Инкрементальность: только diff.
Version 3Третья версия: удаление строк. Manifest содержит 4 фрагмента, но fragment 1 получает deletion vector. Данные не перезаписываются — только новый manifest + deletion vector.

Manifest — это protobuf-сериализованный файл, содержащий:

Содержимое Manifest
SchemaArrow-совместимая схема датасета. Типы данных: все Arrow types (int, float, string, binary, list, struct, dictionary). Lance не определяет собственную систему типов — использует Arrow напрямую.
Fragment ListСписок всех фрагментов в этой версии. Для каждого фрагмента: ID, path к data files, количество строк, path к deletion vector (если есть), path к ANN index (если есть). Фрагменты из предыдущих версий переиспользуются — не копируются.
Version MetadataМетаданные версии: номер, timestamp, тип операции, пользовательские теги (experiment_id, model_version). Позволяет навигацию по истории: 'покажи датасет на момент training run #42'.

Физическая структура на диске

Lance dataset — это директория с фиксированной структурой:

Структура директории Lance dataset

dataset.lance/

Корневая директория dataset'а. На локальном диске или object storage (S3, GCS, Azure Blob). Имя директории = имя dataset'а. Всё содержимое — самодостаточно, нет внешних зависимостей.
data/Директория с data files фрагментов. Каждый фрагмент — поддиректория с .lance файлами (колоночные данные в формате Lance v2). Файлы иммутабельны — никогда не перезаписываются после создания.
_versions/Директория с manifest'ами. Каждый файл — protobuf-сериализованный manifest конкретной версии. Имя файла: {version_number}.manifest. Append-only — старые manifest'ы не удаляются (пока не запущен cleanup).
_indices/Директория с векторными индексами. Индексы привязаны к конкретным фрагментам и версиям. При update данных — индекс перестраивается для затронутых фрагментов.

Deletion Vectors и компакция

Lance использует deletion vectors для soft-delete — аналог deletion vectors в Delta Lake 3.x и Apache Iceberg v2:

Deletion Vector: soft delete

DELETE WHERE id = 42

Операция DELETE WHERE user_id = 'abc'. Вместо перезаписи фрагмента — создаётся deletion vector. Стоимость delete = O(1) write (маленький файл), не O(N) rewrite (весь фрагмент).
Fragment 0Оригинальные данные не изменяются. Data files остаются на диске как были. Это ключевое свойство: иммутабельность data files означает, что конкурентные reader'ы не пострадают от delete.
Deletion VectorНовый файл: битовая маска размером N бит (где N = количество строк в фрагменте). Бит 42 = 1 (удалён), остальные = 0 (активны). Размер: ~125KB для 1M строк. Создаётся атомарно — новая версия manifest ссылается на него.

UPDATE id=42 SET name=…

UPDATE WHERE id = 42 SET name = 'new'. В Lance update = delete старой строки + append новой. Deletion vector маркирует строку 42 в старом фрагменте, новое значение добавляется в новый фрагмент.
Старый фрагментСтрока 42 помечена как удалённая через deletion vector. Данные физически остаются — экономия I/O при update. Для читателя строка 42 невидима.
Новый фрагментНовое значение строки 42 записано в отдельный фрагмент. Manifest новой версии ссылается на оба фрагмента: старый (с deletion vector) и новый (с обновлённой строкой).
WARNING

Deletion vectors накапливаются: после 100 delete операций у фрагмента будет 100 строк в deletion vector. Чтение замедляется (фильтрация на каждой строке). Компакция решает проблему: перезаписывает фрагмент без удалённых строк, обнуляя deletion vector.

Компакция

Компакция в Lance — это фоновый процесс, который физически удаляет строки из deletion vectors и оптимизирует layout фрагментов:

Компакция: до и после

До компакции

До компакции: три фрагмента, два из которых имеют deletion vectors. Суммарно 200K 'мёртвых' строк — занимают место на диске и замедляют чтение (проверка deletion vector на каждом read).
Fragment 0100K удалённых строк из 1M. 10% overhead: каждое чтение проверяет deletion vector. Данные удалённых строк занимают ~6MB на диске (бесполезно).
Fragment 1Ещё один фрагмент с deletion vector. Суммарный overhead: 200K мёртвых строк в двух фрагментах. Компакция объединит живые строки из обоих фрагментов.
Fragment 2Фрагмент без удалений — пропускается при компакции. Компакция обрабатывает только фрагменты с deletion vectors.

После компакции

После компакции: живые строки из fragments 0 и 1 записаны в новый фрагмент. Fragment 2 без изменений. Deletion vectors удалены. Новая версия manifest ссылается на новые фрагменты.
Fragment 3 (новый)Объединённые живые строки: 900K + 900K = 1.8M строк. Разбит на два фрагмента по ~1M. Deletion vectors нет — все строки активны. Чтение быстрее: нет overhead фильтрации.
Fragment 2 (без изменений)Фрагмент 2 не затронут компакцией — у него не было deletion vector. Переиспользуется в новой версии manifest без копирования.

Transaction Log и Time Travel

Append-only version log обеспечивает MVCC (Multi-Version Concurrency Control): каждая операция (append, delete, merge, create_index) создаёт новую версию, не изменяя предыдущие:

Time Travel: навигация по версиям
Version 1Начальная версия: 3 фрагмента, 3M строк. Timestamp: 2025-01-15 10:00. Тег: 'training_baseline'. Reader может открыть эту версию в любое время — данные иммутабельны.
append
Version 2Добавлены новые данные: 4-й фрагмент. Предыдущие 3 фрагмента не копируются — manifest v2 ссылается на них. Copy-on-write только для metadata (manifest), не для data.
delete
Version 3Удалено 50K строк: deletion vector на fragment 1. Данные физически на месте — soft delete. Тег: 'cleaned_v1'. Для ML: 'я удалил мусорные данные, но если нужно — вернусь к v2'.

Python API для time travel:

import lance

# Открыть текущую версию
ds = lance.dataset("s3://bucket/embeddings.lance")
print(f"Current: version {ds.version}, {ds.count_rows()} rows")

# Открыть конкретную версию
ds_v1 = lance.dataset("s3://bucket/embeddings.lance", version=1)
print(f"V1: {ds_v1.count_rows()} rows")

# Список всех версий
for v in ds.list_versions():
 print(f" v{v['version']}: {v['timestamp']}{v['metadata']}")
NOTE

Time travel в Lance — лёгкий: manifest’ы занимают килобайты, data files переиспользуются. В отличие от Delta Lake, где time travel требует хранения всех исторических Parquet-файлов, Lance хранит только manifest’ы + deletion vectors. Очистка старых версий: ds.cleanup_old_versions(older_than=timedelta(days=30)).

Сравнение с Lakehouse-форматами

Lance решает задачи, которые lakehouse-форматы решают иначе или не решают вовсе:

Lance vs Delta Lake / Iceberg / Hudi
СвойствоКлючевые архитектурные различия между форматами. Каждый формат оптимизирован для своего основного workload'а.
LanceLance: Rust-based, Arrow-native формат для ML/AI workload. Фрагментная модель, deletion vectors, встроенный vector search. Производство: LanceDB Cloud.
Delta LakeDelta Lake: append-only transaction log + immutable Parquet files. Spark-native. Производство: Databricks.
IcebergIceberg: metadata tree (snapshot → manifest list → manifests → data files). Vendor-neutral. Широкая экосистема.
HudiHudi: timeline + FileGroup/FileSlice. MOR (merge-on-read) для стриминга. Производство: Uber, AWS.
UnitБазовая единица хранения данных — минимальный атомарный блок, с которым работает формат.
Fragment: ~64MB колоночных данных + deletion vector + опциональный ANN index. Иммутабельный после создания.
Parquet file: стандартный Parquet. Типичный размер: 128MB-1GB. Иммутабельный.
Data file: Parquet или ORC. Организован через manifest'ы. Типичный размер: 128MB-512MB.
FileSlice: base file (Parquet) + log files (Avro). MOR: log files merge-ятся при чтении. COW: только base files.
RandomСкорость произвольного доступа к отдельной строке по row ID или индексу.
100x быстрее Parquet: sliceable encodings позволяют прямой переход к offset строки без декодирования page. O(1) по offset внутри фрагмента.
Медленный: нужно найти row group → загрузить page → декодировать весь page. Page-level granularity = O(page_size) на строку.
Медленный: аналогично Delta Lake. Parquet как физический формат = те же ограничения random access.
Медленный для COW. MOR чуть лучше: log file хранит последние изменения. Но base file — Parquet с теми же ограничениями.
VectorВстроенная поддержка поиска по вектору (nearest neighbor) без внешних зависимостей.
Встроенный: IVF-PQ и HNSW индексы на уровне фрагментов. Sub-millisecond latency. Не нужен отдельный vector DB.
Нет: нужен внешний vector database (Pinecone, Milvus, Weaviate) или custom integration.
Нет: нет нативного векторного индекса. Требуется внешний инструмент.
Нет: Hudi не поддерживает векторный поиск.
VersionМеханизм версионирования данных — возможность вернуться к любой предыдущей версии.
Нативный: version log + manifests. Лёгкий: только metadata меняется, data files переиспользуются. User tags на версиях.
Transaction log: JSON commit files. Time travel через version или timestamp. Vacuum удаляет старые файлы.
Snapshot chain: каждый commit = snapshot. Expire snapshots для cleanup. Metadata tree обеспечивает навигацию.
Timeline: instant objects (.commit, .deltacommit). Savepoint для фиксации. Cleaner для удаления старых версий.

Write Path и Read Path

Write Path: append + update

Append (новые данные)

Append: добавление новых данных. Самая частая операция в ML-pipeline (новые embedding'и, новые обучающие примеры). Стоимость: O(new_data) — пропорциональна только объёму новых данных.
1. EncodeДанные кодируются в формат Lance v2: adaptive structural encodings, колоночный layout. Zero-copy из Arrow RecordBatch если данные уже в Arrow формате.
2. Write FragmentСоздаётся новый фрагмент с data files. Если данных больше одного фрагмента — создаётся несколько. Запись атомарна: фрагмент либо записан полностью, либо нет.
3. CommitАтомарный commit: новый manifest записывается в _versions/. Manifest содержит все старые фрагменты + новый. Конкурентные writer'ы разрешаются через optimistic concurrency (retry при конфликте).

Update (изменение строк)

Update: изменение существующих строк. В Lance update = soft delete старой строки + append новой. Два шага: создать deletion vector + записать новые данные.
1. Find rowsПоиск строк для обновления: scan с предикатом. Результат: список (fragment_id, row_offset) для каждой строки. Использует statistics и индексы для быстрого поиска.
2. Deletion VectorДля каждого затронутого фрагмента: создать или обновить deletion vector. Помечает старые версии строк как удалённые. Data files не изменяются.
3. Append newНовые значения строк записываются как append в новый фрагмент. Manifest новой версии: старые фрагменты (с deletion vectors) + новый фрагмент. Атомарный commit.
Read Path: scan + random access

Full Scan (аналитика)

Scan: чтение всех строк с опциональным предикатом. Аналогично Parquet scan, но с Arrow-native zero-copy. Параллелизм = количество фрагментов.
1. ManifestЗагрузить текущий manifest → список фрагментов. Статистики по фрагментам (min/max, row count) для предикатного pruning.
2. Read fragmentsПараллельное чтение фрагментов. Каждый фрагмент → Arrow RecordBatch (zero-copy). Deletion vector фильтрует удалённые строки. Column projection: читаем только нужные колонки.

Random Access (ML batch)

Random access: чтение конкретных строк по row ID. Ключевое преимущество Lance: O(1) доступ к строке внутри фрагмента через sliceable encodings. Для ML mini-batch: 1000 случайных строк за ~1ms.
1. Row ID → FragmentRow ID декомпозируется: fragment_id + offset_within_fragment. Manifest содержит row count per fragment → O(log N) binary search для нахождения фрагмента.
2. Direct decodeSliceable encoding: прямой переход к offset строки в data file без декодирования предыдущих строк. Нет page-level granularity как в Parquet. Результат: конкретная строка как Arrow record.

Конкурентный доступ

Lance поддерживает optimistic concurrency control — несколько writer’ов могут работать с dataset’ом одновременно:

Optimistic Concurrency: конкурентные writer'ы
Writer AWriter A начинает операцию на version 5. Читает manifest v5, пишет новый фрагмент, готовит manifest v6. Если за время операции другой writer уже создал v6 — конфликт.
Writer BWriter B тоже начинает на version 5 (параллельно). Готовит свой manifest v6. Кто первый сделает atomic commit (rename файла) — тот получит v6. Второй — retry на v7.
atomic commit (file rename)
РезультатWriter A успел первым → v6. Writer B обнаруживает конфликт, перечитывает manifest v6, проверяет совместимость (нет перекрытия в затронутых фрагментах), создаёт manifest v7. Если конфликт неразрешим — ошибка пользователю.
TIP

Conflict resolution в Lance автоматический для большинства случаев: если writer’ы работают с разными фрагментами — конфликта нет. Конфликт возникает только при одновременном update одних и тех же строк. В ML-pipeline’ах это редкость: обычно один writer append’ит новые данные, другой обновляет metadata или создаёт индексы.

Итоги

Lance — это не “ещё один Parquet”. Это формат, спроектированный для другого набора задач:

Ключевые свойства Lance
Фрагменты~64MB иммутабельные единицы хранения. Параллельное чтение/запись. Каждый фрагмент — самодостаточный колоночный блок с deletion vector и опциональным ANN index.
Version LogAppend-only журнал версий. Каждая операция — новый manifest, не изменяющий данные. Time travel, MVCC, user tags для ML experiments.
Deletion VectorsSoft delete: битовая маска вместо перезаписи данных. Update = delete + append. Компакция физически удаляет мёртвые строки.
Arrow-NativeВсе данные возвращаются как Arrow RecordBatch без копирования. Экосистема: Pandas, DuckDB, Polars, PyTorch — всё, что понимает Arrow.

В следующем уроке мы разберём формат Lance v2 — “контейнерный формат” без встроенной системы типов и фиксированных кодировок, который позволяет адаптировать физический layout данных под конкретные workload’ы.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Lance dataset содержит 10 фрагментов по 1M строк. Выполняется DELETE WHERE category = 'spam' — затрагивает 50K строк в fragment 3 и 30K строк в fragment 7. Что физически происходит с данными?

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

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

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

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