Blog
trinocbooptimizerperformanceexplaindata-platform

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

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

Data Dynamics2026年6月5日13 min read
This post is not yet translated. The original Korean version is shown below.

같은 쿼리라도 조인 순서와 데이터 분배 방식에 따라 실행 시간이 수십 배 차이 납니다. 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 엔지니어링 팀