レイクハウスのテーブル形式 — Apache Iceberg をメタデータで理解する
パーティションは列ではなく規則だ — 隠れたパーティショニングとパーティション進化
한국어 원문으로 표시합니다.
한 줄 요약
Iceberg 는 파티션 값을 사람이 만든 열이 아니라 원천 열에 건 변환 규칙(예: days(order_ts))으로 계산해 매니페스트에 적고, 읽는 쪽은 원천 열 조건만으로 파일을 건너뛴다. 규칙은 스펙 ID 를 달고 쌓이므로 도중에 바꿔도 옛 파일은 옛 규칙 그대로 읽힌다.
왜 Hive 식 파티션이 문제였나
Partitioning 문서가 드는 예가 전형적이다. 로그 표를 날짜로 나누려면 Hive 에서는 event_date 라는 열을 따로 만들고, 쓰는 쪽이 event_time 에서 그 값을 계산해 넣어야 한다. 여기서 문제가 줄줄이 생긴다.
- 쓰는 쪽이 틀리면 조용히 틀린다.
2018-12-01대신20181201로 쓰거나, 다른 원천 열(처리 시각)을 쓰거나, 시간대를 잘못 계산해도 오류가 나지 않는다. 결과만 틀린다. - 읽는 쪽이 알아야 빠르다.
event_time조건만 걸면 Hive 는 두 열의 관계를 몰라 모든 파일을 읽는다. 사람들은event_date조건을 덧붙이는 법을 외워야 한다. - 바꿀 수 없다. 쿼리가 파티션 열에 묶여 있어서 날 단위를 시간 단위로 바꾸려면 새 표를 만들고 쿼리를 다 고쳐야 한다.
어떻게 동작하나 — 변환과 파티션 스펙
Iceberg 의 파티션 스펙은 필드마다 원천 열 ID, 파티션 필드 ID, 변환, 이름을 가진다. 엔진은 행을 쓸 때 변환을 계산해 그 파일의 파티션 값으로 매니페스트에 적는다. 한 파일의 행은 모두 같은 파티션 값을 가진다. 변환 목록은 다음과 같다.
| 변환 | 결과 | 쓰임 |
|---|---|---|
| identity | 값 그대로 | 가짓수가 적은 범주 열 |
| year · month · day · hour | 1970 년부터 센 해·달·날·시간 | 시각 열 |
| bucket[N] | 해시를 N 으로 나눈 나머지 | 가짓수가 많은 키(고객 번호) |
| truncate[W] | 폭 W 로 자른 값 | 숫자 구간, 문자열 앞부분 |
| void | 늘 null | 판 1 에서 필드를 없앨 때 |
bucket은 32비트 Murmur3(x86, 시드 0) 해시의 부호 비트를 버리고 N 으로 나눈다. 스펙이 해시를 정해 두었기 때문에 Spark 가 쓰고 파이썬이 읽어도 같은 값이 같은 칸에 간다.
읽는 쪽은 order_ts >= X 같은 원천 열 조건만 쓴다. 스캔 계획이 그 조건을 파티션 조건(order_ts_day >= day(X))으로 바꿔 매니페스트의 파티션 값과 견준다. 이 변환은 '포함하는' 쪽으로 계산되므로 조건에 맞는 행이 있을 수 있는 파일은 절대 빠지지 않는다. 사용자는 파티션이 어떻게 나뉘었는지 몰라도 된다 — 그래서 '숨은' 파티셔닝이다.
파티션 진화 — 옛 파일은 그대로
자료가 늘어 달 단위가 너무 굵어졌다고 하자. Evolution 문서에 따르면 스펙을 바꿔도 옛 자료는 옛 스펙 그대로 남고 새 자료만 새 배치로 쓰인다. 스펙은 목록에 쌓이고 매니페스트마다 자기가 쓴 스펙 ID 를 기억한다. 계획할 때는 스펙마다 따로 조건을 바꿔 파일을 고르는데, 문서는 이것을 split planning 이라 부른다. 그리고 파티션 진화는 metadata 작업이라 파일을 서둘러 다시 쓰지 않는다고 분명히 적는다.
Spark 에서는 ALTER TABLE 로 필드를 더하고(ADD PARTITION FIELD), 빼고(DROP PARTITION FIELD), 바꾼다(REPLACE PARTITION FIELD … WITH …).
ALTER TABLE lake.demo.events REPLACE PARTITION FIELD months(event_ts) WITH days(event_ts);
현장에서 만나는 모습
'파티션을 바꿨으니 옛 자료도 다시 써야 한다'. 가장 흔한 오해다. 옛 달 단위 파일은 그대로 두어도 정확히 읽힌다. 다시 쓰는 것은 선택이고, 이유가 있어야 한다 — 옛 기간을 하루 단위로 자주 조회해서 읽는 양을 줄이고 싶을 때처럼. 그때도 압축(rewrite_data_files) 같은 별도 작업으로 한다.
하루 조건인데 한 달 치를 읽는다. 달 단위 파일에 하루 조건을 걸면 파티션으로는 그 달 파일을 통째로 골라야 한다. 파일 안의 열 통계(하한·상한)가 도움이 되지만 파일이 한 달을 고루 담고 있으면 소용없다. 계획이 여는 파일의 행 수와 실제로 걸리는 행 수의 차이가 그 비용이다.
고객 번호로 나눴더니 파티션이 수만 개. identity 로 가짓수가 많은 열을 나누면 작은 파일이 폭발한다. bucket 으로 칸 수를 정해 두는 것이 맞다.
실무에서 진짜 중요한 것
- 파티션은 규칙이다. 원천 열과 변환만 적고, 파티션 값은 엔진이 계산한다.
- 질의는 원천 열로만 건다. 파티션 열을 따로 알 필요가 없다.
- 스펙은 쌓이고 파일은 자기 스펙을 기억한다. 진화 뒤 옛 파일을 다시 쓸 필요는 없다.
- 가짓수가 많은 열은 bucket, 시각은 year·month·day·hour.
다음 실습에서 할 것
3월 한 달을 months(order_ts) 로 나눈 표에 넣고 pyiceberg 로 하루 조건의 계획을 세워 몇 파일·몇 행을 여는지 잰다. 파티션을 days(order_ts) 로 바꾼 뒤 4월을 넣고, 3월 파일은 스펙 0 그대로·4월 파일만 스펙 1 인 것을 확인한다. 3월 하루와 4월 하루 조건이 읽는 행 수를 견주고, 고객 표를 bucket(4, customer_id) 로 나눠 칸이 스펙대로 계산됐는지 본다.