レイクハウスのテーブル形式 — Apache Iceberg をメタデータで理解する
同じ削除と MERGE を二通りで — ファイルを書き直すか、消す行を書き留めるか
한국어 원문으로 표시합니다.
목표
같은 3월 주문으로 copy-on-write(COW) 표와 merge-on-read(MOR) 표를 만들고, 같은 DELETE 와 같은 MERGE 를 두 표에 돌린다. COW 는 바뀐 행이 든 데이터 파일을 통째로 다시 쓰고, MOR 은 '어느 파일의 몇 번째 행을 지운다' 는 위치 삭제 파일만 더한다는 것을 스냅샷 요약과 파일로 확인한다. 위치 삭제 파일을 직접 열어 보고, 다른 엔진(DuckDB·pyiceberg)이 그 삭제를 반영해 읽는지 본다.
왜 중요한가
Parquet 파일은 고칠 수 없다. 한 행을 바꾸려면 그 파일을 새로 쓰거나(COW), 그 행이 지워졌다는 사실을 다른 파일에 적어 두고 읽을 때 걸러 내야(MOR) 한다. COW 는 읽기가 싸고 쓰기가 비싸다 — 백만 행 파일에서 한 행을 고쳐도 백만 행을 다시 쓴다. MOR 은 쓰기가 싸고 읽기가 비싸다 — 읽을 때마다 삭제 파일을 데이터 파일과 맞춰 봐야 하고, 삭제 파일이 쌓일수록 느려진다.
CDC 처럼 작은 변경이 자주 오는 표는 MOR 로 쓰고 주기적으로 압축하는 것이 보통이고, 하루 한 번 크게 고치고 많이 읽는 표는 COW 가 낫다. 어느 쪽이든 표 속성 세 개(write.delete.mode·write.update.mode·write.merge.mode)로 정해지고, 판단 근거는 느낌이 아니라 커밋마다 요약에 남는 쓴 바이트와 파일 수다.
단계
- /root/ice/rl/tables.py(앱
ice-rl-tables)로lake.rl.cow(세 모드 모두copy-on-write)와lake.rl.mor(세 모드 모두merge-on-read)를 format-version 2 로 만들고, 3월 한 달을 각각 한 번에 넣으세요. - /root/ice/rl/delete_cow.py(앱
ice-rl-delete-cow)로DELETE FROM lake.rl.cow WHERE status = 'cancelled'를 돌리세요. - /root/ice/rl/delete_mor.py(앱
ice-rl-delete-mor)로 같은 삭제를lake.rl.mor에 돌리세요. - /root/ice/rl/merge.py(앱
ice-rl-merge)로/data/ice/changes.csv(op 가 U·D·I)를 두 표에 똑같이 MERGE 하세요. - /root/ice/rl/posdel.py 로
lake.rl.mor의 위치 삭제 파일 하나를 pyarrow 로 열어 /root/ice/rl/out/posdel.json 에 쓰세요. - /root/ice/rl/cost.py 로 두 표의 MERGE 커밋이 쓴 바이트(
added-files-size)를 /root/ice/rl/out/cost.json 에 쓰세요. - /root/ice/rl/read.py 로
lake.rl.mor를 DuckDB 와 pyiceberg 로 읽어 /root/ice/rl/out/read.json 에 쓰세요. - /root/ice/rl/report.md 에
## 삭제 두 방식## 위치 삭제 파일## 쓰기와 읽기의 맞바꿈세 절을 쓰세요.
참고
- 변경 묶음의 열은
op, order_id, customer_id, region, amount, status, order_ts입니다. U 는 상태를 refunded 로 바꾸고, D 는 지우고, I 는 새 주문입니다. - 커밋 요약은
SELECT operation, summary FROM lake.rl.mor.snapshots, 파일 종류는SELECT content, file_path, record_count FROM lake.rl.mor.files(content 0 데이터 · 1 위치 삭제 · 2 동등 삭제)로 봅니다. - 이 실습의 표는 format-version 2 라서 위치 삭제 파일이 Parquet 로 쓰입니다. format-version 3 에서는 같은 정보가 deletion vector(Puffin)로 쓰입니다.
- 흔한 실수: 2단계를 두 표 모두에 돌리는 것(3단계와 순서가 섞이면 비교가 어긋납니다), MERGE 를 두 번 돌리는 것. 되돌리려면 두 표를
DROP TABLE … PURGE한 뒤 1단계부터 하세요. - 공식 문서: Spark Writes — MERGE INTO · Configuration — write.delete.mode · Spec — Row-level Deletes · DuckDB — Iceberg extension
같은 자료, 다른 쓰기 모드
/root/ice/rl/tables.py 를 앱 이름 ice-rl-tables 로 만들어 lake.rl.cow 와 lake.rl.mor 를 만드세요. 둘 다 열 여섯 개·'format-version' = '2' 이고, write.delete.mode·write.update.mode·write.merge.mode 를 cow 는 모두 copy-on-write, mor 은 모두 merge-on-read 로 둡니다. 3월 31개 파일을 각 표에 한 번씩 넣으세요.
쓰기 모드는 표 속성이라 엔진이 아니라 표가 기억합니다. 어느 엔진이 쓰든 같은 방식을 따르게 하려는 것입니다. 채점기는 속성 여섯 개와 첫 커밋의 행 수를 봅니다.
COW 삭제 — 파일을 다시 쓴다
/root/ice/rl/delete_cow.py 를 앱 이름 ice-rl-delete-cow 로 만들어 DELETE FROM lake.rl.cow WHERE status = 'cancelled' 를 돌리세요.
취소 주문이 든 데이터 파일은 취소를 뺀 새 파일로 바뀝니다. 요약의 deleted-data-files·added-data-files 가 그것이고, 삭제 파일은 하나도 없습니다. 채점기는 cow 의 두 번째 커밋 요약과 그때 취소 주문이 남지 않았는지 봅니다.
MOR 삭제 — 지울 행을 적어 둔다
/root/ice/rl/delete_mor.py 를 앱 이름 ice-rl-delete-mor 로 만들어 DELETE FROM lake.rl.mor WHERE status = 'cancelled' 를 돌리세요.
데이터 파일은 그대로 두고, 지울 행의 (파일 경로, 행 번호)를 모은 위치 삭제 파일을 더합니다. 요약에 added-position-delete-files 가 생기고 deleted-data-files 는 없습니다. 채점기는 mor 의 두 번째 커밋 요약과 매니페스트의 content 1 파일을 봅니다.
MERGE — 고치고, 지우고, 더한다
/root/ice/rl/merge.py 를 앱 이름 ice-rl-merge 로 만들어 /data/ice/changes.csv 를 임시 뷰로 읽고, 두 표 각각에 MERGE INTO … ON t.order_id = s.order_id 로 op D 는 DELETE, op U 는 status·amount 를 UPDATE, 표에 없는 op I 는 INSERT 하세요.
원본 한 행에 변경이 둘 걸리면 MERGE 는 실패합니다(이 묶음은 주문마다 변경이 하나뿐입니다). 이미 지워진 취소 주문에 대한 U·D 는 짝이 없어 아무 일도 하지 않습니다. 채점기는 두 표의 내용을 원본과 변경 묶음으로 계산한 기대값과 견주고, mor 에만 위치 삭제 파일이 있는지 봅니다.
위치 삭제 파일을 열어 본다
/root/ice/rl/posdel.py 로 lake.rl.mor 의 지금 스냅샷에서 content 가 1 인 파일 하나를 pyarrow.parquet.read_table 로 열고, /root/ice/rl/out/posdel.json 에 {"delete_file": 경로, "rows": 행 수, "targets": [그 파일이 가리키는 데이터 파일 경로, …]} 를 쓰세요.
위치 삭제 파일은 file_path·pos 두 열짜리 Parquet 입니다. 행 하나가 '이 데이터 파일의 pos 번째 행은 없다' 는 뜻입니다. 채점기는 여러분이 적은 행 수와 대상 목록을 파일에서 다시 읽어 견주고, 대상이 지금 살아 있는 데이터 파일인지 봅니다.
MERGE 한 번에 쓴 바이트
/root/ice/rl/cost.py 로 두 표의 지금 스냅샷(= MERGE 커밋) 요약에서 added-files-size 를 읽어 /root/ice/rl/out/cost.json 에 {"cow_added_bytes": 정수, "mor_added_bytes": 정수} 로 쓰세요.
COW 는 변경이 닿은 데이터 파일을 모두 다시 쓰고, MOR 은 새·바뀐 행과 삭제 파일만 씁니다. 같은 변경 1,400건에 쓴 바이트 차이가 곧 쓰기 증폭입니다. 채점기는 두 값을 요약과 견주고 COW 쪽이 더 큰지 봅니다.
다른 엔진도 삭제를 반영해 읽는가
/root/ice/rl/read.py 로 lake.rl.mor 의 지금 metadata 경로를 iceberg_scan() 에 넣어 DuckDB 로 행 수와 sum(amount) 를, pyiceberg 로 행 수를 세어 /root/ice/rl/out/read.json 에 {"duckdb_rows", "duckdb_amount", "pyiceberg_rows"} 로 쓰세요.
MOR 표는 읽는 쪽이 삭제 파일을 적용해야 올바른 결과가 나옵니다. 삭제 파일을 모르는 엔진은 지운 행까지 돌려줍니다 — 여러 엔진이 같은 표를 읽는다면 반드시 확인할 것입니다. DuckDB 의 iceberg 확장은 빌드 때 이미지에 넣어 두었습니다(LOAD iceberg). 채점기는 세 값을 기대값과 견줍니다.
어느 표를 어느 모드로
/root/ice/rl/report.md 에 ## 삭제 두 방식 ## 위치 삭제 파일 ## 쓰기와 읽기의 맞바꿈 세 절을 쓰세요. 셋째 절에는 6단계의 두 바이트 값을 숫자로 넣으세요.
여러분 팀의 표 둘을 떠올려 하나는 COW, 하나는 MOR 로 정한다면 무엇을 근거로 하겠습니까. MOR 을 고른다면 삭제 파일이 쌓이는 것을 언제 누가 치울지도 적어 보세요.