Learning Platform
Глоссарий Troubleshooting
Урок 13.05 · 40 мин
Продвинутый
Apache IcebergCopy-on-WriteMerge-on-ReadPosition DeleteEquality DeleteDeletion VectorPuffinRoaring BitmapMERGE INTOSequence Number

Row-level операции: CoW, MoR, deletion vectors

В уроке 2 мы разобрали, что каждый коммит создаёт новый snapshot, а данные в файлах иммутабельны. Но как тогда работают DELETE, UPDATE и MERGE INTO, если файлы нельзя модифицировать?

Iceberg предлагает три стратегии: Copy-on-Write (V1+), Merge-on-Read с delete files (V2), и Deletion Vectors (V3). Каждая — с разным балансом между стоимостью записи и скоростью чтения.

NOTE

Примеры кода — на Python с pyiceberg 0.11.1 (март 2026). Версия спецификации — V2 (с пометками о V3 deletion vectors). V3 ратифицирован в июне 2025, deletion vectors доступны в Spark, Flink, AWS EMR 7.12.

Зачем нужны row-level deletes?

Data lakehouse — не только append. Реальные сценарии требуют удаления и обновления строк:

СценарийОперацияПример
GDPR / Right to be forgottenDELETEУдалить все данные конкретного пользователя
CDC (Change Data Capture)MERGE INTOПрименить changelog из OLTP-базы
Коррекция данныхUPDATEИсправить ошибочные значения
ДедупликацияDELETE + INSERTУдалить дубликаты, вставить уникальные
SCD Type 2UPDATE + INSERTЗакрыть текущую версию, вставить новую

Во всех случаях проблема одна: Parquet-файлы иммутабельны. Нельзя “зайти” в файл и изменить одну строку.

Copy-on-Write (CoW)

Простейшая стратегия, доступная с V1:

Идея: чтобы удалить/обновить строку в файле, перезаписать весь файл без этой строки (или с обновлённым значением).

Copy-on-Write: DELETE одной строки

data-001.parquet (1M строк)

Исходный Parquet-файл с 1 000 000 строк. DELETE WHERE customer_id = 42 затрагивает одну строку. Но файл иммутабельный — нельзя удалить строку in-place.
DELETE WHERE customer_id=42

Прочитать → Отфильтровать → Записать

CoW: движок читает ВСЕ 1 000 000 строк, фильтрует customer_id=42, записывает 999 999 строк в НОВЫЙ файл. Старый файл отмечается как deleted в manifest. Огромный I/O для одной строки.
Snapshot NПредыдущий snapshot ссылается на data-001.parquet (1M строк). Этот snapshot остаётся для time travel — файл не удаляется физически.
Snapshot N+1Новый snapshot ссылается на data-002.parquet (999 999 строк). data-001.parquet отсутствует в новом snapshot — его строки 'заменены'. При snapshot expiry файл будет удалён физически.

Характеристики CoW:

АспектОценка
Write amplification[!] Высокая — перезапись целых файлов (256-512 MB) ради одной строки
Read performance[OK] Отличная — нет overhead при чтении, все данные актуальны
Latency записи[!] Высокая — пропорциональна размеру файла, не числу изменённых строк
Конкурентность[OK] Простая — нет delete files для reconciliation
TIP

CoW оптимален для bulk операций (DELETE целого partition, полная перезапись таблицы) и для таблиц с редкими обновлениями. Для CDC/streaming — слишком дорого.

Merge-on-Read (MoR) — V2

V2 спецификация добавила delete files — специальные файлы, которые описывают удалённые строки без перезаписи data files.

Идея: вместо перезаписи — записать маленький файл-маркер “эти строки удалены”. При чтении — merge data file с delete file и отфильтровать удалённые строки.

Position delete files

Position delete file — Parquet-файл с двумя колонками:

file_path (string) | pos (long)
---------------------------------
"s3://.../data-001.parquet" | 42
"s3://.../data-001.parquet" | 1337
"s3://.../data-001.parquet" | 99999

Каждая запись — координата (файл, позиция строки в файле). Позиция — 0-based индекс строки в Parquet row group.

Position delete: точечное удаление по координатам

data-001.parquet (1M строк)

Data file содержит 1M строк. Три строки нужно удалить. Вместо перезаписи всего файла — создаём маленький position delete file с координатами удалённых строк.
Position deletesPosition delete file: три записи (file_path, pos). Размер: ~200 байт вместо перезаписи 256MB data file. Write amplification снижается на порядки.

Read: merge data + deletes

При чтении: движок загружает data file + position delete file. Сканируя строки, пропускает позиции 42, 1337, 99999. Overhead: загрузка и merge delete file при каждом чтении.
РезультатЧитатель видит 999 997 строк. Три удалённые строки отфильтрованы. Data file не изменён — остаётся иммутабельным.

Equality delete files

Equality delete file — Parquet-файл с значениями предикатов:

customer_id (long)
------------------
42
73

Все строки во всех data files, где customer_id = 42 или customer_id = 73, считаются удалёнными.

Equality delete: удаление по значению
Equality delete fileСодержит значения предикатов: customer_id=42, customer_id=73. Все строки с этими значениями в ЛЮБОМ data file считаются удалёнными. Мощный механизм для GDPR: один маленький файл удаляет данные по всей таблице.

Read: проверить каждую строку

При чтении: движок загружает equality delete file, строит фильтр, применяет к каждой строке каждого data file. Overhead значительно выше, чем у position deletes — проверка каждой строки.
OverheadEquality deletes дороже при чтении: нужно загрузить delete file, распаковать значения, для каждой строки проверить принадлежность. Для больших таблиц это может замедлить чтение в разы.

Sequence numbers — корректность при конкурентных записях

Как отличить строку, добавленную ДО delete от строки, добавленной ПОСЛЕ? Без этого delete мог бы удалить строки из будущих INSERT-ов.

V2 использует sequence numbers:

Snapshot 5 (seq=5): INSERT → data-001.parquet
Snapshot 6 (seq=6): DELETE customer_id=42 → equality-delete-001.parquet (seq=6)
Snapshot 7 (seq=7): INSERT → data-002.parquet (содержит customer_id=42!)

Equality delete с seq=6 применяется только к файлам с seq ≤ 6. Файл data-002.parquet с seq=7 — не затронут, даже если содержит customer_id=42.

Sequence numbers: защита от ложного удаления
data-001.parquetЗаписан в snapshot с sequence-number=5. Содержит строку customer_id=42. Equality delete с seq=6 ПРИМЕНЯЕТСЯ к этому файлу (5 ≤ 6). Строка удалена.
eq-delete-001.parquetEquality delete file с sequence-number=6. Предикат: customer_id=42. Применяется только к data files с seq ≤ 6. Файлы с seq > 6 — не затронуты.
data-002.parquetЗаписан в snapshot с sequence-number=7. Содержит строку customer_id=42. Equality delete с seq=6 НЕ ПРИМЕНЯЕТСЯ (7 > 6). Строка жива — это новый INSERT после DELETE.
WARNING

Без sequence numbers equality deletes были бы опасны: DELETE WHERE customer_id=42 убил бы ВСЕ будущие INSERT-ы с customer_id=42. Sequence numbers — обязательный компонент V2.

Deletion Vectors (V3)

V3 спецификация (ратифицирована в июне 2025) заменяет position delete files deletion vectors — компактными бинарными битмапами.

Проблема position deletes в V2

Position delete files — Parquet-файлы с координатами. При активных UPDATE/DELETE:

  1. Proliferation: каждая операция создаёт новый position delete file. После 1000 UPDATE-ов — 1000 delete files для одного data file
  2. Read overhead: при чтении нужно загрузить и merge все 1000 delete files
  3. Нет compaction гарантий: спецификация V2 не требовала от writers объединять delete files. Многие не делали — таблицы деградировали

V3 решение: Roaring bitmaps в Puffin files

Deletion Vectors V3: bitmap вместо Parquet

V2: Position deletes (Parquet)

V2: каждая DELETE-операция создаёт отдельный position delete file (Parquet). 100 операций = 100 файлов, которые нужно merge при чтении. I/O и CPU растут линейно с числом удалений.
Проблема100 delete files → 100 чтений → merge 100 наборов позиций → O(N × M) при N=файлов, M=delete files. Для CDC-таблиц с миллионами обновлений — катастрофа.

V3: Deletion Vector (Roaring bitmap)

V3: один deletion vector (Roaring bitmap) на один data file. Bitmap сжимает позиции удалённых строк в компактную бинарную структуру. Хранится в Puffin-файле — бинарном контейнере для metadata blobs.
РешениеОдин DV на data file. Размер: несколько KB для миллионов позиций. Чтение: одна операция, O(1) проверка per-row. Writer обязан объединять новые deletes с существующим DV.

Формат deletion vector

  1. Roaring bitmap: компактная структура для разреженных множеств целых чисел. Каждый бит = одна строка: 0 = жива, 1 = удалена
  2. Puffin file: бинарный контейнер (аналог Parquet footer для blob-ов). Один Puffin-файл может содержать deletion vectors для нескольких data files
  3. Правило одного DV: в snapshot максимум один deletion vector на data file. Writer обязан merge новых deletes с существующим DV
data-001.parquet → file-001.puffin [DV: bitmap, positions 42, 1337, 99999]
data-002.parquet → file-001.puffin [DV: bitmap, positions 7, 15, 28]
data-003.parquet → (нет DV — все строки живы)

Read path с deletion vectors

1. Открыть data file → начать сканировать строки
2. Загрузить DV для этого data file (один blob из Puffin)
3. Для каждой строки: проверить бит в bitmap
 - бит = 0 → строка жива, вернуть
 - бит = 1 → строка удалена, пропустить

Roaring bitmap — O(1) проверка per-bit (lookup по контейнеру + bit test). Минимальный CPU overhead.

Совместимость с V2

  • V3 deprecates position delete files — writers не должны создавать новые position delete files в V3 таблицах
  • Существующие position deletes валидны — при upgrade V2→V3 старые position delete files читаются
  • Writer обязан при обновлении deletes для data file: прочитать существующие position deletes + DV → записать объединённый DV
TIP

При upgrade V2→V3 не нужно перезаписывать delete files. Они продолжают работать. Новые deletes пишутся как DVs. Со временем, при compaction, все position deletes мигрируют в DVs.

UPDATE и MERGE INTO

UPDATE и MERGE INTO — комбинация DELETE + INSERT:

UPDATE

UPDATE orders SET status = 'cancelled' WHERE order_id = 42;
UPDATE = DELETE + INSERT

UPDATE orders SET status=‘cancelled’ WHERE order_id=42

UPDATE в Iceberg — не in-place модификация. Это DELETE строки с order_id=42 + INSERT новой строки с обновлённым status='cancelled'. Два действия в одном атомарном коммите.

DELETE: old row

Шаг 1: удалить старую строку. В CoW — перезаписать data file без этой строки. В MoR — записать position delete / deletion vector для позиции этой строки.
CoWCopy-on-Write: перезаписать весь data file, исключив строку с order_id=42. Новый файл содержит все остальные строки + обновлённую строку.

INSERT: new row

Шаг 2 (MoR): записать новую строку с обновлённым status в отдельный data file. Delete marker + new data file = обновление без перезаписи оригинала.
MoRMerge-on-Read: записать position delete/DV для старой строки + новый data file с обновлённой строкой. Два маленьких файла вместо перезаписи большого.

MERGE INTO

MERGE INTO target USING source
ON target.id = source.id
WHEN MATCHED AND source.op = 'delete' THEN DELETE
WHEN MATCHED AND source.op = 'update' THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;

MERGE INTO — основная операция для CDC. Один statement обрабатывает delete, update и insert:

  1. Join source и target по ключу
  2. Matched + delete → position delete / DV для строки
  3. Matched + update → position delete / DV + INSERT новой строки
  4. Not matched → INSERT
  5. Всё в одном атомарном snapshot

CoW vs MoR vs Deletion Vectors

Сравнение стратегий row-level операций
Copy-on-WriteПерезапись файлов. Write: дорого (целый файл). Read: дёшево (нет merge). Лучше для bulk-операций, редких обновлений. Доступно с V1.
MoR (Position deletes)Delete files (Parquet). Write: дёшево (маленький файл). Read: дороже (merge N delete files). Проблема: proliferation delete files. Доступно с V2.
Deletion Vectors (V3)Roaring bitmaps в Puffin. Write: дёшево (один bitmap). Read: дёшево (O(1) check). Обязательный merge при записи. Лучше: CDC, streaming, частые обновления. V3 (июнь 2025).

Количественное сравнение

МетрикаCoWMoR (V2)DV (V3)
DELETE 1 строки из 1M-строчного файлаПерезапись 256 MB+1 Parquet file (~200 B)+1 DV blob (~50 B)
1000 DELETE-ов по 1 строке1000 перезаписей файлов+1000 delete filesОдин merged DV
Read overhead после 1000 delete-овMerge 1000 files1 bitmap check
Storage overhead≈ 2x на каждый UPDATE+1 файл на операцию+1 blob на data file

Конфигурация стратегии

В Spark:

-- Установить MoR для DELETE/UPDATE
ALTER TABLE orders SET TBLPROPERTIES (
 'write.delete.mode' = 'merge-on-read',
 'write.update.mode' = 'merge-on-read',
 'write.merge.mode' = 'merge-on-read'
);

-- Или CoW
ALTER TABLE orders SET TBLPROPERTIES (
 'write.delete.mode' = 'copy-on-write',
 'write.update.mode' = 'copy-on-write',
 'write.merge.mode' = 'copy-on-write'
);
NOTE

Для V3 таблиц MoR автоматически использует deletion vectors вместо position delete files. Переключение — только format-version=3 в свойствах таблицы.

Row lineage (V3)

V3 добавляет row lineage — метаданные для отслеживания изменений на уровне строк:

  • _row_id — уникальный идентификатор строки (присваивается при записи)
  • _last_updated_sequence_number — sequence number snapshot-а, в котором строка была последний раз изменена

Это позволяет:

  • Incremental CDC: SELECT * FROM orders WHERE _last_updated_sequence_number > 5 — все изменённые строки с snapshot 5
  • Audit trail: точная история изменений каждой строки
  • Efficient replication: только изменённые строки, без полного scan
-- Создать V3 таблицу с row lineage
CREATE TABLE orders (
 order_id BIGINT,
 amount DECIMAL(10,2),
 status STRING
) USING iceberg
TBLPROPERTIES ('format-version' = '3');

-- Все строки с изменениями после snapshot 5
SELECT * FROM orders WHERE _last_updated_sequence_number > 5;
TIP

Row lineage — engine-agnostic: метаданные записываются в Parquet-файлы согласно спецификации. Не нужен специфичный движок для чтения — любой движок с поддержкой V3 увидит _row_id и _last_updated_sequence_number.

Сравнение с Delta Lake

АспектDelta LakeIceberg V2Iceberg V3
CoW (по умолчанию)
MoR Deletion vectors Position/equality deletes Deletion vectors
DV форматRoaring bitmap (проприетарный)Roaring bitmap (Puffin)
Equality deletes
Row lineageRow tracking (proprietary) (открытая спецификация)
Spec requirement: merge DVs (writer обязан)
InteropSpark + DatabricksSpark, Flink, Trino, DuckDB…Spark, Flink, EMR 7.12…
V2→V3: эволюция delete механизмов
V1: CoW onlyIceberg V1: только Copy-on-Write. Нет row-level deletes. Каждый UPDATE/DELETE перезаписывает целые data files. Простая модель, но высокая write amplification.
V2: Position + EqualityV2 добавил Merge-on-Read: position delete files (координаты) и equality delete files (предикаты). Sequence numbers для корректности. Проблема: proliferation delete files без compaction.
V3: Deletion VectorsV3 заменяет position deletes на deletion vectors (Roaring bitmaps в Puffin). Writer обязан merge. Row lineage для change tracking. Equality deletes по-прежнему поддерживаются. Position deletes deprecated.

Итоги

  1. Copy-on-Write — перезапись файлов; дорогая запись, дешёвое чтение. Для bulk/редких обновлений
  2. Position deletes (V2) — координаты (file_path, pos) в Parquet; дешёвая запись, растущий read overhead
  3. Equality deletes (V2) — предикаты (customer_id=42); мощные для GDPR, но дорогие при чтении (per-row check)
  4. Sequence numbers — защита от ложного удаления будущих INSERT-ов; обязательны в V2+
  5. Deletion vectors (V3) — Roaring bitmaps в Puffin files; O(1) per-row check, writer обязан merge, один DV per data file
  6. Row lineage (V3)_row_id + _last_updated_sequence_number для incremental CDC и audit
  7. UPDATE = DELETE + INSERT, MERGE INTO — комбинация в одном атомарном snapshot
Debezium → Iceberg sink — CDC в lakehouse Kafka Connect Iceberg sink GDPR retention в lakehouse — governance

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Iceberg V2: DELETE FROM orders WHERE status = 'cancelled'. Таблица содержит 100 data-файлов, 5 из них содержат cancelled строки. Режим: Copy-on-Write (CoW). Что произойдёт на storage?

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

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

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

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