LabHub
시작하기
배우기 러닝패스 코스

레이크하우스 표 형식 — Apache Iceberg 를 metadata 로 이해한다

Spark 가 만들고 파이썬이 쓰고 DuckDB 가 읽는다 — 한 표를 세 엔진이

LabHub 에서 이어서 보기

목표

Spark 가 만든 표에 pyiceberg 가 하루치를 더 쓰고, DuckDB 와 pyiceberg 가 그 표를 읽고, 다시 Spark 가 모든 엔진이 쓴 행을 모아 집계한다. 그 사이에 옛 metadata 경로를 붙들고 읽는 '낡은 포인터' 함정과, 한 엔진의 스키마 변경이 다른 엔진에 어떻게 보이는지를 확인한다.

왜 중요한가

표 형식의 가치는 엔진을 고르지 않는다는 데 있다. 배치는 Spark 로, 작은 적재는 파이썬으로, 즉석 분석은 DuckDB 로 해도 모두 같은 metadata 와 같은 Parquet 파일을 본다. 그런데 이 약속에는 조건이 붙는다. 모든 엔진이 같은 카탈로그를 거쳐 '지금' metadata 를 찾아야 하고, 같은 스펙 기능(형식 판·삭제 파일·타입)을 지원해야 한다. metadata.json 경로를 직접 넘겨 읽는 도구는 편하지만 그 경로가 가리키는 순간에 고정된다. 누군가 그 뒤에 커밋하면 여러분은 옛 표를 읽고 있는데 아무 경고도 없다. 타입도 조심해야 한다 — Spark 의 TIMESTAMP 는 Iceberg 의 timestamptz 라서, 시간대 없는 시각을 주는 파이썬 코드는 스키마 검사에 걸린다. 반대로 열 이름 바꾸기처럼 필드 ID 로 풀리는 변경은 모든 엔진에 그대로 보인다.

단계

  1. /root/ice/eng/spark.py(앱 ice-eng-spark)로 lake.eng.ordersdays(order_ts) 로 나누어 만들고 2026-03-01 부터 03-07 까지 일곱 파일을 한 번에 커밋하세요.
  2. /root/ice/eng/py_append.py(pyiceberg)로 2026-03-08 을 같은 표에 추가하세요.
  3. /root/ice/eng/duck.py(DuckDB)로 지금 metadata 를 읽어 지역별 amount 합계를 /root/ice/eng/out/duck_region.json 에 쓰세요.
  4. /root/ice/eng/py_read.py(pyiceberg)로 region = 'seoul' 이고 order_ts >= 2026-03-05 인 행 수를 /root/ice/eng/out/py_count.json 에 쓰세요.
  5. 지금 metadata 경로를 적어 둔 뒤 /root/ice/eng/append.py(앱 ice-eng-append)로 2026-03-09 를 커밋하고, /root/ice/eng/stale.py 로 옛 경로와 새 경로를 각각 DuckDB 로 읽어 /root/ice/eng/out/stale.json 에 쓰세요.
  6. /root/ice/eng/rename.py(앱 ice-eng-rename)로 regionarea 로 바꾸고, /root/ice/eng/columns.py 로 pyiceberg 와 DuckDB 가 보는 열 이름을 /root/ice/eng/out/rename.json 에 쓰세요.
  7. /root/ice/eng/daily.py(앱 ice-eng-daily)로 날마다 주문 수·금액 합계 표 lake.eng.daily(d, orders, amount) 를 만드세요.
  8. /root/ice/eng/report.md## 한 표, 세 엔진 ## 낡은 포인터 ## 이름 바꾸기 세 절을 쓰세요.

참고

Spark 가 표를 만든다

/root/ice/eng/spark.py 를 앱 이름 ice-eng-spark 로 만들어 lake.eng.orders(열 여섯 개, PARTITIONED BY (days(order_ts)), 'format-version' = '2')를 만들고 2026-03-01 부터 03-07 까지 일곱 파일을 한 번의 append() 로 넣으세요.

이 스냅샷의 요약에는 engine-name 이 spark 로 남습니다. 채점기는 첫 커밋의 행 수와 작성 엔진을 봅니다.

파이썬이 같은 표에 쓴다

/root/ice/eng/py_append.py 로 2026-03-08 파일을 pyarrow 로 읽고(amount int32, order_ts 는 UTC 시간대의 timestamp), load_catalog("lake").load_table("eng.orders").append(…) 로 추가하세요.

pyiceberg 는 쓰기 전에 pyarrow 스키마를 표 스키마와 견줍니다. order_ts 가 시간대 없는 timestamp 면 timestamptz 와 맞지 않는다고 거절합니다. 파티션(days) 값은 pyiceberg 가 직접 계산해 매니페스트에 적습니다. 채점기는 두 번째 커밋이 3월 8일 행 수를 더했는지, Spark 가 아닌 엔진이 썼는지 봅니다.

DuckDB 가 읽는다

/root/ice/eng/duck.pyice-loc eng.orders 가 찍는 경로를 iceberg_scan() 에 넣어 지역별 sum(amount) 를 구하고 /root/ice/eng/out/duck_region.json{"지역": 합계, …} 로 쓰세요.

DuckDB 는 카탈로그를 거치지 않고 metadata.json 한 파일에서 시작해 매니페스트를 따라 내려갑니다. Spark 가 쓴 파일과 pyiceberg 가 쓴 파일이 같은 목록에 있으니 둘 다 읽힙니다. 채점기는 3월 1–8일 원본으로 계산한 합계와 견줍니다.

pyiceberg 가 조건으로 읽는다

/root/ice/eng/py_read.pyregion == 'seoul' 그리고 order_ts >= 2026-03-05T00:00:00+00:00 인 행 수를 세어 /root/ice/eng/out/py_count.json{"rows": 정수} 로 쓰세요.

pyiceberg 의 조건은 먼저 매니페스트의 파티션 값(날짜)과 열 통계로 파일을 고르고, 고른 파일 안에서 행을 거릅니다. 채점기는 3월 5–8일 원본으로 계산한 값과 견줍니다.

낡은 포인터 — 옛 경로는 옛 표다

OLD=$(ice-loc eng.orders) 로 지금 경로를 적어 두고, /root/ice/eng/append.py(앱 ice-eng-append, 날짜 인자)로 2026-03-09 를 커밋한 뒤 NEW=$(ice-loc eng.orders) 를 읽으세요. /root/ice/eng/stale.py 가 두 경로를 인자로 받아 각각 DuckDB 로 행 수를 세어 /root/ice/eng/out/stale.json{"old_path", "old_rows", "new_path", "new_rows"} 로 쓰게 하세요.

metadata 파일은 한 번 쓰이면 바뀌지 않습니다. 옛 경로로 읽으면 언제 읽든 그 순간의 표가 나옵니다 — 타임트래블에는 쓸모 있지만, '지금' 을 읽으려던 대시보드라면 조용히 낡은 숫자를 보여 줍니다. 채점기는 두 경로가 metadata 이력에 있는지, 행 수가 그때의 total-records 와 같은지 봅니다.

한 엔진이 바꾼 이름을 다른 엔진이 본다

/root/ice/eng/rename.py(앱 ice-eng-rename)로 ALTER TABLE lake.eng.orders RENAME COLUMN region TO area 를 돌리고, /root/ice/eng/columns.py 로 pyiceberg 의 tbl.schema() 열 이름과 DuckDB 의 iceberg_scan() 결과 열 이름을 /root/ice/eng/out/rename.json{"pyiceberg": [...], "duckdb": [...]} 로 쓰세요.

이름 바꾸기는 metadata 의 스키마만 바꾸고, 파일은 옛 이름(region)을 그대로 들고 있습니다. 필드 ID 로 짝을 맞추는 엔진은 새 이름으로 옛 파일의 값을 읽습니다. 채점기는 두 목록에 area 가 있고 region 이 없는지 봅니다.

Spark 가 모든 엔진의 행을 모은다

/root/ice/eng/daily.py 를 앱 이름 ice-eng-daily 로 만들어 lake.eng.orders 를 날짜(to_date(order_ts))로 묶어 주문 수와 amount 합계를 lake.eng.daily(d, orders, amount) 로 만드세요(CREATE TABLE … AS SELECT).

3월 8일 행은 pyiceberg 가, 나머지는 Spark 가 썼습니다. 읽는 엔진에게는 차이가 없습니다. 날짜 경계는 세션 시간대(UTC)로 잘립니다. 채점기는 3월 1–9일 원본으로 계산한 날마다의 값과 견줍니다.

여러 엔진을 한 표에 붙일 때의 규칙

/root/ice/eng/report.md## 한 표, 세 엔진 ## 낡은 포인터 ## 이름 바꾸기 세 절을 쓰세요. 둘째 절에는 5단계의 old_rowsnew_rows 를 숫자로 넣으세요.

여러분 팀에 새 엔진(예: 사내 BI 도구)을 붙인다면 무엇을 먼저 확인하겠습니까 — 카탈로그를 거치는가, 어떤 형식 판·삭제 파일을 읽는가, timestamptz 를 어떻게 다루는가.