Trino 쿼리 성능 튜닝: EXPLAIN ANALYZE, CBO, 파티셔닝 전략
느린 쿼리를 마주쳤을 때 어디서 시작하는가
Trino 쿼리가 기대보다 느리거나 OOM(Out of Memory)으로 실패하면 막막하다. 어느 단계에서 병목이 생겼는지, 조인 순서가 잘못된 건지, 파일을 너무 많이 읽는 건지 알기 어렵다.
Trino는 두 가지 진단 도구를 제공한다.
- EXPLAIN: 쿼리를 실제로 실행하지 않고 실행 계획(plan)과 비용 추정치를 보여준다.
- EXPLAIN ANALYZE: 쿼리를 실제로 실행하고, 각 단계의 실측 통계(행 수, CPU 시간, 소요 시간)를 계획 위에 겹쳐 보여준다.
EXPLAIN 읽기
EXPLAIN
SELECT c.name, SUM(o.amount)
FROM mysql_prod.crm.customers c
JOIN iceberg.warehouse.orders o ON c.id = o.customer_id
WHERE o.order_date >= DATE '2026-01-01'
GROUP BY c.name;출력 예시(요약):
Fragment 0 [SINGLE]
Output[columnNames = [name, _col1]]
└─ Aggregate(FINAL)[name]
│ _col1 := sum("sum")
└─ Exchange[GATHER]
└─ Fragment 1 [HASH(name)]
Aggregate(PARTIAL)[name]
sum := sum("amount")
└─ InnerJoin[("id" = "customer_id")]
Cost: {rows: 500000 (40MB), cpu: 2.00G, memory: 50MB, network: 40MB}
├─ ScanProject[table = mysql_prod:crm.customers]
│ Estimates: {rows: 200000 (16MB), ...}
└─ ScanFilterProject[table = iceberg:warehouse.orders, filterPredicate = ...]
Estimates: {rows: 5000000 (400MB), ...}Fragment: Stage의 표현
출력에 나타나는 Fragment는 실행 계획의 Stage에 해당한다. Fragment 경계에서 Exchange(워커 간 데이터 이동)가 발생한다. Fragment 수가 많다는 것은 셔플이 많다는 뜻이다.
Cost 블록 읽기
Cost: {rows: 500000 (40MB), cpu: 2.00G, memory: 50MB, network: 40MB}| 항목 | 의미 |
|---|---|
rows: 500000 | 이 노드가 출력할 것으로 예상되는 행 수 |
(40MB) | 예상 출력 데이터 크기 |
cpu: 2.00G | 예상 CPU 비용 (상대적 단위) |
memory: 50MB | 예상 메모리 사용량 |
network: 40MB | 예상 네트워크 전송량 |
이 숫자들은 추정치다. 통계가 없거나 오래됐으면 크게 틀릴 수 있다.
EXPLAIN ANALYZE: 실측 데이터와 비교
EXPLAIN ANALYZE
SELECT c.name, SUM(o.amount)
FROM mysql_prod.crm.customers c
JOIN iceberg.warehouse.orders o ON c.id = o.customer_id
WHERE o.order_date >= DATE '2026-01-01'
GROUP BY c.name;EXPLAIN ANALYZE는 쿼리를 실제 실행하고 실측 통계를 계획에 붙인다.
CPU time vs Scheduled time 해석
Actual CPU time이 Scheduled time보다 훨씬 작으면, 워커가 CPU를 쓰지 않고 기다리고 있다는 뜻이다. 이는 I/O 병목(S3에서 데이터를 느리게 읽거나, MySQL에서 결과가 늦게 오거나)을 나타낸다.
Actual CPU time: 30s
Scheduled time: 5m 10s
→ CPU 효율 ≈ 10%. I/O 병목이 거의 모든 시간을 차지함.반대로 CPU time ≈ Scheduled time이면 연산이 병목이다. 조인 알고리즘 변경, 집계 순서 조정, 더 많은 워커 추가 등이 도움된다.
비용 기반 최적화(CBO)
CBO가 결정하는 것
Trino CBO는 쿼리 실행 전, 통계 정보를 바탕으로 다음을 최적화한다.
- 조인 순서: 여러 테이블 조인 시 어떤 순서로 조인할지.
- 조인 알고리즘: Broadcast Join vs Partitioned (Hash) Join.
- 집계 위치: Partial 집계를 워커에서 먼저 하고 이후 Final 집계를 할지.
- 푸시다운 여부: 필터/집계를 커넥터로 내릴지.
ANALYZE or 커넥터 자동
rows·bytes·카디널리티
최소 비용 순열 선택
Broadcast or Hash Join
SHOW STATS FOR t + join_reordering_strategy
= AUTOMATIC (기본값) + join_distribution_type
= AUTOMATIC (기본값)
통계 확인과 갱신
-- 현재 통계 상태 확인
SHOW STATS FOR iceberg.warehouse.orders;출력 예:
column_name | data_size | distinct_values_count | nulls_fraction | row_count
-------------|-----------|----------------------|----------------|----------
customer_id | 39321600 | 850000 | 0.0 | NULL
amount | 39321600 | 50000 | 0.02 | NULL
order_date | 9830400 | 365 | 0.0 | NULL
| | | | 4920000row_count가 NULL이 아닌 숫자가 마지막 행에 있으면 통계가 있다는 뜻이다. distinct_values_count가 NULL이면 카디널리티 통계가 없어 CBO 정확도가 낮아진다.
-- 통계 갱신 (Iceberg/Hive)
ANALYZE iceberg.warehouse.orders;
-- 특정 컬럼만 (대용량 테이블에서 시간 절약)
ANALYZE iceberg.warehouse.orders
WITH (columns = ARRAY['customer_id', 'order_date', 'amount']);MySQL/PostgreSQL 커넥터는 자동으로 DB 내장 통계를 사용한다. Kafka 커넥터는 통계를 제공하지 않는다.
Broadcast Join vs Hash Join
가장 중요한 CBO 결정 중 하나다.
| 방식 | 동작 | 언제 유리한가 | 단점 |
|---|---|---|---|
| Broadcast Join | 작은 테이블을 모든 워커에 복사 후 로컬 조인 | 한쪽이 수십 MB 이하일 때 | 큰 테이블엔 메모리 폭발 |
| Hash Join | 양쪽을 조인 키 해시로 파티셔닝 후 같은 파티션끼리 조인 | 양쪽 모두 클 때 | Exchange(셔플) 비용 발생 |
CBO가 잘못된 선택을 하거나 통계가 없을 때 힌트로 강제할 수 있다.
-- Broadcast 강제
SELECT /*+ BROADCAST(small_table) */ ...
FROM big_table
JOIN small_table ON ...
-- Hash Join 강제 (기본 동작이지만 명시 가능)
SET SESSION join_distribution_type = 'PARTITIONED';파티셔닝 전략
Iceberg 테이블의 파티셔닝은 Trino 쿼리 성능에 직접 영향을 미친다. 파티션 프루닝이 잘 동작하면 읽는 파일 수가 줄어든다.
파티션 프루닝이 동작하는 조건
-- ✅ 프루닝 동작: 상수 조건
WHERE order_date = DATE '2026-07-01'
WHERE order_date BETWEEN DATE '2026-07-01' AND DATE '2026-07-31'
-- ✅ 프루닝 동작: IN 절
WHERE region IN ('KR', 'JP', 'SG')
-- ❌ 프루닝 안 됨: 함수 변환
WHERE DATE_TRUNC('month', order_date) = DATE '2026-07-01'
WHERE YEAR(order_date) = 2026함수를 파티션 컬럼에 적용하면 Trino는 프루닝을 건너뛰고 모든 파일을 읽는다. 파티션 컬럼을 직접 조건에 쓰도록 SQL을 바꾸는 것이 더 낫다.
-- 월별 조회: DATE_TRUNC 대신 범위 조건
WHERE order_date >= DATE '2026-07-01'
AND order_date < DATE '2026-08-01'파티션 설계 선택지
실무 권장: 일(day) 파티션이 대부분의 이벤트 테이블에 무난한 선택이다. 조회 범위가 주로 최근 수일~수주라면 충분한 프루닝 효과를 얻으면서 파일 수도 관리 가능하다.
Iceberg Partition Evolution
Iceberg의 강점 중 하나는 테이블을 재작성하지 않고도 파티션 방식을 바꿀 수 있다는 것이다.
-- 기존: 월별 파티션
CREATE TABLE orders (
id BIGINT,
order_date DATE,
...
) WITH (
partitioning = ARRAY['month(order_date)']
);
-- 파티션을 일별로 변경 (기존 데이터는 유지)
ALTER TABLE orders
SET PROPERTIES partitioning = ARRAY['day(order_date)'];변경 이후 신규 데이터는 일별 파티션에, 기존 데이터는 원래 월별 파티션에 그대로 있다. Trino는 이 혼재 상태를 투명하게 처리한다.
세션 속성을 이용한 성능 조정
쿼리 수준에서 동작을 조정하는 주요 세션 속성이다.
-- 조인 알고리즘 고정
SET SESSION join_distribution_type = 'BROADCAST'; -- 또는 'PARTITIONED', 'AUTOMATIC'
-- 조인 순서 재정렬 전략
SET SESSION join_reordering_strategy = 'AUTOMATIC'; -- 또는 'ELIMINATE_CROSS_JOINS', 'NONE'
-- 집계 푸시다운 활성화 (MySQL/PostgreSQL)
SET SESSION mysql_prod.aggregation_pushdown_enabled = true;
-- 해시 조인 파티션 수 조정 (기본 100)
SET SESSION hash_partition_count = 200;
-- 스필 활성화 (메모리 부족 시 디스크 사용)
SET SESSION spill_enabled = true;실무 진단 순서
실행 → 가장 느린 Stage
찾기 (Scheduled time) → CPU vs Scheduled
비율 확인
(I/O 병목) → 파티션 프루닝 동작 확인 → 파일 크기/수 확인 (compaction) → 커넥터 pushdown 여부 점검
(연산 병목) → 조인 순서/알고리즘 점검 → Estimates vs Actual 비교 → ANALYZE로 통계 갱신
자주 발생하는 성능 문제와 해결
문제 1: 잘못된 조인 순서
-- 문제: CBO가 큰 테이블을 Broadcast 대상으로 선택
EXPLAIN ANALYZE
SELECT ...
FROM big_table -- 1억 행
JOIN small_table -- 1만 행
ON big_table.id = small_table.ref_id;EXPLAIN 결과에서 big_table이 Broadcast 쪽(Hash Build)으로 잡혀 있다면 워커 메모리가 폭발한다. 두 가지 방법으로 해결한다.
-- 방법 1: 힌트로 Broadcast 대상 지정
SELECT /*+ BROADCAST(small_table) */ ...
-- 방법 2: 통계 갱신 후 CBO에게 맡기기
ANALYZE iceberg.warehouse.big_table;
ANALYZE iceberg.warehouse.small_table;문제 2: 통계 없이 수십억 행 조인
쿼리가 시작하자마자 Exchange 단계에서 매우 오랫동안 머물면, Stage 간 데이터 이동이 병목일 가능성이 높다. CBO가 통계 없이 Partitioned Join을 선택했다면 Exchange 비용이 크다.
-- 통계 상태 점검
SHOW STATS FOR iceberg.warehouse.orders;
-- row_count가 NULL이면 통계 없음 → ANALYZE 실행문제 3: SELECT * 로 불필요한 컬럼 전달
-- 나쁜 예: 200개 컬럼 테이블에서 전체 읽기
SELECT * FROM iceberg.warehouse.wide_events
WHERE event_date = DATE '2026-07-01';
-- 좋은 예: 필요한 컬럼만
SELECT user_id, event_type, created_at
FROM iceberg.warehouse.wide_events
WHERE event_date = DATE '2026-07-01';Trino는 컬럼 프루닝(column pruning)을 지원해 필요한 컬럼만 커넥터에서 읽는다. Parquet/ORC의 열지향 포맷 특성상 불필요한 컬럼을 건너뛰는 효과가 크다.
문제 4: 대용량 DISTINCT 또는 GROUP BY
-- 카디널리티가 수천만인 컬럼에 DISTINCT
SELECT COUNT(DISTINCT user_id) FROM events;이 패턴은 모든 user_id를 Exchange하고 전역에서 DISTINCT 처리해 메모리 압력이 크다. 이 경우 HyperLogLog 근사 집계를 쓸 수도 있다.
-- HLL 근사치 (±2% 오차, 훨씬 빠르고 메모리 적음)
SELECT APPROX_DISTINCT(user_id) FROM events;웹 UI에서 확인하는 지표
Trino 웹 UI (http://coordinator:8080)에서 EXPLAIN 없이도 진행 중인 쿼리를 분석할 수 있다.
| UI 항목 | 의미 | 경고 신호 |
|---|---|---|
| Queued 시간 | 실행 슬롯 대기 | 워커 부족, 쿼리 동시성 제한 도달 |
| Split 처리 속도 (Rows/sec) | 커넥터 I/O 처리량 | 느리면 커넥터 병목 또는 S3 API 제한 |
| Memory (워커별) | 각 워커의 메모리 사용 | 한쪽 워커만 높으면 데이터 스큐 |
| Blocked 상태 | Exchange 버퍼 포화 | 셔플이 많거나 메모리 부족 |
| Failed tasks | 워커 장애 또는 OOM | 로그에서 세부 오류 확인 |
튜닝 체크리스트
EXPLAIN ANALYZE로 가장 오래 걸리는 Stage와 CPU/Scheduled 비율을 확인한다.SHOW STATS FOR로 통계 상태를 점검하고, 오래됐으면ANALYZE를 실행한다.- 파티션 컬럼에 함수를 사용한 조건이 없는지 확인한다 (프루닝 방해).
SELECT *대신 필요한 컬럼만 명시한다.- 조인 양쪽의 크기를 확인하고, 작은 테이블이 Broadcast 대상인지 확인한다.
- VARCHAR 컬럼 필터가 MySQL 커넥터에서 푸시다운되지 않음을 기억한다.
- OOM이 발생하면
spill_enabled = true설정 후 재시도한다. - 대규모 DISTINCT는
APPROX_DISTINCT로 대체 가능한지 검토한다.
References
- Trino Cost in EXPLAIN 공식 문서, https://trino.io/docs/current/optimizer/cost-in-explain.html
- Trino Query Optimizer 공식 문서, https://trino.io/docs/current/optimizer.html
- Trino CBO 소개 블로그 (2019), https://trino.io/blog/2019/07/04/cbo-introduction.html
- Starburst: Trino Query Plan Syntax, https://www.starburst.io/resources/query-plan-syntax/
- Medium — Simon Thelin: Query Plans in Trino, https://medium.com/@simon.thelin90/query-plans-analyse-sql-performance-in-trino-97ac1e8f8044
- CelerData: Trino Query Optimization Best Practices, https://celerdata.com/glossary/trino-query-optimization
- Alluxio Blog: Speed Trino Queries with Performance-Tuning Tips, https://www.alluxio.io/blog/speed-trino-queries-with-these-performance-tuning-tips
- Lester Martin: Trino Query Plan Analysis Video Series, https://lestermartin.blog/2025/04/22/trino-query-plan-analysis-video-series/