Trino Cost-Based Optimizer 깊이 보기 — 통계, 조인 재정렬, EXPLAIN 읽는 법
Trino 의 비용 기반 옵티마이저(CBO)가 통계를 근거로 조인 순서와 분배 방식을 어떻게 결정하는지, ANALYZE 로 통계를 채우는 법, 그리고 EXPLAIN / EXPLAIN ANALYZE 를 실제로 읽고 느린 쿼리를 진단하는 법을 정리합니다.
같은 쿼리라도 조인 순서와 데이터 분배 방식에 따라 실행 시간이 수십 배 차이 납니다. Trino 의 비용 기반 옵티마이저(Cost-Based Optimizer, CBO) 는 통계를 근거로 이 결정을 자동으로 내립니다. 문제는 통계가 없거나 부정확하면 CBO 가 엉뚱한 계획을 세운다는 것입니다.
이 글은 CBO 가 무엇을 보고 결정하는지, 통계를 어떻게 채우는지, 그리고 EXPLAIN 으로 실행 계획을 읽어 느린 쿼리를 진단하는 법을 정리합니다.
1. CBO 가 결정하는 것
옵티마이저는 논리적으로 같은 결과를 내는 여러 실행 계획 중 비용이 가장 낮은 것을 고릅니다. CBO 가 좌우하는 핵심 결정은 두 가지입니다.
| 결정 | 내용 | 영향 |
|---|---|---|
| 조인 순서(Join Reordering) | 여러 테이블을 어떤 순서로 조인할지 | 중간 결과 크기 → 메모리·시간 |
| 조인 분배(Join Distribution) | 브로드캐스트 vs 파티션(해시 재분배) | 네트워크 셔플 비용 |
이 둘을 잘못 고르면, 작은 테이블을 먼저 줄여 처리할 것을 거대한 중간 결과를 만들어 메모리를 터뜨립니다. CBO 의 판단 근거가 바로 통계입니다.
2. CBO 가 쓰는 통계
Trino 가 활용하는 테이블/컬럼 통계는 다음과 같습니다.
| 통계 | 의미 | 쓰임 |
|---|---|---|
| row count | 테이블/파티션의 행 수 | 조인 크기 추정 |
| NDV (distinct values) | 컬럼의 고유값 개수 | 조인 선택도, 그룹 수 추정 |
| min / max | 컬럼 값 범위 | 범위 조건 선택도, 프루닝 |
| null fraction | NULL 비율 | 선택도 보정 |
| data size | 컬럼 평균 크기 | 메모리·네트워크 비용 |
Iceberg 테이블은 데이터 파일 수준 통계(row count, min/max, null count)를 manifest 에 항상 들고 있어 기본 프루닝은 통계 수집 없이도 됩니다. 다만 NDV 같은 CBO 핵심 통계는 ANALYZE 로 별도 수집해야 조인 순서 최적화 품질이 올라갑니다.
3. ANALYZE — 통계 수집
-- 전체 컬럼 통계 수집
ANALYZE iceberg.analytics.events;
-- 특정 컬럼만 (자주 조인/필터되는 컬럼 위주로)
ANALYZE iceberg.analytics.events
WITH (columns = ARRAY['user_id', 'event_type', 'event_time']);수집된 통계는 SHOW STATS 로 확인합니다.
SHOW STATS FOR iceberg.analytics.events;
-- 조건이 걸린 결과에 대한 추정 통계도 볼 수 있다
SHOW STATS FOR (
SELECT * FROM iceberg.analytics.events
WHERE event_time >= TIMESTAMP '2026-06-01 00:00:00 UTC'
);출력에는 컬럼별 distinct_values_count, nulls_fraction, row_count, data_size, low_value, high_value 가 나옵니다. distinct_values_count 가 비어 있으면(NULL) CBO 가 추측에 의존하게 되므로, 이 값이 채워졌는지가 1차 점검 포인트입니다.
운영 팁: 큰 테이블은 적재 파이프라인 끝에
ANALYZE를 붙이거나, 야간 스케줄로 정기 수집하세요. 통계가 오래되면(데이터는 늘었는데 통계는 옛날 값) CBO 가 크기를 과소·과대 추정해 계획이 나빠집니다.
4. 조인 분배 — 브로드캐스트 vs 파티션
두 테이블을 조인할 때 데이터를 워커에 어떻게 배치할지 두 전략이 있습니다.
Broadcast Join (복제 조인)
작은 테이블(build side)을 모든 워커에 복제하고, 큰 테이블(probe side)은 그 자리에서 조인합니다.
큰 테이블 (그대로 분산) × 작은 테이블 (모든 워커에 복제)
→ 큰 테이블의 셔플 없음. 작은 테이블이 충분히 작을 때 최고Partitioned Join (해시 재분배 조인)
양쪽 테이블을 조인 키로 해시 재분배해서 같은 키가 같은 워커에 모이게 합니다.
양쪽 모두 조인 키로 셔플 → 큰 테이블끼리 조인할 때 필수| Broadcast | Partitioned | |
|---|---|---|
| build side 처리 | 모든 워커에 복제 | 키로 재분배 |
| 적합 | 한쪽이 작을 때 | 양쪽 다 클 때 |
| 위험 | build 가 크면 메모리 폭발 | 셔플 네트워크 비용 |
설정으로 정책을 제어합니다.
# etc/config.properties
join-distribution-type=AUTOMATIC # CBO 가 통계 기반으로 자동 선택 (권장)
# BROADCAST 또는 PARTITIONED 로 강제 가능AUTOMATIC 일 때 CBO 는 통계를 보고 작은 쪽을 broadcast 할지, 둘 다 클 때 partitioned 로 갈지 결정합니다. 통계가 없으면 이 판단이 빗나가 broadcast 하면 안 되는 큰 테이블을 복제해 OOM 을 냅니다.
5. 조인 재정렬 (Join Reordering)
여러 테이블 조인에서 순서가 성능을 좌우합니다. 핵심 원리는 중간 결과를 최대한 작게 유지하는 것입니다.
SELECT *
FROM fact_events e -- 10억 행
JOIN dim_users u ON e.user_id = u.id -- 100만 행
JOIN dim_region r ON u.region_id = r.id -- 50 행
WHERE r.country = 'KR';r.country = 'KR' 로 dim_region 을 먼저 줄이고, 그 결과로 dim_users 를, 마지막에 fact_events 와 조인하면 중간 결과가 작게 유지됩니다. 반대 순서로 가면 10억 행을 먼저 부풀려 메모리를 터뜨립니다. CBO 는 통계(특히 NDV·row count)로 각 순서의 비용을 추정해 최적 순서를 고릅니다.
optimizer.join-reordering-strategy=AUTOMATIC # 통계 기반 재정렬 (권장)AUTOMATIC 은 통계가 있을 때만 제대로 동작합니다. 통계가 없으면 ELIMINATE_CROSS_JOINS 수준의 보수적 재정렬만 합니다.
6. EXPLAIN 읽는 법
6.1 EXPLAIN (논리/분산 계획)
EXPLAIN
SELECT u.region_id, count(*)
FROM iceberg.analytics.events e
JOIN iceberg.analytics.users u ON e.user_id = u.id
GROUP BY u.region_id;-- 분배 방식까지 보려면
EXPLAIN (TYPE DISTRIBUTED)
SELECT ...;계획에서 확인할 핵심 키워드:
| 키워드 | 의미 |
|---|---|
ScanFilterProject | 테이블 스캔 + 필터/투영. 여기에 pushdown 된 조건이 보임 |
InnerJoin 등 | 조인 노드. 옆에 (REPLICATED)=broadcast, (PARTITIONED)=해시 조인 |
Aggregate(PARTIAL/FINAL) | 부분 집계 → 최종 집계 (2단계 집계) |
RemoteExchange | 워커 간 데이터 셔플 (네트워크 비용 지점) |
Estimates | CBO 의 행 수·크기·비용 추정치 |
Estimates: rows = ? 처럼 추정치가 ? 로 나오면 통계가 없다는 신호입니다. 이 경우 ANALYZE 부터 돌려야 합니다.
6.2 EXPLAIN ANALYZE (실제 실행 후)
실제로 쿼리를 실행하고 단계별 실측치를 보여줍니다. 진단의 핵심 도구입니다.
EXPLAIN ANALYZE
SELECT u.region_id, count(*)
FROM iceberg.analytics.events e
JOIN iceberg.analytics.users u ON e.user_id = u.id
GROUP BY u.region_id;출력에서 볼 것:
- 각 연산자의 실제 행 수 vs 추정 행 수: 크게 어긋나면 통계가 부정확하다는 뜻. →
ANALYZE재수집. - 연산자별 소요 시간(CPU/wall): 시간이 집중된 스테이지가 병목.
- Input/Output rows: 어디서 데이터가 폭증하는지.
- 셔플(Exchange) 데이터량: partitioned 조인의 네트워크 비용.
진단 1순위: 추정 행 수와 실제 행 수의 괴리. CBO 의 모든 결정이 추정에서 나오므로, 추정이 틀리면 계획 전체가 틀립니다. 괴리가 크면 통계 문제, 괴리가 작은데도 느리면 데이터 레이아웃(파티션·정렬·작은 파일) 문제입니다.
7. Pushdown 과 Dynamic Filtering — CBO 의 동반자
CBO 가 좋은 계획을 세워도, 스캔 단계에서 데이터를 못 줄이면 소용없습니다. 두 메커니즘이 보완합니다.
- Predicate / Aggregate Pushdown: WHERE 조건이나 집계를 커넥터(소스)로 밀어 내려, 소스에서 미리 필터/집계합니다. RDBMS 커넥터에서 특히 강력합니다.
- Dynamic Filtering: 조인의 build side 결과로부터 동적 필터를 만들어 probe side 스캔에 적용해, 읽을 파일/행을 런타임에 줄입니다.
EXPLAIN 에서 dynamicFilter 항목이 있는지로 동적 필터링 동작을 확인할 수 있습니다. WHERE 절에서 컬럼을 함수로 감싸면(CAST(ts AS DATE), year(ts)) pushdown 이 깨지므로, 컬럼은 좌변에 단독으로 두는 것이 원칙입니다.
8. CBO 가 빗나갈 때 — 체크리스트
| 증상 | 원인 | 처방 |
|---|---|---|
EXPLAIN 의 Estimates 가 ? | 통계 없음 | ANALYZE 수집 |
| 추정 행 수 ≪ 실제 | 통계가 오래됨 | ANALYZE 재수집 |
| 큰 테이블이 broadcast 돼 OOM | 크기 추정 실패 | 통계 갱신, 필요시 join_distribution_type 세션 강제 |
| 조인 순서가 비효율 | 재정렬 비활성/통계 부재 | join-reordering-strategy=AUTOMATIC + 통계 |
| pushdown 안 됨 | WHERE 에 함수 래핑 | 컬럼을 좌변에 단독으로 |
| 동적 필터 미작동 | build side 과대 / OUTER JOIN | 조인 구조 재검토 |
9. 세션 레벨 미세 조정
쿼리 하나만 다르게 다루고 싶을 때:
SET SESSION join_reordering_strategy = 'AUTOMATIC';
SET SESSION join_distribution_type = 'PARTITIONED'; -- broadcast 강제 해제전역 기본값은 안정적으로 두고, 특이 쿼리만 세션으로 조정하는 것이 운영상 안전합니다.
10. 정리
| 핵심 | 요점 |
|---|---|
| CBO 의 연료 | 통계(row count, NDV, min/max, null). 없으면 추측 |
| 통계 채우기 | ANALYZE, 정기 스케줄, 적재 파이프라인 끝에 |
| 조인 분배 | AUTOMATIC + 정확한 통계로 broadcast/partitioned 자동 |
| 조인 순서 | 통계 기반 재정렬로 중간 결과 최소화 |
| 진단 | EXPLAIN 으로 계획, EXPLAIN ANALYZE 로 추정 vs 실제 괴리 확인 |
| 스캔 효율 | pushdown·dynamic filtering, 함수 래핑 금지 |
CBO 의 모든 결정은 통계 추정에서 출발합니다. 따라서 Trino 성능 튜닝의 절반은 "정확한 통계를 유지하는 일"이고, 나머지 절반은 "EXPLAIN ANALYZE 로 추정과 실제의 괴리를 읽어내는 일"입니다. 이 두 가지가 습관이 되면, 느린 쿼리를 감이 아니라 근거로 고칠 수 있습니다.
이 글은 Trino 440번대 기준으로 작성되었습니다. 쿼리 성능 진단·튜닝이나 통계 운영 체계 수립이 필요하시면 언제든 문의해 주세요.
— Data Dynamics 엔지니어링 팀