- 들어가며 — v3에서 로우 리니지는 끌 수 없다
- 로우 리니지가 주는 것
- 핵심 설계 — 값을 쓰지 않고 상속한다
- 읽을 때의 규칙
- 여기서부터가 진짜 — 리니지가 끊기는 자리들
- 엔진 현실 점검 — 스펙의 "must"와 구현의 거리
- 언제 쓰지 말아야 하나
- 마치며
- 참고 자료
들어가며 — v3에서 로우 리니지는 끌 수 없다
Apache Iceberg 스펙 원문의 "Format Versioning" 절은 이렇게 시작합니다 — 버전 1, 2, 3은 완료되었고 커뮤니티가 채택했으며, 버전 4는 활발히 개발 중이고 아직 공식 채택되지 않았다. v3가 더 이상 미래형이 아니라는 뜻입니다. Java 라이브러리는 이미 v4까지 읽고 쓸 수 있게 상수가 잡혀 있습니다(SUPPORTED_TABLE_FORMAT_VERSION = 4).
v3가 더한 것은 목록으로 보면 이렇습니다 — 나노초 타임스탬프, unknown·variant·geometry·geography 타입, 컬럼 기본값, 다중 인자 파티션 변환, 로우 리니지 추적, 바이너리 삭제 벡터, 테이블 암호화 키. 이 중 삭제 벡터는 이야기가 많이 됐습니다. 이 글은 상대적으로 덜 다뤄진, 그리고 훨씬 더 조용히 당신의 파이프라인을 물어뜯을 수 있는 로우 리니지를 봅니다.
먼저 알아야 할 사실 하나. 로우 리니지는 켜고 끄는 테이블 속성이 아닙니다. Iceberg 코어의 판정 로직은 이게 전부입니다.
public static boolean supportsRowLineage(Table table) {
Preconditions.checkArgument(null != table, "Invalid table: null");
if (table instanceof BaseMetadataTable) {
return false;
}
return formatVersion(table) >= TableMetadata.MIN_FORMAT_VERSION_ROW_LINEAGE;
}
MIN_FORMAT_VERSION_ROW_LINEAGE는 3입니다. 즉 포맷 버전이 3 이상이면 로우 리니지는 켜져 있습니다. 초기 설계에는 이걸 켜는 메타데이터 필드가 있었지만 제거됐고("Core: Enable row lineage for all v3 tables", PR #12593), 스펙은 단정적으로 씁니다 — v3 이상에서 Iceberg 테이블은 새로 생성되는 모든 행에 대해 로우 리니지 필드를 반드시 추적해야 한다. v3로 올리는 순간, 그 테이블에 쓰는 모든 엔진이 리니지 유지 의무를 집니다. 당신이 그 기능을 원했든 아니든.
반대로 안심할 사실도 하나. 같은 파일에서 DEFAULT_TABLE_FORMAT_VERSION은 여전히 2입니다. 2026년 7월 현재 main 브랜치에서도 그렇습니다. 새 테이블을 만든다고 v3가 되지는 않습니다 — v3는 명시적으로 선택해야 들어가는 문입니다.
로우 리니지가 주는 것
v3는 두 개의 예약된 메타데이터 컬럼을 정의합니다.
| 필드 ID | 이름 | 타입 | 의미 |
|---|---|---|---|
| 2147483540 | _row_id | long | 테이블 안에서 행마다 유일한 식별자 |
| 2147483539 | _last_updated_sequence_number | long | 그 행을 마지막으로 수정한 커밋의 시퀀스 번호 |
핵심은 _row_id가 행의 정체성이라는 점입니다. 행이 UPDATE되어도, 컴팩션으로 다른 파일에 옮겨 써져도, 파티션이 바뀌어도 같은 _row_id를 유지해야 합니다. 물리적 위치와 논리적 정체성을 분리하는 것 — 이게 없으면 "이 행이 언제 처음 들어왔고 그동안 몇 번 바뀌었나"를 테이블 바깥의 별도 장부 없이는 답할 수 없습니다.
여기에 _last_updated_sequence_number가 붙으면 "시퀀스 번호 N 이후에 바뀐 행만 달라"가 테이블 메타데이터만으로 표현됩니다. 증분 처리와 CDC를 외부 도구 없이 세우는 토대가 이겁니다.
핵심 설계 — 값을 쓰지 않고 상속한다
여기가 이 기능의 전부입니다. _row_id는 데이터 파일에 저장되지 않습니다.
새 행을 쓸 때 writer는 _row_id와 _last_updated_sequence_number를 null로 둡니다. 아예 컬럼을 생략해도 됩니다 — 스펙은 컬럼이 없으면 reader가 "모든 행에서 null인 컬럼이 존재하는 것처럼" 취급하라고 합니다. 실제 값은 읽는 시점에 계산됩니다.
왜 이렇게 하냐면, 스펙이 직접 답을 씁니다 — 커밋 시퀀스 번호와 시작 행 ID는 스냅샷이 성공적으로 커밋되기 전까지 정해지지 않기 때문입니다. Iceberg의 커밋은 낙관적 동시성 제어입니다. 두 writer가 동시에 커밋을 시도하면 하나는 지고, 진 쪽은 최신 스냅샷 위에서 재시도합니다. 만약 행 ID를 데이터 파일 안에 박아 넣었다면, 커밋이 재시도될 때마다 이미 다 써 놓은 데이터 파일과 매니페스트 파일을 전부 다시 써야 합니다. 쓰기가 클수록 충돌 한 번의 대가가 그대로 커지는 구조입니다. 스펙의 표현대로, 상속은 값이 정해지기 전에 파일을 쓸 수 있게 해서 낙관적 커밋이 재시도될 때 파일을 다시 쓰지 않아도 되게 하려고 있습니다.
대가는 명확합니다. 값이 없으니, 값을 만들어 내는 체인이 정확해야 합니다. 체인은 이렇게 내려옵니다.
테이블 메타데이터 next-row-id (다음에 나눠 줄 ID의 시작점)
└─ 스냅샷 first-row-id = 커밋 시점의 테이블 next-row-id
└─ 매니페스트 first_row_id = 앞선 매니페스트들의 행 수만큼 밀린 값
└─ 데이터 파일 first_row_id = 앞선 파일들의 record_count 만큼 밀린 값
└─ 행 _row_id = 데이터 파일의 first_row_id + _pos
맨 아래 한 줄이 결론입니다 — 행의 ID는 그 행이 담긴 데이터 파일의 first_row_id에 파일 내 행 위치(_pos)를 더한 값입니다. ID를 저장하는 대신, ID 공간을 구간으로 예약해 나눠 주고 위치로 오프셋을 잡는 구조입니다.
스펙의 예제를 따라가 보기
스펙에 실린 예제가 이 체인을 잘 보여줍니다. next-row-id가 1000인 테이블에서 시작합니다. append 스냅샷은 first-row-id로 테이블의 현재 next-row-id인 1000을 받습니다.
매니페스트 리스트를 쓸 때 각 매니페스트에 first_row_id가 할당됩니다.
manifest_path | added_rows_count | existing_rows_count | first_row_id |
|---|---|---|---|
| existing | 75 | 0 | 925 |
| added1 | 100 | 25 | 1000 |
| added2 | 0 | 100 | 1125 |
| added3 | 125 | 25 | 1225 |
added1은 스냅샷과 같은 1000을 받고, 그다음부터는 앞선 매니페스트의 행 수(추가된 행 + 기존 행)만큼 밀립니다. 1000 + 125 = 1125, 1125 + 100 = 1225.
여기서 스펙이 짚는 함정 하나 — added2는 추가된 데이터 파일이 하나도 없는데도 다음 매니페스트의 first_row_id를 100만큼 밀어 놓습니다. first_row_id가 없는 데이터 파일이라면 EXISTING 상태여도 ID를 할당받을 수 있기 때문에 그만큼 공간을 비워 두는 겁니다.
첫 매니페스트 added1 안의 데이터 파일들은 이렇게 됩니다.
status | file_path | record_count | first_row_id |
|---|---|---|---|
| EXISTING | data1 | 25 | 800 |
| ADDED | data2 | 50 | null → 1000 |
| ADDED | data3 | 50 | null → 1050 |
data1은 이미 예전에 800을 할당받았으므로 그 값이 그대로 복사됩니다. data2와 data3은 null로 쓰여 있고, 읽을 때 매니페스트의 first_row_id(1000)에서 시작해 앞선 파일들의 record_count만큼 밀려 각각 1000과 1050을 받습니다. 그래서 data3의 세 번째 행의 _row_id는 1050 + 2 = 1052입니다.
커밋이 끝나면 테이블의 next-row-id가 갱신됩니다. 스펙의 의무 조항은 "그 스냅샷에서 새로 할당된 행 ID 수 이상"만큼 밀라는 것이고, 권장 계산은 first_row_id를 할당받은 매니페스트들의 added_rows_count와 existing_rows_count를 모두 더하라는 것입니다. 예제에서 그 합이 375(added1 125 + added2 100 + added3 150)이므로 next-row-id는 1000 + 375 = 1375가 됩니다.
여기서 EXISTING 행들도 ID 공간을 소비한다는 점에 주목하세요. 이미 자기 ID를 갖고 있어 새 ID를 받지 않는 행들인데도 범위는 예약됩니다. 의무는 하한만 정하고 권장 계산은 그보다 넉넉히 잡으니, ID 공간에는 구멍이 생기는 게 정상입니다. _row_id는 유일할 뿐 조밀하지 않습니다 — 행 개수를 세거나 연속성을 가정하는 용도로 쓰면 안 됩니다.
읽을 때의 규칙
reader 쪽 규칙은 스펙 부록 E에 못 박혀 있습니다.
- 데이터 파일의
first_row_id가 non-null이면, reader는 null이거나 없는_row_id를first_row_id + _pos로 채운다. - 마찬가지로 null이거나 없는
_last_updated_sequence_number를 그 데이터 파일의data_sequence_number로 채운다. - 이미 non-null인 값은 손대지 않고 그대로 읽는다.
- 데이터 파일의
first_row_id가 null이면, reader는_row_id와_last_updated_sequence_number를 null로 낸다.
세 번째 규칙이 중요합니다. 값이 이미 있으면 그건 "옮겨져 온 행"이라는 뜻이고, 그 정체성을 덮어쓰면 안 됩니다.
여기서부터가 진짜 — 리니지가 끊기는 자리들
컴팩션이 리니지를 죽인다
행이 다른 데이터 파일로 옮겨질 때(컴팩션, 리라이트, 파티션 변경 등 이유 불문) writer가 지켜야 할 규칙은 이렇습니다.
- 행의 기존 non-null
_row_id는 새 데이터 파일로 복사되어야 한다. - 그 쓰기가 행을 수정했다면
_last_updated_sequence_number를null로 둔다(수정 시점의 시퀀스 번호가 상속되도록). - 수정하지 않았다면 기존 non-null
_last_updated_sequence_number를 그대로 복사한다.
말은 간단한데, 구현 입장에서는 고약합니다. 컴팩션은 원래 "데이터를 읽어서 큰 파일로 다시 쓰는" 단순한 작업입니다. 그런데 v3에서는 컴팩션 job이 숨은 메타데이터 컬럼 두 개를 함께 읽어서 함께 써야 합니다. 그냥 다시 쓰면 _row_id가 전부 새로 발급되고, 테이블의 모든 행이 "방금 처음 생긴 행"이 되어 버립니다. 리니지 위에 세운 CDC 파이프라인은 전체 재처리를 하게 됩니다. 조용히, 에러 하나 없이.
이게 이론적 우려가 아니라는 증거는 커밋 로그에 다 있습니다. 뒤에서 봅니다.
equality delete는 리니지를 추적하지 않는다
스펙이 명시적으로 예외를 둡니다 — equality delete로 수정된 행에 대해서는 로우 리니지를 추적하지 않습니다. 이유도 스펙에 있습니다. equality delete를 쓰는 엔진은 애초에 기존 데이터를 읽지 않고 변경을 쓰기 때문에, 새 행에 원래의 row ID를 넣어 줄 수가 없습니다. 그래서 이런 갱신은 기존 행이 완전히 제거되고 유일한 새 행이 추가된 것처럼 취급됩니다.
이건 큽니다. Flink의 upsert 스트리밍처럼 equality delete에 의존하는 워크로드라면, v3로 올려도 행 정체성은 UPDATE를 건너지 못합니다. 로우 리니지를 CDC 기반으로 쓰려는 계획이 여기서 정확히 절반쯤 무너질 수 있습니다.
업그레이드한 테이블의 과거는 영원히 null
v2 테이블을 v3로 올리면 next-row-id는 0으로 초기화되고, 기존 스냅샷은 수정되지 않습니다. first-row-id가 없는 그 스냅샷들에서는 데이터 파일과 매니페스트의 first_row_id가 null이고, 따라서 모든 행의 _row_id가 null로 읽힙니다. 스펙의 표현 그대로 — 업그레이드 전에 만들어진 스냅샷에는 행 ID가 없습니다.
업그레이드 후 첫 커밋에서 기존 파일들에도 ID가 할당되긴 합니다. 하지만 그건 "그 파일들이 지금 막 ID를 받았다"는 뜻이지, 과거 이력이 복원된다는 뜻이 아닙니다. 로우 리니지는 소급되지 않습니다.
브랜치마다 ID 범위가 갈라진다
스펙이 조용히 남긴 각주 하나. 업그레이드 후, 서로 다른 브랜치의 새 스냅샷들은 커밋 시점의 next-row-id에 따라 기존 데이터 파일들에 서로 겹치지 않는 ID 범위를 할당합니다. 여러 브랜치에 있는 데이터 파일에 대해 writer는 다른 브랜치의 first_row_id를 쓸 수도 있고, 큰 메타데이터 재작성을 피하려고 새 first_row_id를 할당할 수도 있습니다.
즉 브랜치를 쓰는 테이블에서 _row_id는 브랜치 간에 같은 행을 가리킨다고 보장되지 않습니다. _row_id를 조인 키로 쓰려던 계획이 있다면 여기서 멈춰야 합니다.
엔진 현실 점검 — 스펙의 "must"와 구현의 거리
여기가 이 글을 쓴 이유입니다. 벤더 블로그들은 "v3 지원"을 체크박스로 그립니다. 실제 코드는 다른 이야기를 합니다. 아래는 2026년 7월 16일 기준으로 각 저장소의 main/master 브랜치와 병합된 PR을 직접 확인한 내용입니다.
Spark 4.0이 사실상의 레퍼런스입니다. 로우 리니지 지원이 PR #13310으로 2025년 7월 14일에 들어갔습니다. PR 설명에 따르면 Spark 4.0에서 새로 생긴 조건부 nullification 메커니즘을 쓰는데, 이게 Spark 3.5에서는 없어서 Iceberg가 자체 rewrite 규칙을 따로 구현해야 했습니다(PR #12736, 2025년 4월 28일). 그 PR이 정리한 규칙은 앞에서 본 스펙 그대로입니다 — 기존 row ID는 연산 종류와 무관하게 항상 이어받고, INSERT에서는 null이며, 수정되지 않은 행의 시퀀스 번호는 보존하고 수정된 행은 null로 만든다.
컴팩션 보존은 별도의 작업이었습니다. 앞서 "컴팩션이 리니지를 죽인다"고 한 게 정확히 이것 때문입니다. Spark 4.0의 컴팩션 리니지 보존은 PR #13555로 2025년 7월 22일에야 들어갔고, 3.5와 3.4로는 그다음 날 백포트됐습니다(PR #13637, #13641). 구현 방식이 흥미로운데, PR 작성자 본인의 표현으로는 rewrite인 경우 SparkTable의 스키마를 "가로채서(hijacking)" 메타데이터 컬럼을 추가하는 방식입니다. 우아하지 않다는 걸 저자도 알고 있습니다.
Trino는 생각보다 앞서 있습니다. 이 부분은 검색 결과가 저를 속일 뻔했습니다 — 여러 2차 자료가 "Trino는 2026년 6월 기준 v3 준비가 안 됐다"거나 "로우 리니지는 아직"이라고 적고 있는데, 저장소를 직접 보면 사실이 아닙니다. 삭제 벡터는 PR #27788로 2026년 1월 16일에, 로우 리니지는 PR #27836으로 2026년 3월 19일에 master로 병합됐습니다. 후자는 읽기뿐 아니라 UPDATE/MERGE에서 원래 row ID를 보존하고, v3 테이블에서 OPTIMIZE를 다시 허용하면서 리니지를 지키는 것까지 포함합니다.
Trino가 그 전에 한 일도 볼 만합니다. PR #27786(2026년 1월 10일)은 v3 테이블 생성을 열되, 아직 구현하지 않은 v3 기능은 전부 NOT_SUPPORTED로 명시적으로 거부하도록 만들었습니다. v3 테이블에 대한 DELETE/UPDATE/MERGE, OPTIMIZE, add_files, 삭제 벡터, 컬럼 기본값, 암호화가 모두 여기에 걸렸습니다. PR 설명의 표현대로 "스펙 위반을 피하기 위해" 조용히 틀린 답을 내는 대신 빠르고 예측 가능하게 실패하는 쪽을 택한 겁니다. 부분 구현 상태에서 이건 옳은 선택이고, 다른 엔진들이 배울 만합니다.
컬럼 이름은 엔진마다 다릅니다. Iceberg 코어와 Spark는 _row_id / _last_updated_sequence_number를 쓰고, Trino는 자기 메타데이터 컬럼 관례에 따라 $row_id / $last_updated_sequence_number로 노출합니다. 필드 ID는 같으니 데이터는 호환되지만, 쿼리는 그대로 옮겨지지 않습니다.
Flink는 두 갈래입니다. 이게 가장 헷갈리는 부분입니다. Iceberg main 브랜치의 Flink 커넥터에는 컴팩션 경로가 둘 있고, 로우 리니지에 대해 정반대로 동작합니다.
구형 actions API(org.apache.iceberg.flink.actions.RewriteDataFilesAction)는 지금도 v3 테이블을 그냥 거부합니다. main 브랜치의 v1.20, v2.0, v2.1 모두에 이 코드가 그대로 있습니다.
Preconditions.checkArgument(
!TableUtil.supportsRowLineage(table),
"Flink does not support compaction on row lineage enabled tables (V3+)");
반면 신형 maintenance API(org.apache.iceberg.flink.maintenance.operator.DataFileRewriteRunner)는 리니지를 보존합니다. PR #14149로 2025년 11월 6일에 들어갔고, 코드는 이렇게 갈라집니다.
boolean preserveRowId = TableUtil.supportsRowLineage(value.table());
try (TaskWriter<RowData> writer = writerFor(value, preserveRowId)) {
try (DataIterator<RowData> iterator = readerFor(value, preserveRowId)) {
정리하면 — Flink로 v3 테이블을 컴팩션하려면 maintenance API를 써야 하고, 구형 actions API를 쓰고 있다면 v3로 올리는 순간 컴팩션이 예외를 던지며 멈춥니다. 나쁜 소식 같지만 실은 좋은 설계입니다. 조용히 리니지를 날려 먹는 것보다 시끄럽게 멈추는 게 낫습니다. 참고로 저 거부 로직 자체가 PR #13646("Flink: fail file rewrite for V3 tables as row lineage not supported", 2025년 7월 23일)으로 의도적으로 추가된 것입니다.
PyIceberg는 아직 v3 테이블을 쓸 수 없습니다. main 브랜치의 pyiceberg/table/metadata.py에는 아직 이렇게 적혀 있습니다.
SUPPORTED_TABLE_FORMAT_VERSION = 2
v3 매니페스트/매니페스트 리스트 쓰기와 로우 리니지 스냅샷 커밋을 넣으려던 PR #3070은 병합되지 않고 닫혔고, 후속인 PR #3551("Writing v3 Table Metadata")은 2026년 7월 8일에 마지막으로 갱신된 채 아직 열려 있습니다. v3 업그레이드 지원 PR #3623은 #3551이 병합되기를 기다리는 draft 상태입니다. Python으로 Iceberg에 쓰는 파이프라인이 있다면 v3 계획에서 일단 빼 두는 게 정확합니다.
그리고 상속 로직은 아직도 고쳐지고 있습니다. 이 글을 쓰기 일주일 전인 2026년 7월 9일, PR #17039("Core: Fix row lineage last updated sequence inheritance")가 병합됐습니다. 버그는 이렇습니다 — Java가 _last_updated_sequence_number 메타데이터 컬럼을 채울 때 data_sequence_number 대신 fileSequenceNumber()를 쓰고 있었습니다.
둘의 차이가 평소엔 안 보입니다. 그런데 v2 시절 컴팩션이 더 오래된 data sequence number를 가진 대체 파일을 만들어 놓았고, 그 테이블을 나중에 v3로 올리면 갈라집니다. data_sequence_number는 파일의 내용에 결부된 값이라 리라이트를 건너 보존되지만, file_sequence_number는 그 물리적 파일이 추가된 시점입니다. 그래서 버그가 있는 버전은 "이 행이 마지막으로 수정된 시점"을 물었을 때 원래의 갱신 시점 대신 컴팩션이 돌아간 시점을 답했습니다.
이게 상속 설계의 대가를 정확히 보여줍니다. 값이 파일에 없으니, 상속 규칙 하나가 틀리면 데이터는 멀쩡한데 답이 틀립니다. 그리고 그 틀림은 예외를 던지지 않습니다. 스펙이 2024년 10월(PR #11130)에 로우 리니지를 처음 넣은 뒤 21개월이 지난 지금도 이런 버그가 나온다는 사실은, 이 기능의 난이도가 스펙 문서 분량에 비해 훨씬 높다는 뜻입니다. Avro reader 쪽에서도 2026년 2월과 3월에 비슷한 성격의 수정이 있었습니다(PR #15187, #15508).
언제 쓰지 말아야 하나
정직하게 정리하면 이렇습니다.
아직 v3로 올리지 마세요, 이런 경우엔
- 파이프라인에 PyIceberg writer가 하나라도 있다. 지금은 v3 테이블을 쓸 수 없습니다.
- Flink 구형 actions API로 컴팩션을 돌리고 있고, maintenance API로 옮길 계획이 없다.
- 워크로드가 equality delete 중심이다(전형적으로 Flink upsert 스트리밍). 리니지를 얻겠다고 올렸는데 정작 UPDATE에서 행 정체성이 안 이어집니다.
- 여러 엔진이 같은 테이블에 쓰는데, 그중 하나라도 리니지 보존이 검증되지 않았다. 리니지를 안 지키는 writer 하나가 테이블 전체의 리니지를 망가뜨립니다.
올려도 되는 경우
- writer가 Spark 4.0(또는 백포트된 3.5/3.4)이나 최근 Trino로 한정된다.
- 컴팩션 경로가 리니지를 보존하는지 실제로 확인했다 — 문서가 아니라 컴팩션 전후로
_row_id를 직접 비교해서. - 삭제 벡터의 이득만으로도 이미 값이 나온다. 로우 리니지는 v3의 덤으로 따라오는 것이고, 사실 대부분의 조직이 v3에 가는 실제 이유는 삭제 벡터입니다.
그리고 로우 리니지를 CDC로 쓰기 전에 반드시 확인할 것 — _row_id는 유일하지만 조밀하지 않고, 브랜치 간에 안정적이지 않으며, 업그레이드 이전 이력에는 존재하지 않고, equality delete를 건너지 못합니다. 이 네 가지가 당신의 설계와 충돌하지 않는지 먼저 보세요.
마치며
로우 리니지의 설계는 영리합니다. 행 ID를 저장하는 대신 ID 공간을 예약해 나눠 주고 위치로 오프셋을 잡음으로써, 낙관적 커밋이 재시도돼도 데이터 파일을 다시 쓰지 않아도 됩니다. Iceberg의 커밋 모델을 아는 사람이라면 이게 왜 이렇게 생겼는지 바로 이해할 겁니다.
대가는 책임의 이동입니다. 값이 파일에 없으니, 그 값을 만들어 내고 지켜 내는 일이 전적으로 writer와 reader의 정확성에 달립니다. 스펙은 "must"라고 쓰지만 스펙은 코드를 실행하지 않습니다. 그래서 2026년 7월 현재의 그림은 이렇습니다 — Spark 4.0은 되고, Trino는 3월부터 되고, Flink는 어느 API를 쓰느냐에 따라 갈리고, PyIceberg는 아직이고, 코어의 상속 로직은 지난주에도 고쳐졌습니다.
이건 v3를 쓰지 말라는 이야기가 아닙니다. 삭제 벡터만으로도 v3는 갈 만한 곳입니다. 다만 v3로 올리는 순간 로우 리니지는 끌 수 없이 켜지고, 그 테이블을 만지는 모든 도구가 암묵적 계약에 서명하게 된다는 걸 알고 가야 합니다. 그 계약을 못 지키는 도구가 하나라도 섞여 있으면, 당신이 얻는 건 리니지가 아니라 리니지가 있다는 착각입니다. 그게 훨씬 나쁩니다.
Iceberg의 전반적인 구조와 운영은 Apache Iceberg 데이터 레이크하우스 가이드 편에서 따로 다룹니다.
참고 자료
- Apache Iceberg Table Spec — 원문 — 로우 리니지 절과 부록 E(버전별 변경)가 이 글의 1차 근거입니다. 원문은 raw.githubusercontent.com의 format/spec.md에서 그대로 볼 수 있습니다
- Iceberg
TableMetadata.java—DEFAULT_TABLE_FORMAT_VERSION = 2,SUPPORTED_TABLE_FORMAT_VERSION = 4,MIN_FORMAT_VERSION_ROW_LINEAGE = 3 - Iceberg
TableUtil.java— 로우 리니지가 포맷 버전만으로 결정된다는 판정 로직 - Iceberg
MetadataColumns.java—_row_id,_last_updated_sequence_number정의 - PR #11130 — Spec: Adds Row Lineage — 로우 리니지가 스펙에 처음 들어간 커밋(2024-10)
- PR #12593 — Core: Enable row lineage for all v3 tables — 옵션이 아니게 된 지점
- PR #13310 — Spark 4.0: Row Lineage support
- PR #12736 — Spark 3.5: Update MERGE and UPDATE for row lineage — 리니지 보존 규칙이 가장 명확하게 서술된 PR 설명
- PR #13555 — Spark 4.0: Preserve row lineage information on compaction
- PR #17039 — Core: Fix row lineage last updated sequence inheritance — 2026-07-09에 병합된 상속 버그 수정
- PR #14149 — Flink: Preserve row lineage in RewriteDataFiles
- Flink
RewriteDataFilesAction.java— 구형 API가 v3를 거부하는 코드 - Flink
DataFileRewriteRunner.java— 신형 maintenance API의 리니지 보존 - Trino PR #27836 — Iceberg v3 Row lineage — 2026-03-19 병합
- Trino PR #27788 — Iceberg v3 Deletion vector — 2026-01-16 병합
- Trino PR #27786 — Enable reading Iceberg v3 failing on all unimplemented features — 미구현 기능을 명시적으로 거부하는 접근
- Trino
IcebergMetadataColumn.java—$row_id컬럼 이름의 근거 - PyIceberg
metadata.py—SUPPORTED_TABLE_FORMAT_VERSION = 2 - PyIceberg PR #3551 — Writing v3 Table Metadata — 열려 있는 상태
- Apache Iceberg Releases — 1.10.0(2025-09-11), 1.11.0(2026-05-19) 릴리스 노트
- Apache Iceberg 데이터 레이크하우스 가이드 (관련 글)
현재 단락 (1/124)
Apache Iceberg 스펙 원문의 "Format Versioning" 절은 이렇게 시작합니다 — 버전 1, 2, 3은 완료되었고 커뮤니티가 채택했으며, 버전 4는 활발히 개발...