惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Troy Hunt's Blog
Scott Helme
Scott Helme
T
Threat Research - Cisco Blogs
T
Tenable Blog
L
LINUX DO - 热门话题
V
Visual Studio Blog
I
Intezer
Blog — PlanetScale
Blog — PlanetScale
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
C
Cyber Attacks, Cyber Crime and Cyber Security
F
Fortinet All Blogs
aimingoo的专栏
aimingoo的专栏
Know Your Adversary
Know Your Adversary
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
N
Netflix TechBlog - Medium
SecWiki News
SecWiki News
I
InfoQ
Microsoft Security Blog
Microsoft Security Blog
Project Zero
Project Zero
W
WeLiveSecurity
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
Recorded Future
Recorded Future
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Vercel News
Vercel News
S
Securelist
Spread Privacy
Spread Privacy
L
LangChain Blog
云风的 BLOG
云风的 BLOG
G
Google Developers Blog
MongoDB | Blog
MongoDB | Blog
Google DeepMind News
Google DeepMind News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CERT Recently Published Vulnerability Notes
罗磊的独立博客
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
The Last Watchdog
The Last Watchdog
Attack and Defense Labs
Attack and Defense Labs
博客园 - 司徒正美
Help Net Security
Help Net Security
L
Lohrmann on Cybersecurity
人人都是产品经理
人人都是产品经理
Forbes - Security
Forbes - Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
PCI Perspectives
PCI Perspectives
博客园 - 【当耐特】
T
Tor Project blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Iceberg без Spark для каждой мелочи: UPDATE, DELETE и MERGE INTO из одного SQL-движка в Apache Doris 4.1
shatzibitten · 2026-05-02 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение14 мин

Охват и читатели0

Обзор

Перевод

Атрибуция: Этот материал является переводом и адаптацией статьи Apache Doris 4.1 on Iceberg V3: Running the Full Lakehouse Lifecycle from One SQL Engine за авторством Mingyu Chen (Rayner), опубликованной по лицензии CC BY 4.0. Адаптация выполнена для русскоязычной аудитории Хабра с дополнительным контекстом по архитектурным компромиссам.


Сегодня поговорим про Iceberg V3 и Apache Doris 4.1. Но не в формате «у нас теперь есть поддержка новой версии формата, хлопаем в ладоши». Гораздо интереснее другой вопрос: что меняется, когда SQL-движок умеет не только читать Iceberg-таблицу, но и делать маленькие исправления, reconciliation, maintenance и диагностику без отдельного Spark-кластера на каждое телодвижение?

Спойлер: Spark никто не отменял. Flink тоже. Но есть целый класс ежедневной работы, где переключение движка дороже самой операции.

Если вы когда-нибудь писали Spark job ради исправления одной строки, эта статья может вызвать лёгкие флешбеки.

Хотите сразу попробовать? Переходите в Часть 8: Quick Start там путь от нуля до V3-таблицы за пять минут.


Часть 1: 3 часа ночи, одна строка сломана

Представим конкретную ситуацию.

Data engineer в e-commerce компании сидит в Doris и разбирает Iceberg-таблицу с заказами. Видит, что вчерашний batch из ERP проставил status = 'cancelled' для 200 заказов, которые на самом деле были оплачены — upstream-система отправила кривой snapshot. Запрос подтверждает: вот они, 200 строк с неверным статусом. Исправление - банальный WHERE order_id IN (...) и SET status = 'paid'.

В идеальном мире это выглядит так:

UPDATE orders_iceberg
SET status = 'paid'
WHERE order_id IN (SELECT order_id FROM bad_batch_ids);

Что происходит дальше в реальном мире — красиво описано в оригинальной статье Mingyu Chen. Инженер закрывает SQL-клиент и начинает другой рабочий процесс:

  1. Пишет Spark job.

  2. Подстраивает scheduler.

  3. Открывает PR.

  4. Ждёт review.

  5. Чаще всего — передаёт задачу платформенной команде, потому что у него нет прямого доступа к write-кластеру.

  6. Утром возвращается в Doris и проверяет, что всё отработало.

Полный цикл — 14 часов. Код по смыслу — один UPDATE.

Есть и менее драматичный, но более частый сценарий. CDC-поток пишет в Iceberg, small files и delete files копятся, запросы постепенно деградируют. Все понимают, что нужен rewrite_data_files.

В командах с self-service инфраструктурой инженер запустит compaction сам. Но во многих средних и крупных организациях write-доступ к Iceberg-таблицам живёт у выделенной platform team — и тогда задача, которая по сути один SQL-вызов, превращается в ticket, maintenance window и ожидание. Именно этот организационный паттерн описывает автор оригинальной статьи, и он встречается чаще, чем хотелось бы.

Iceberg решил огромную часть проблемы: он сделал таблицу открытым transactional-форматом поверх файлов в object storage. Но работа с этой таблицей часто остаётся не такой уж открытой:

  • читаем в одном движке;

  • пишем в другом;

  • compaction и maintenance делаем через третий;

  • CDC и streaming ingestion живут отдельно.

Получается два налога на каждое маленькое действие: switch-engine tax и cross-team tax. Apache Doris 4.1 пытается уменьшить оба.


Часть 2: Где Doris находится в Iceberg-экосистеме

Важно сразу снять лишнее ожидание: Doris не пытается заменить Spark во всех задачах.

Apache Doris — это MPP SQL engine, который начинал как data warehouse, а за последние два мажорных релиза сместил центр тяжести в сторону lakehouse workloads. Spark при этом остаётся нормальным инструментом для cross-source backfills, тяжёлого ETL, больших batch-job’ов и широкой batch/streaming-экосистемы. Flink остаётся естественным выбором для streaming ingestion и continuous CDC write paths.

Роль Doris в этом контексте узкая и конкретная: real-time query layer для Iceberg, который постепенно забирает соседний workflow:

  • вы уже пришли в SQL-клиент с запросом;

  • увидели проблему или расхождение;

  • хотите сделать небольшое исправление;

  • хотите запустить incremental reconciliation;

  • хотите выполнить maintenance или диагностику;

  • не хотите ради этого прыгать в другой кластер.

В Doris 4.1 матрица Iceberg-поддержки выглядит довольно широко:

Зона

Что поддерживается

Read

V1/V2/V3, time travel, branch/tag, views, system tables, Position/Equality/DV delete

Write

INSERT, OVERWRITE, CTAS, INSERT INTO BRANCH, DELETE, UPDATE, MERGE INTO

DDL

create/drop table, schema change, partition evolution, branch/tag management

Maintain

rewrite_data_files, expire_snapshots, rewrite_manifests, rollback, fast_forward

Diagnose

data file distribution, dangling delete

Apache Doris 4.1 & Iceberg V3 Lakehouse Lifecycle

Apache Doris 4.1 & Iceberg V3 Lakehouse Lifecycle

Архитектура: один SQL-движок для Read, Write, DDL, Maintenance и Diagnostics на Iceberg V3 таблицах. Источник: оригинальная статья Mingyu Chen.

Но в этой статье нас интересует не вся матрица. Фокус на двух вещах:

  1. DML completeness: UPDATE, DELETE, MERGE INTO прямо из SQL-клиента.

  2. Iceberg V3 mechanics: Deletion Vectors и Row Lineage, без которых DML быстро начинает копить технический долг.


Часть 3: DML возвращается в query layer

Начнём с простого.

Исправить одну строку:

UPDATE iceberg_tbl
SET name = 'Alice-fixed'
WHERE id = 1;

Откатить плохой batch:

DELETE FROM iceberg_tbl
WHERE dt = '2026-04-01'
  AND source = 'bad_pipeline';

Сделать upsert по incremental batch:

MERGE INTO iceberg_tbl t
USING (
    SELECT 1 AS id, 'Alice_new' AS name, 26 AS age, 'U' AS flag
    UNION ALL SELECT 2, 'Bob',  30, 'D'
    UNION ALL SELECT 4, 'Dora', 28, 'I'
) s
ON t.id = s.id
WHEN MATCHED AND s.flag = 'D' THEN DELETE
WHEN MATCHED THEN UPDATE SET name = s.name, age = s.age
WHEN NOT MATCHED THEN INSERT (id, name, age) VALUES (s.id, s.name, s.age);

MERGE INTO здесь самая важная операция. Именно на ней держатся типичные CDC/upsert-сценарии:

  • изменения из PostgreSQL или MySQL прилетают через Flink CDC или похожий инструмент;

  • downstream Iceberg-таблица должна принять inserts, updates и deletes вместе;

  • incremental materialized views должны обновлять wide tables по change key.

Без MERGE INTO вы либо пересобираете данные целиком, либо уносите логику в Spark/Flink и вручную обслуживаете отдельный pipeline.

В Doris 4.1 MERGE INTO поддерживает partitioned targets, subqueries как source и expressions в UPDATE-части, например age = age * 2 + 1.

Но caveat сразу: target table должен быть format-version >= 2, а сам MERGE INTO в оригинальной статье помечен как experimental. Это не «сразу катим в прод в пятницу вечером», а «делаем POC на своём workload».


Часть 4: Почему DML без V3 быстро копит долги

Может показаться, что раз Doris теперь умеет UPDATE, DELETE, MERGE INTO на Iceberg — задача решена.

Не совсем. Наличие DML — это ещё не хороший DML.

В Iceberg V2 каждая такая операция создаёт Position Delete file. Это отдельный parquet-файл, в котором записано: «вот эти позиции из data file нужно считать удалёнными». Чтобы вернуть корректный результат, reader вынужден выполнять anti-join: прочитать data files, прочитать все связанные delete files, отфильтровать помеченные строки.

Один delete file — ничего страшного. Десять — терпимо. Сотни и тысячи маленьких DML commits в CDC-таблице — уже неприятно.

Проблема растёт линейно:

  • каждый DML commit добавляет новые delete files;

  • каждый последующий query вынужден открывать и объединять все релевантные delete files;

  • периодически нужен rewrite_data_files, чтобы вернуть таблицу в нормальное состояние;

  • а значит, снова maintenance job, scheduler и платформа — тот самый Jira-ticket из начала статьи.

Это первый долг: performance debt. DML вроде есть, но каждый commit медленно ухудшает чтение.

Есть второй долг: observability debt. После того как commit произошёл, downstream-системы больше не знают, когда конкретная строка была последний раз изменена. Они полагаются на snapshot diff. Но compaction и rewrite_data_files тоже создают новые snapshots, хотя данные логически не менялись.

В результате downstream pipeline видит физический rewrite и интерпретирует его как новые данные — и начинает переобрабатывать строки, которых никто не трогал.

Если коротко:

DML без V3 быстро превращается в два долга: performance debt и observability debt.

Iceberg V3 отвечает на них двумя механизмами:

  • Deletion Vectors — чтобы DML не раздувал delete files.

  • Row Lineage — чтобы row-level changes были наблюдаемыми.


Часть 5: Deletion Vectors — меньше delete files, меньше anti-join

Deletion Vector — это bitmap, который хранит, какие строки в data file считаются удалёнными. В Iceberg V3 он хранится в Puffin file format.

Контраст с V2 такой:

  • V2: каждый DML commit создаёт отдельный Position Delete parquet.

  • V3: все пометки на удаление для одного data file схлопываются в один Puffin Deletion Vector.

  • V2 reader: читает data file, потом anti-join со всеми связанными delete files.

  • V3 reader: читает data file + DV и применяет bitmap за один проход. Anti-join исчезает.

Iceberg V2 Position Deletes vs V3 Deletion Vector

Iceberg V2 Position Deletes vs V3 Deletion Vector

V2 копит Position Delete parquet-файлы при каждом DML commit. V3 схлопывает все пометки в один Puffin Deletion Vector. Источник: оригинальная статья Mingyu Chen.

Мини-пример V2:

CREATE TABLE orders_v2 (
    id INT,
    status STRING,
    amount DECIMAL(10,2)
) PROPERTIES ('format-version' = '2');

INSERT INTO orders_v2 VALUES
    (1, 'pending', 100),
    (2, 'pending', 200),
    (3, 'pending', 300);

UPDATE orders_v2 SET status = 'shipped' WHERE id = 1;
UPDATE orders_v2 SET status = 'shipped' WHERE id = 2;
DELETE FROM orders_v2 WHERE id = 3;

После трёх DML commits в V2 появляются три Position Delete files.

Та же логика на V3:

CREATE TABLE orders_v3 (
    id INT,
    status STRING,
    amount DECIMAL(10,2)
) PROPERTIES ('format-version' = '3');

INSERT INTO orders_v3 VALUES
    (1, 'pending', 100),
    (2, 'pending', 200),
    (3, 'pending', 300);

UPDATE orders_v3 SET status = 'shipped' WHERE id = 1;
UPDATE orders_v3 SET status = 'shipped' WHERE id = 2;
DELETE FROM orders_v3 WHERE id = 3;

Теперь вместо набора parquet delete files рядом с data file лежит Puffin Deletion Vector. SQL на поверхности тот же, физическая механика другая.

Проверить это можно через system table:

SELECT content, file_path, record_count
FROM orders_v3$files;

В нормальном V3-сценарии вы ожидаете увидеть data file и .puffin DV, а не растущий список ...delete.parquet.

Что дают числа

В оригинальной статье приведён Doris-side benchmark на нескольких сценариях. Самое важное:

Сценарий

Iceberg V2: Position Deletes

Iceberg V3: Deletion Vector

16 data files, 20% deleted

336 files to open: 16 data + 320 delete

17 files: 16 data + 1 Puffin

100M rows, 99% deleted

98 MiB delete storage

3.8 MiB DV storage

Delete metadata reduction

~96%

Latency на 16 data files и 1M rows:

Delete %

Doris V2

Doris V3

Speedup

5%

0.31s

0.15s

2.1x

10%

0.35s

0.16s

2.2x

20%

0.43s

0.17s

2.5x

30%

0.46s

0.14s

3.3x

40%

0.39s

0.17s

2.3x

Для large file с 99% deleted:

Table version

Doris Q1

Doris Q2

V2, Position Delete

3.42s

3.28s

V3, Deletion Vector

1.03s

0.86s

Speedup

~3x

~3x

Doris on Iceberg V3 Deletion Vector performance

Doris on Iceberg V3 Deletion Vector performance

Doris V3 vs V2: latency остаётся плоской при росте delete ratio, в то время как V2 деградирует линейно. Источник: оригинальная статья Mingyu Chen.

Смысл не в конкретном множителе «всегда 3x». Паттерн другой: под V2 anti-join cost растёт вместе с числом delete files, и query latency деградирует по мере роста delete ratio. Под V3 bitmap применяется за один проход, и latency остаётся практически плоской независимо от того, сколько строк помечено на удаление.

Что именно Doris реализует для Deletion Vectors

Важный нюанс из оригинала: чтобы Deletion Vectors приносили пользу, движок должен уметь и читать, и писать DV. Read-only движок может потреблять DV, записанные кем-то другим, но сам их не создаёт. Write-only движок создаёт DV, которые никто не прочитает.

Doris 4.1 поддерживает обе стороны:

  • Read: Puffin-format Deletion Vectors читаются без дополнительной настройки. V3-таблицы queryable сразу.

  • Write: DELETE, UPDATE, MERGE INTO на V3-таблице автоматически создают Puffin DV вместо Position Delete files. Пользователю достаточно указать format-version = 3 при создании таблицы — SQL-синтаксис не меняется.

Caveats по Deletion Vectors

Три практических ограничения:

  • DELETE, UPDATE, MERGE INTO требуют format-version >= 2.

  • Deletion Vectors появляются только при format-version = 3.

  • Concurrent writes используют Iceberg optimistic concurrency control, так что конфликты будут всплывать как transaction exceptions. Для high-conflict workloads нужны retry или сериализация.


Часть 6: Row Lineage — watermark для CDC без snapshot diff

Deletion Vectors отвечают на performance debt. Теперь про observability debt.

В Iceberg V1/V2 change tracking в основном живёт на уровне snapshot. Это нормальная модель для многих задач, но у неё есть неприятный эффект: физический rewrite может выглядеть как логическое изменение.

Например:

  • downstream-система подписана на изменения Iceberg-таблицы;

  • она сравнивает snapshots;

  • между checkpoint’ами прошёл rewrite_data_files;

  • физические файлы новые, snapshot новый;

  • downstream не знает, изменились строки логически или просто переехали между файлами.

И начинает переобрабатывать лишнее.

Iceberg V3 добавляет Row Lineage: две скрытые системные колонки для каждой строки.

Iceberg V3 Row Lineage hidden columns

Iceberg V3 Row Lineage hidden columns

Row Lineage: _row_id даёт стабильную идентичность строки, _last_updated_sequence_number — watermark для CDC. Источник: оригинальная статья Mingyu Chen.

Колонка

Что означает

_row_id

стабильный numeric identifier строки

_last_updated_sequence_number

sequence number последнего логического изменения строки

Обе колонки поддерживаются системой. Пользователь не записывает их руками.

Самая важная часть: _last_updated_sequence_number меняется при настоящем UPDATE или MERGE INTO, но не меняется при compaction и physical rewrite. Значит, его можно использовать как CDC watermark.

Пример:

CREATE TABLE users_v3 (
    id INT,
    name STRING,
    email STRING
) PROPERTIES ('format-version' = '3');

SET show_hidden_columns = true;

INSERT INTO users_v3 VALUES
    (1, 'Alice', 'alice@x.com'),
    (2, 'Bob',   'bob@x.com'),
    (3, 'Carol', 'carol@x.com');

SELECT id, name, email, _row_id, _last_updated_sequence_number
FROM users_v3;

После первого insert все три строки получают стабильные _row_id, а _last_updated_sequence_number равен 1.

Дальше меняем Bob:

UPDATE users_v3
SET email = 'bob@newmail.com'
WHERE id = 2;

У Bob _row_id остаётся тем же, но _last_updated_sequence_number становится 2. Alice и Carol остаются с SN = 1.

Добавляем Dora:

INSERT INTO users_v3
VALUES (4, 'Dora', 'dora@x.com');

Dora получает новый _row_id и SN = 3.

Теперь downstream, который синхронизирован по watermark 1, может запросить только реальные изменения:

SELECT id, name, email, _last_updated_sequence_number
FROM users_v3
WHERE _last_updated_sequence_number > 1;

Вернутся Bob и Dora. Alice и Carol не попадут в pipeline. Если между checkpoint’ами случится compaction, они всё равно не попадут, потому что physical rewrite не двигает _last_updated_sequence_number.

Это и есть главный выигрыш: downstream хранит один integer watermark, а не пытается угадывать смысл snapshot diff.

Как меняется CDC-pipeline

Контраст в архитектуре:

V2 (snapshot diff): Upstream (PostgreSQL и т.д.) → Flink CDC → Iceberg V2 Sink → Downstream делает snapshot diff, но не может отличить реальное изменение от compaction/rewrite → false positives → переобработка.

V3 (Row Lineage): Upstream → Flink CDC → Iceberg V3 Sink → Downstream делает WHERE _last_updated_sequence_number > :watermark → только реальные изменения → без false positives.

Разница не только в точности, но и в простоте: вместо инфраструктуры для snapshot diffing — один SQL-запрос с одним integer watermark.

Что Doris реализует для Row Lineage

В Doris скрытые колонки можно читать явно или включить их отображение:

SET show_hidden_columns = true;

UPDATE и MERGE INTO на V3-таблицах автоматически обновляют _last_updated_sequence_number. Пользователь не добавляет отдельный audit trigger, не пишет отдельную таблицу lineage, не синхронизирует внешний log.

Но caveat снова важный: Row Lineage в оригинале помечен как experimental. Проверять на своём workload обязательно.


Часть 7: Audit trail через _row_id

Ещё один полезный сценарий — аудит.

Финансовый аудитор спрашивает: «Order 102 менялся три раза. Какие суммы были на каждом шаге?»

В V2 обычно приходится идти во внешнюю audit log, потом пытаться совместить её с текущим состоянием таблицы, потом учитывать compaction, file rewrite и прочие радости жизни. Это возможно, но хрупко: audit system и table state живут в разных местах.

В V3 появляется более удобная опора: _row_id остаётся lifetime identity строки.

Условно:

Snapshot SN

Amount

Event

SN=1

80.00

Order created

SN=2

90.00

Customer complaint, partial refund

SN=3

100.00

Final settlement

Сама Row Lineage не заменяет time travel и не хранит полную историю «магически». Важная деталь именно в связке:

  • _row_id даёт стабильный ключ строки;

  • snapshots/time travel дают возможность смотреть прошлые состояния;

  • _last_updated_sequence_number показывает логическую хронологию изменений;

  • compaction не ломает эту идентичность.

Примерный запрос для восстановления состояния:

SELECT order_id, amount
FROM orders_v3 FOR VERSION AS OF <snapshot_id_at_SN2>
WHERE _row_id = 1;

Да, вам всё ещё нужны snapshots. Но _row_id и _last_updated_sequence_number — это логические свойства строки, а не физические. Если между шагами прошёл ALTER TABLE orders_v3 EXECUTE rewrite_data_files(), физические файлы будут новые, а _row_id останется прежним и SN не изменится. Под V2 тот же compaction создал бы новый snapshot, и без внешней audit log вы бы не знали, менялись строки логически или нет.


Часть 8: Quick Start

Если хочется быстро потрогать механику руками, минимальный путь такой.

Prerequisites

Нужны:

  • Apache Doris 4.1.0 или новее (скачать или docker pull apache/doris:4.1.0).

  • Iceberg catalog с поддержкой V3.

  • Object store, доступный Doris BE: S3, MinIO, OSS или HDFS.

По catalog’ам в оригинале рекомендация такая:

  • REST Catalog — наиболее надёжный путь для V3. Например, Apache Polaris, Lakekeeper или Tabular open-source REST catalog image.

  • Hive Metastore-backed catalogs подходят для V1/V2 reads, но отстают по V3.

  • AWS Glue и Aliyun DLF на момент оригинальной статьи указаны как read-only.

1. Подключить catalog

CREATE CATALOG iceberg_v3 PROPERTIES (
    'type'                 = 'iceberg',
    'iceberg.catalog.type' = 'rest',
    'uri'                  = 'http://your-rest-catalog:8181',
    'warehouse'            = 's3://your-bucket/warehouse',
    's3.endpoint'          = 'https://s3.us-west-2.amazonaws.com',
    's3.access_key'        = '<AK>',
    's3.secret_key'        = '<SK>',
    's3.region'            = 'us-west-2'
);

SWITCH iceberg_v3;
CREATE DATABASE IF NOT EXISTS demo;
USE demo;

2. Создать V3-таблицу и сделать DML

CREATE TABLE orders (
    id INT,
    status STRING,
    amount DECIMAL(10,2)
) PROPERTIES ('format-version' = '3');

INSERT INTO orders VALUES
    (1, 'pending', 100),
    (2, 'pending', 200),
    (3, 'pending', 300);

UPDATE orders SET status = 'shipped' WHERE id = 1;
DELETE FROM orders WHERE id = 3;

3. Проверить V3-поведение

Проверка Deletion Vector:

SELECT content, file_path, record_count
FROM orders$files;

Ожидаем .puffin DV рядом с data file, а не россыпь parquet delete files.

Проверка Row Lineage (скрытые колонки по умолчанию не видны):

SET show_hidden_columns = true;

SELECT id, status, _row_id, _last_updated_sequence_number
FROM orders;

4. Попробовать MERGE INTO

MERGE INTO orders t
USING (SELECT 1 AS id, 'delivered' AS status, 110 AS amount) s
ON t.id = s.id
WHEN MATCHED THEN UPDATE SET status = s.status, amount = s.amount
WHEN NOT MATCHED THEN INSERT (id, status, amount)
VALUES (s.id, s.status, s.amount);

Напоминание: MERGE INTO experimental. Это хороший кандидат для POC, но не для blind rollout в production.

Если orders$files показывает один Puffin file рядом с data file, а выжившие строки несут корректные значения _row_id / _last_updated_sequence_number — ваш стек V3-ready end to end.

Полный справочник по конфигурации: Doris Iceberg catalog docs.


Часть 9: Common gotchas

Короткий список того, обо что легко удариться:

  • format-version = 3 нужно задать при создании таблицы или мигрировать существующую V2-таблицу через ALTER TABLE ... SET PROPERTIES ('format-version' = '3').

  • Hidden columns (_row_id, _last_updated_sequence_number) работают только на V3. На V1/V2 таблицах запрос к ним даст ошибку.

  • Если MERGE INTO не парсится, проверьте Doris 4.1.0+ и target table V2/V3.

  • DML требует format-version >= 2; Deletion Vectors появляются только на V3.

  • Concurrent writers могут ловить optimistic concurrency conflicts. Для high-conflict workloads нужны retry, backoff или сериализация.

  • REST Catalog behavior зависит от конкретной реализации. Для production лучше свериться с support matrix в Doris docs.

  • AWS Glue и Aliyun DLF в оригинале указаны как read-only на момент статьи.


Часть 10: Где граница Doris, Spark и Flink

Самая опасная версия этой статьи звучала бы так: «Теперь Doris заменяет Spark для Iceberg».

Это плохая версия. Не надо так.

Более честная матрица:

Инструмент

Где уместен

Doris

real-time queries, small DML, incremental reconciliation, row-level provenance, day-to-day maintenance, диагностика Iceberg tables из SQL

Spark

heavy backfills, cross-source ETL, long-running batch jobs

Flink

streaming ingestion, continuous CDC write paths

Doris 4.1 не заменяет Spark. Он сокращает число случаев, когда Spark приходится запускать ради мелкой операции вокруг проблемы, которую вы уже нашли.

Если запрос уже привёл вас в SQL-клиент — маленькое исправление не должно превращаться в distributed-computing ритуал с отдельным кластером.


Итог

Одна фраза хорошо описывает направление Doris на Iceberg:

Если запрос уже привёл вас в SQL-клиент, маленькое исправление, incremental reconciliation или day-to-day maintenance не должны заставлять уходить в другой движок.

Что меняется в Apache Doris 4.1:

  • UPDATE, DELETE, MERGE INTO закрывают loop «query → fix → verify» внутри одного SQL-клиента.

  • Deletion Vectors в Iceberg V3 не дают DML накапливать performance debt через растущий набор Position Delete files.

  • Row Lineage даёт _row_id и _last_updated_sequence_number, чтобы CDC и audit не ломались от compaction и physical rewrite.

  • Maintenance и диагностика становятся ближе к человеку, который уже смотрит на данные.

Что не меняется:

  • Spark остаётся для тяжёлых batch/backfill/ETL задач.

  • Flink остаётся для streaming ingestion.

  • MERGE INTO и Row Lineage в оригинале помечены как experimental.

  • Production adoption требует POC на своём catalog, storage, concurrency pattern и workload.

Если коротко: Iceberg V3 делает DML архитектурно более здоровым, а Doris 4.1 пытается сделать этот DML доступным из того же SQL-движка, где вы уже нашли проблему.

Для data engineer’а это не звучит как революция. Это звучит как минус один ticket, минус один job, минус один context switch.

А иногда именно это и есть настоящая продуктивность.


Ссылки


Оригинал по лицензии CC BY 4.0 © 2026 Mingyu Chen (Rayner).