Blog
trinocbooptimizerperformanceexplaindata-platform

Trino Cost-Based Optimizer 깊이 보기 — 통계, 조인 재정렬, EXPLAIN 읽는 법

Trino 의 비용 기반 옵티마이저(CBO)가 통계를 근거로 조인 순서와 분배 방식을 어떻게 결정하는지, ANALYZE 로 통계를 채우는 법, 그리고 EXPLAIN / EXPLAIN ANALYZE 를 실제로 읽고 느린 쿼리를 진단하는 법을 정리합니다.

Data Dynamics2026년 6월 5일13 min read

같은 쿼리라도 조인 순서와 데이터 분배 방식에 따라 실행 시간이 수십 배 차이 납니다. 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 fractionNULL 비율선택도 보정
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 (해시 재분배 조인)

양쪽 테이블을 조인 키로 해시 재분배해서 같은 키가 같은 워커에 모이게 합니다.

양쪽 모두 조인 키로 셔플 → 큰 테이블끼리 조인할 때 필수
BroadcastPartitioned
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워커 간 데이터 셔플 (네트워크 비용 지점)
EstimatesCBO 의 행 수·크기·비용 추정치

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 엔지니어링 팀