Trino + Iceberg 는 파티션 문제를 어떻게 해결하는가
Hive/Impala 에서 파티션 컬럼 누락 시 풀스캔이 발생하는 이유와, Trino + Iceberg 의 hidden partitioning·메타데이터 기반 프루닝이 이 문제를 구조적으로 제거하는 방법을 정리합니다.
분석 쿼리 하나를 날렸더니 클러스터가 멈춰버린 경험, 혹시 있으신가요? WHERE 절에 파티션 컬럼을 빠뜨린 것뿐인데, 수백 GB 풀스캔이 시작되는 상황 말이에요. Trino 에서 Parquet 포맷의 Iceberg 테이블을 만들고, 같은 데이터를 Impala 나 Hive 에서도 조회하는 구성은 실무에서 흔히 볼 수 있습니다. 문제는 Hive/Impala 에서 파티션 컬럼을 빠뜨리는 순간 구조적으로 풀스캔을 피할 방법이 없다는 점이죠. 이 글에서는 그 이유를 구체적으로 살펴보고, Trino + Iceberg 가 어떻게 이 문제를 근본적으로 없애버리는지 단계별로 설명합니다.
이 글에서 배우는 것
- Hive/Impala 의 디렉터리 기반 파티셔닝이 왜 풀스캔을 피할 수 없는지
- Iceberg 의 메타데이터 3계층 구조와 manifest 기반 프루닝이 어떻게 다른지
- Hidden Partitioning 으로 파티션 컬럼을 스키마에서 숨기는 방법
- Trino 의 3단계 파일 프루닝 메커니즘(Manifest → Row Group → Dynamic Filtering)
- Trino + Iceberg 에서도 성능이 나빠지는 상황과 대응 방법
1. 문제: 파티션 컬럼을 빠뜨리면 클러스터가 멈춘다
Hive 와 Impala 에서 파티션 테이블을 만드는 전형적인 패턴입니다.
CREATE TABLE events (
event_id BIGINT,
user_id BIGINT,
event_type STRING,
event_time TIMESTAMP,
city STRING,
payload STRING
)
PARTITIONED BY (dt STRING)
STORED AS PARQUET;이 테이블에 1년치 데이터가 쌓여 있다고 가정합니다. 약 500GB, 365개 파티션.
-- 정상: 파티션 프루닝 동작, 하루치 약 1.4GB 만 스캔
SELECT * FROM events WHERE dt = '2026-04-12' AND user_id = 12345;
-- 문제: dt 파티션 컬럼 누락 → 365개 파티션 전체 스캔 (500GB)
SELECT * FROM events WHERE user_id = 12345;두 번째 쿼리는 user_id 조건만 있고 dt 가 없습니다. Hive/Impala 는 어떤 파티션에 user_id = 12345 데이터가 있는지 알 방법이 없으므로, 모든 파티션의 모든 파일을 열어서 확인해야 합니다.
2. 왜 Hive/Impala 는 이 문제를 피할 수 없는가
디렉터리 기반 파티셔닝
Hive/Impala 의 파티션은 물리 디렉터리입니다. 마치 날짜별로 폴더를 나눠놓은 것과 같아서, 엔진은 폴더 이름만 보고 어느 폴더를 열지 결정합니다.
/warehouse/events/dt=2026-04-01/
/warehouse/events/dt=2026-04-02/
/warehouse/events/dt=2026-04-03/
...
/warehouse/events/dt=2026-04-12/
├── 000000_0.parquet
├── 000001_0.parquet
└── 000002_0.parquetHive Metastore(HMS)는 파티션 디렉터리 목록을 관리하지만, 각 파일 안에 어떤 데이터가 들어있는지(min/max 통계)는 모릅니다. 쿼리 플래너가 파티션을 줄이려면 WHERE 절에 파티션 컬럼(dt)이 명시적으로 있어야 합니다.
Strict Mode 로 막을 수는 있지만
Hive 는 hive.mapred.mode=strict, Impala 는 쿼리 옵션으로 파티션 컬럼이 없는 쿼리를 거부할 수 있습니다. 하지만 이것은 "실수를 차단"하는 것이지, 파티션 컬럼 없이도 효율적으로 조회하는 것은 불가능합니다.
한 문장으로: Hive/Impala 파티션은 파티션 컬럼을 WHERE 에 명시해야만 동작하는 구조라, 빠뜨리는 순간 풀스캔이 구조적으로 피할 수 없습니다.
3. 파티션 개수 제한: Hive/Impala vs Trino+Iceberg
파티션을 잘게 나누면 쿼리 성능이 좋아지지만, Hive/Impala 에서는 파티션 수 자체가 병목이 됩니다. 파티션 정보가 모두 외부 메타스토어(HMS)에 등록되어야 하기 때문이에요.
Hive/Impala 의 파티션 제한
| 항목 | Hive | Impala |
|---|---|---|
| 파티션 컬럼 수 | 제한 없음 (실무상 3~4개 권장) | 동일 |
| 최대 파티션 수 | HMS 성능 한계로 수만~수십만 개에서 심각한 저하 | Catalogd 메모리에 파티션 메타를 전부 올리므로 수십만 개 넘으면 OOM·갱신 지연 |
| 동적 파티션 생성 제한 | hive.exec.max.dynamic.partitions 기본값 1000 | MAX_DYNAMIC_PARTITIONS 쿼리 옵션으로 제한 |
| 플래닝 시 파티션 로드 | HMS 에 RPC → 파티션 수가 많으면 쿼리 플래닝만 수 분 | HDFS NameNode + Catalogd 동기화 비용 추가 |
파티션 컬럼을 2~3개 조합하면 카디널리티가 폭발합니다. 예를 들어 dt × region × device_type = 365 × 50 × 10 = 182,500 파티션/년. Hive/Impala 에서는 이런 설계를 할 수 없습니다.
Trino + Iceberg 의 파티션 제한
| 항목 | Trino + Iceberg |
|---|---|
| 파티션 컬럼 수 | Iceberg spec 상 제한 없음 |
| 최대 파티션 수 | 사실상 무제한. 파티션이 HMS 레코드가 아니라 manifest 내부 메타데이터 |
| 동적 파티션 생성 제한 | 없음 |
| 플래닝 시 파티션 로드 | Manifest 파일만 읽으면 됨. HMS RPC 불필요 |
Iceberg 는 파티션 정보를 HMS 에 등록하지 않고 manifest 파일에 저장합니다. 따라서 파티션 수가 늘어나도 HMS 에 RPC 를 날리지 않으며, 100만 개 파티션이 있어도 manifest 프루닝으로 필요한 파일만 빠르게 필터링합니다.
비교 요약
| 항목 | Hive/Impala | Trino + Iceberg |
|---|---|---|
| 파티션 = | HMS 레코드 + HDFS 디렉터리 | Manifest 내부 메타데이터 |
| 파티션 수 실무 한계 | 수만~수십만 (이후 성능 저하) | 사실상 무제한 |
| 파티션 추가 비용 | HMS ALTER TABLE ADD PARTITION (RPC) | 데이터 쓰기 시 자동 반영 (manifest commit) |
| 파티션 수 × 플래닝 시간 | 선형 증가 | 거의 일정 |
| 동적 파티션 생성 제한 | 기본 1000개/쿼리 | 없음 |
실무 시사점: Hive/Impala 에서는 시간 파티션을 일 단위가 한계이지만, Trino + Iceberg 에서는 hours(event_time) 파티션도 부담 없고, days(event_time), bucket(32, user_id) 같은 복합 파티션도 자유롭습니다.
4. Iceberg 가 다른 점: 메타데이터 기반 아키텍처
Iceberg 는 테이블 메타데이터를 3계층 구조로 관리합니다. 쉽게 말하면 "이 테이블의 어떤 파일에, 어떤 범위의 데이터가 있는지"를 테이블 포맷 자체가 알고 있는 구조입니다.
핵심은 manifest file 입니다. 각 데이터 파일의 파티션 값과 컬럼별 min/max 통계가 manifest 안에 들어있으므로, 데이터 파일을 열지 않고도 "이 파일이 쿼리에 필요한지"를 판단할 수 있습니다.
Hive/Impala 와의 근본적인 차이:
| 항목 | Hive/Impala | Iceberg |
|---|---|---|
| 메타데이터 위치 | HMS (외부 RPC) | Manifest 파일 (스토리지 직접 읽기) |
| 파일 수준 통계 | 없음 (파티션 단위만) | min/max, null count, row count 등 |
| 파일 프루닝 | 불가능 | Manifest 기반으로 가능 |
5. Hidden Partitioning — 사용자가 파티션을 몰라도 되는 구조
이것이 Iceberg 의 가장 큰 차별점입니다. Hive 방식에서는 INSERT 할 때도, SELECT 할 때도 파티션 컬럼을 여러분이 직접 챙겨야 하죠. Iceberg 는 이 부담을 엔진이 떠안습니다.
Hive 방식
-- 테이블 생성 시 파티션 컬럼을 스키마에 명시
CREATE TABLE events (...) PARTITIONED BY (dt STRING);
-- INSERT 시 파티션 값을 명시적으로 지정
INSERT INTO events PARTITION (dt = '2026-04-12') VALUES (...);
-- SELECT 시에도 파티션 컬럼을 WHERE 에 명시해야 프루닝
SELECT * FROM events WHERE dt = '2026-04-12';사용자가 dt 라는 파티션 컬럼의 존재를 알고, INSERT/SELECT 모두에서 명시해야 합니다.
Iceberg 방식: Partition Transform
-- 파티션 컬럼이 스키마에 노출되지 않음
CREATE TABLE iceberg_catalog.db.events (
event_id BIGINT,
user_id BIGINT,
event_type VARCHAR,
event_time TIMESTAMP(6) WITH TIME ZONE,
city VARCHAR,
payload VARCHAR
)
WITH (
format = 'PARQUET',
partitioning = ARRAY['days(event_time)']
);event_time 컬럼에서 자동으로 일 단위 파티션이 생성됩니다. 별도의 dt 파티션 컬럼이 스키마에 없습니다.
-- 사용자는 그냥 event_time 으로 조건을 걸면 됨
SELECT * FROM events
WHERE event_time >= TIMESTAMP '2026-04-12 00:00:00 UTC'
AND event_time < TIMESTAMP '2026-04-13 00:00:00 UTC';Trino 가 days(event_time) partition transform 을 알고 있으므로, 자동으로 해당 일자의 파티션만 스캔합니다. 파티션 컬럼이 스키마에 없으니 빠뜨릴 일 자체가 없습니다.
지원되는 Partition Transform:
| Transform | 설명 | 예시 |
|---|---|---|
years(col) | 연 단위 | years(event_time) |
months(col) | 월 단위 | months(event_time) |
days(col) | 일 단위 | days(event_time) |
hours(col) | 시간 단위 | hours(event_time) |
bucket(N, col) | 해시 버킷 N개 | bucket(32, user_id) |
truncate(W, col) | 문자열/숫자를 W 폭으로 잘라서 파티션 | truncate(10, city) |
6. Trino + Iceberg 에서 파티션 컬럼이 꼭 필요한가?
결론: 필수는 아니다. 하지만 없으면 달라지는 것이 있다
파티션 없는 Iceberg 테이블도 만들 수 있습니다. 하지만 파티션이 있을 때와 없을 때의 동작은 꽤 다릅니다. 직접 비교해 봅시다.
-- 파티션 없는 Iceberg 테이블
CREATE TABLE iceberg_catalog.db.events_no_partition (
event_id BIGINT,
user_id BIGINT,
event_time TIMESTAMP(6) WITH TIME ZONE,
city VARCHAR
)
WITH (format = 'PARQUET');이 테이블에 WHERE event_time = TIMESTAMP '2026-04-12 10:00:00 UTC' 로 조회하면:
- Manifest 에 파티션 값이 없음 → 모든 manifest entry 를 확인
- 하지만 Parquet footer 의 column statistics(min/max)로 row group 단위 스킵은 가능
- 결과: 파일은 전부 열지만, row group 단위로 일부 스킵 → Hive 풀스캔보다는 낫지만, 파티션이 있을 때보다 훨씬 느림
| 구성 | 파일 프루닝 | Row Group 프루닝 | 전체 스캔 범위 |
|---|---|---|---|
| 파티션 없음 | 불가 | Parquet min/max 로 일부 스킵 | 거의 전체 |
| 파티션 있음 (hidden) | Manifest 기반으로 대부분 스킵 | 추가로 row group 스킵 | 해당 파티션 파일만 |
파티션 컬럼이 아닌 컬럼을 WHERE 에 넣으면?
파티션은 days(event_time) 으로 설정했지만, 쿼리는 WHERE user_id = 12345 인 경우:
- Manifest 프루닝:
user_id는 파티션 transform 과 무관 → 파일을 줄일 수 없음 → 모든 데이터 파일이 후보 - Parquet min/max 프루닝: 각 파일의
user_id컬럼 min/max 확인 → 범위 밖이면 스킵 - 실제 읽기: min/max 범위 안에 드는 파일만 열어서 row group 단위로 한 번 더 필터
결과: 파티션 프루닝 효과는 0, Parquet 통계에만 의존. 데이터가 정렬되어 있지 않으면 사실상 풀스캔에 가까움.
대응법:
-- 방법 1: user_id 에 대한 bucket 파티션 추가
ALTER TABLE events SET PROPERTIES
partitioning = ARRAY['days(event_time)', 'bucket(64, user_id)'];
-- 방법 2: 정렬 키 설정으로 min/max 통계 효과 극대화
ALTER TABLE events SET PROPERTIES
sorted_by = ARRAY['user_id'];
ALTER TABLE events EXECUTE optimize;존재하지 않는 컬럼을 WHERE 에 넣으면?
SELECT * FROM events WHERE non_existent_column = 'value';
-- 결과: COLUMN_NOT_FOUND 에러로 즉시 쿼리 실패Iceberg 스키마에 없는 컬럼이므로 플래닝 단계에서 거부됩니다. 이 동작은 Hive/Impala 도 동일합니다.
단, Schema Evolution 으로 나중에 추가된 컬럼의 경우, 추가 이전의 데이터 파일에는 해당 컬럼이 없으므로 NULL 로 처리됩니다. 쿼리 자체는 정상 실행됩니다.
7. Trino 의 파일 프루닝 메커니즘
Trino + Iceberg 는 3단계 깔때기로 불필요한 데이터를 걸러냅니다. 각 단계를 거칠수록 실제로 읽는 데이터가 극적으로 줄어드는 구조입니다.
각 단계가 순차적으로 데이터를 줄이므로, 최종적으로 읽는 데이터량이 극적으로 감소합니다.
8. 실전 비교: 같은 데이터, 같은 쿼리, 다른 결과
시나리오 설정
- 테이블: 1년치 이벤트 로그, 약 500GB
- 파티션: 일 단위 (Hive:
dt컬럼, Iceberg:days(event_time)) - Parquet 포맷
쿼리 1: 파티션 컬럼 명시
-- Hive/Impala
SELECT * FROM events WHERE dt = '2026-04-12' AND user_id = 12345;
-- Trino + Iceberg
SELECT * FROM events
WHERE event_time >= TIMESTAMP '2026-04-12 00:00:00 UTC'
AND event_time < TIMESTAMP '2026-04-13 00:00:00 UTC'
AND user_id = 12345;| 엔진 | 스캔 범위 | 비고 |
|---|---|---|
| Hive/Impala | ~1.4GB (1일분) | 파티션 프루닝 정상 동작 |
| Trino + Iceberg | ~1.4GB (1일분) | Hidden partition 프루닝 동작 |
모두 빠릅니다.
쿼리 2: 파티션 컬럼 누락 — 시간 범위만
-- Hive/Impala: dt 를 안 쓰고 event_time 으로만 조건
SELECT * FROM events WHERE event_time >= '2026-04-12' AND event_time < '2026-04-13';
-- Trino + Iceberg: 동일한 조건
SELECT * FROM events
WHERE event_time >= TIMESTAMP '2026-04-12 00:00:00 UTC'
AND event_time < TIMESTAMP '2026-04-13 00:00:00 UTC';| 엔진 | 스캔 범위 | 비고 |
|---|---|---|
| Hive/Impala | ~500GB (전체) | event_time 은 파티션 컬럼이 아니므로 풀스캔 |
| Trino + Iceberg | ~1.4GB (1일분) | days(event_time) transform 으로 자동 프루닝 |
350배 차이. Hive/Impala 에서는 풀스캔, Trino + Iceberg 에서는 하루치만 읽습니다.
쿼리 3: 파티션과 무관한 컬럼
SELECT * FROM events WHERE user_id = 12345;| 엔진 | 스캔 범위 | 비고 |
|---|---|---|
| Hive/Impala | ~500GB (전체) | 풀스캔 |
Trino + Iceberg (파티션: days(event_time) 만) | ~500GB (전체) | user_id 는 파티션과 무관, Parquet min/max 에만 의존 |
Trino + Iceberg (파티션: days(event_time) + bucket(64, user_id)) | ~7.8GB (1/64) | bucket 파티션 프루닝 |
파티션 설계가 쿼리 패턴과 맞아야 효과가 있습니다.
쿼리 4: 파티션 없는 Iceberg 테이블
SELECT * FROM events_no_partition
WHERE event_time >= TIMESTAMP '2026-04-12 00:00:00 UTC'
AND event_time < TIMESTAMP '2026-04-13 00:00:00 UTC';| 엔진 | 스캔 범위 | 비고 |
|---|---|---|
| Trino + Iceberg (파티션 없음) | 거의 전체 | Parquet min/max 로 일부 row group 스킵, 파일 프루닝 불가 |
| Trino + Iceberg (파티션 있음) | ~1.4GB | 파티션 프루닝으로 대부분 스킵 |
파티션 없는 Iceberg 도 Hive 풀스캔보다는 약간 낫지만, 파티션이 있을 때와는 10~50배 성능 차이가 납니다.
9. Iceberg 테이블 생성과 파티션 설정
기본 생성
CREATE TABLE iceberg_catalog.db.events (
event_id BIGINT,
user_id BIGINT,
event_type VARCHAR,
event_time TIMESTAMP(6) WITH TIME ZONE,
city VARCHAR,
payload VARCHAR
)
WITH (
format = 'PARQUET',
partitioning = ARRAY['days(event_time)']
);복합 파티션
CREATE TABLE iceberg_catalog.db.events (
event_id BIGINT,
user_id BIGINT,
event_type VARCHAR,
event_time TIMESTAMP(6) WITH TIME ZONE,
city VARCHAR
)
WITH (
format = 'PARQUET',
partitioning = ARRAY['days(event_time)', 'bucket(32, user_id)']
);Partition Evolution — 기존 데이터 재작성 없이 파티션 전략 변경
Iceberg 고유 기능입니다. Hive/Impala 에서는 불가능합니다. 운영 중인 테이블의 파티션 전략을 바꾸려면 Hive 에서는 테이블을 통째로 다시 만들어야 하지만, Iceberg 는 ALTER TABLE 한 줄로 앞으로 쓰이는 데이터에만 새 전략을 적용합니다.
-- 기존: 일 단위 파티션
-- 변경: 시간 단위 파티션으로 전환
ALTER TABLE events SET PROPERTIES
partitioning = ARRAY['hours(event_time)'];이후 새로 쓰이는 데이터만 시간 단위 파티션을 적용받고, 기존 데이터는 일 단위 파티션 그대로 유지됩니다. Iceberg 의 manifest 가 파티션 spec 버전을 추적하므로 혼재된 상태에서도 프루닝이 정상 동작합니다.
10. Trino + Iceberg 에서도 성능이 나빠지는 경우
Trino + Iceberg 가 만능은 아닙니다. 다음 상황에서는 성능이 크게 저하됩니다.
10.1 Small File Problem
Iceberg 는 파티션 수 제한이 없는 대신, INSERT 가 잦으면 파티션당 수백~수천 개의 작은 파일이 쌓입니다. 각 파일마다 manifest entry 가 존재하고, Parquet footer 를 파일마다 열어야 하므로 파일 수 × I/O 오버헤드가 발생합니다.
증상: 데이터 양은 적은데 쿼리 플래닝이 수십 초 걸림.
-- 작은 파일 병합 (컴팩션)
ALTER TABLE events EXECUTE optimize;
-- 또는 WHERE 조건으로 특정 파티션만 컴팩션
ALTER TABLE events EXECUTE optimize
WHERE event_time >= TIMESTAMP '2026-04-01 00:00:00 UTC'
AND event_time < TIMESTAMP '2026-05-01 00:00:00 UTC';10.2 Manifest 비대화
파일 수가 수십만 개를 넘으면 manifest 자체를 읽는 시간이 늘어납니다.
-- 파일 수 확인
SELECT count(*) AS file_count,
sum(file_size_in_bytes) / (1024*1024*1024) AS total_gb
FROM "events$files";10.3 Predicate Pushdown 이 안 되는 WHERE 조건
WHERE 절에 컬럼을 함수로 감싸면 predicate pushdown 이 깨집니다.
| 패턴 | Pushdown | 설명 |
|---|---|---|
WHERE event_time = TIMESTAMP '...' | O | 직접 비교 |
WHERE CAST(event_time AS DATE) = DATE '...' | X | 함수 적용 → 풀스캔 |
WHERE year(event_time) = 2026 | X | 함수 래핑 → 풀스캔 |
WHERE LOWER(city) = 'seoul' | X | 문자열 함수 → 풀스캔 |
WHERE city LIKE '%eoul' | X | 앞쪽 와일드카드 → min/max 활용 불가 |
WHERE city IN ('Seoul', 'Busan') | O | IN 리스트 → pushdown 가능 |
WHERE event_time BETWEEN ... AND ... | O | 범위 비교 → pushdown + partition 프루닝 |
핵심 규칙: 컬럼은 항상 왼쪽에 단독으로, 변환은 오른쪽(상수 쪽)에서 처리.
-- BAD: pushdown 안 됨
WHERE CAST(event_time AS DATE) = DATE '2026-04-12'
-- GOOD: pushdown 됨
WHERE event_time >= TIMESTAMP '2026-04-12 00:00:00 UTC'
AND event_time < TIMESTAMP '2026-04-13 00:00:00 UTC'10.4 정렬되지 않은 데이터
Parquet row group 의 min/max 통계는 데이터가 정렬되어 있을 때 효과적입니다.
- 정렬된 경우: row group 1 은
user_id 1~1000, row group 2 는1001~2000→WHERE user_id = 500이면 row group 1만 읽음 - 비정렬 경우: 모든 row group 의
user_id범위가1~100000으로 겹침 → 전부 읽어야 함
-- 정렬 키 설정
ALTER TABLE events SET PROPERTIES
sorted_by = ARRAY['user_id'];
-- 기존 데이터 재정렬
ALTER TABLE events EXECUTE optimize;10.5 JOIN 시 Dynamic Filtering 미작동
Dynamic Filtering 이 작동하지 않는 조건:
- Build side 결과가 너무 클 때 (수십만 건 이상) → dynamic filter 생성 포기
LEFT JOIN/FULL OUTER JOIN→ probe side 를 줄일 수 없음- JOIN 키가 파티션 컬럼과 무관할 때
10.6 과도한 스냅샷 누적
Iceberg 는 매 쓰기마다 새 스냅샷을 생성합니다. 시간이 지나면 수천 개의 스냅샷이 쌓이고, metadata file 크기 증가로 플래닝이 느려집니다.
-- 오래된 스냅샷 만료
ALTER TABLE events EXECUTE expire_snapshots(retention_threshold => '7d');
-- 고아 파일 제거
ALTER TABLE events EXECUTE remove_orphan_files(retention_threshold => '7d');10.7 Worker 메모리 부족
파티션/파일 수와 무관하게, 대량 데이터를 집계·조인하면 Worker 메모리가 초과됩니다. Trino 는 디스크로 spill 가능하지만 성능이 크게 저하됩니다.
대응:
query.max-memory-per-node조정SELECT *지양, 필요한 컬럼만 지정- 대량 집계는 단계별로 분리
11. 성능 확보를 위한 추가 팁
| 항목 | 설정/명령 | 효과 |
|---|---|---|
| 통계 수집 | ANALYZE events | Trino 플래너가 더 나은 실행 계획 생성 |
| Row group 크기 | write.parquet.row-group-size-bytes | 적절한 크기로 row group 프루닝 효과 조정 |
| 정렬 키 | sorted_by = ARRAY['col'] | min/max 통계 범위를 좁혀서 프루닝 효과 극대화 |
| 컴팩션 | ALTER TABLE ... EXECUTE optimize | 작은 파일 병합, 정렬 적용 |
| 스냅샷 관리 | EXECUTE expire_snapshots | metadata 크기 감소 |
| 고아 파일 제거 | EXECUTE remove_orphan_files | 불필요한 스토리지 회수 |
12. 성능 저하 진단 체크리스트
문제가 발생했을 때 아래 순서로 확인하세요.
| 순서 | 확인 항목 | 확인 방법 |
|---|---|---|
| 1 | WHERE 절에 함수가 감싸져 있지 않은가 | 쿼리 리뷰 |
| 2 | 파일/파티션 프루닝이 동작하는가 | EXPLAIN (TYPE DISTRIBUTED) |
| 3 | 작은 파일이 과다하지 않은가 | SELECT count(*), sum(file_size_in_bytes) FROM "table$files" |
| 4 | 데이터가 정렬되어 있는가 | sorted_by 속성 확인 |
| 5 | 스냅샷이 과도하게 쌓여 있지 않은가 | SELECT count(*) FROM "table$snapshots" |
| 6 | Dynamic Filtering 이 동작하는가 | EXPLAIN 에서 dynamicFilter 존재 여부 |
| 7 | Worker 메모리가 충분한가 | Trino Web UI → Query Detail → Stage 별 메모리 |
13. 정리
| 항목 | Hive/Impala | Trino + Iceberg |
|---|---|---|
| 파티션 구조 | 디렉터리 기반, HMS 관리 | Manifest 기반, HMS 무관 |
| 파티션 컬럼 누락 시 | 풀스캔 | Hidden partitioning 으로 자동 프루닝 |
| 파티션 수 한계 | 수만~수십만 | 사실상 무제한 |
| 파일 수준 프루닝 | 불가능 | Manifest 의 min/max 통계로 가능 |
| 파티션 전략 변경 | 테이블 재생성 필요 | Partition Evolution 으로 무중단 변경 |
| 동적 파티션 생성 제한 | 있음 (기본 1000) | 없음 |
Hive/Impala 의 파티션 문제는 "사용자의 실수"가 아니라 디렉터리 기반 파티셔닝의 구조적 한계입니다. Iceberg 는 메타데이터 계층을 테이블 포맷 안으로 가져옴으로써 이 한계를 근본적으로 제거했고, Trino 는 이 메타데이터를 최대한 활용하여 불필요한 I/O 를 줄입니다.
다만 Trino + Iceberg 도 만능은 아닙니다. Small file problem, predicate pushdown 이 깨지는 SQL 패턴, 비정렬 데이터, 스냅샷 누적 등은 여전히 성능을 저하시킵니다. EXPLAIN 으로 실행 계획을 확인하고, 정기적인 컴팩션과 스냅샷 관리를 습관화하는 것이 핵심입니다.
마치며 — 핵심 요약
- Hive/Impala 의 파티션은 HDFS 디렉터리 구조에 묶여 있어, 파티션 컬럼을 WHERE 에 명시하지 않으면 구조적으로 풀스캔이 발생합니다.
- Iceberg 는 파티션 정보를 manifest 파일에 저장하므로 HMS RPC 없이 빠르게 프루닝하며, 파티션 수도 사실상 무제한입니다.
- Hidden Partitioning 덕분에 파티션 컬럼이 스키마에서 사라지고, 엔진이 transform 을 통해 자동으로 파티션을 결정합니다 — 사용자가 빠뜨릴 컬럼 자체가 없습니다.
- Trino 는 Manifest → Row Group → Dynamic Filtering 3단계 깔때기로 불필요한 I/O 를 걸러내며, 쿼리 패턴에 맞는 파티션 설계가 뒷받침돼야 효과가 극대화됩니다.
- Small file problem, pushdown 이 깨지는 함수 래핑, 스냅샷 누적은 Trino + Iceberg 에서도 여전히 성능을 갉아먹습니다.
- 정기적인
optimize(컴팩션)와expire_snapshots를 운영 루틴으로 만들어두면 대부분의 성능 저하를 예방할 수 있습니다.
아키텍처가 올바르게 설계된 Iceberg 테이블은 "파티션 컬럼을 실수로 빠뜨려 클러스터가 멈추는 일"을 구조적으로 없앨 수 있습니다. 여러분의 데이터 플랫폼에서도 이 차이를 직접 확인해 보시길 바랍니다.
이 글은 Trino 443 + Iceberg spec v2 기준으로 작성되었습니다. 실제 환경에서의 파티션 설계나 마이그레이션에 대해 도움이 필요하시면 언제든 문의해 주세요.
— Data Dynamics 엔지니어링 팀