데이터 엔지니어 인터뷰 — Trino

기본 구조

Trino는 무엇이고 어디에 쓰나요

분산 SQL 쿼리 엔진입니다. 저장소를 갖지 않고, 여러 데이터 소스에 있는 데이터를 SQL로 조회합니다.

핵심 특징은 두 가지입니다.

  • 저장소 비소유 — S3의 Parquet, MySQL, Kafka, Elasticsearch를 각각의 자리에서 읽습니다
  • 인터랙티브 지향 — 중간 결과를 디스크에 쓰지 않고 메모리에서 파이프라인으로 흘려보내 지연을 줄입니다

애드혹 분석, BI 대시보드 백엔드, 여러 소스를 조인하는 페더레이션 쿼리에 적합합니다. Presto에서 갈라져 나온 프로젝트입니다.

Coordinator와 Worker의 역할을 설명해보세요

Coordinator는 SQL을 파싱하고 실행 계획을 세우고, 스플릿을 워커에 배분하며, 결과를 취합합니다. 클라이언트가 접속하는 지점이기도 합니다.

Worker는 스플릿을 처리하고 워커끼리 데이터를 교환합니다.

Coordinator는 보통 하나이고 단일 장애점입니다. 최근 버전에는 fault-tolerant execution과 다중 코디네이터 옵션이 있지만, 기본 구성에서는 코디네이터가 죽으면 실행 중인 쿼리가 전부 실패합니다.

Trino와 Spark는 어떻게 다른가요

  Trino Spark
목적 인터랙티브 쿼리 범용 분산 처리
중간 결과 메모리 파이프라인 디스크 셔플 가능
장애 대응 기본적으로 쿼리 전체 재시작 태스크 단위 재시도
실행 시간 초~분 분~시간
API SQL SQL + DataFrame + RDD

Trino는 실패하면 처음부터 다시입니다. 메모리에 중간 결과를 들고 있어 워커가 죽으면 복구할 방법이 없기 때문입니다. 그래서 몇 시간짜리 ETL에는 부적합하고, 그런 작업은 Spark가 맞습니다.

Trino 400번대 이후의 fault-tolerant execution(exchange manager로 중간 결과를 외부 스토리지에 저장)을 켜면 장기 실행 쿼리도 가능해지지만, 그만큼 지연이 늘어납니다.

커넥터 모델을 설명해보세요

각 데이터 소스마다 커넥터가 있고, 커넥터는 세 가지를 제공합니다.

  • 메타데이터 — 어떤 스키마·테이블이 있는지
  • 데이터 위치 — 스플릿을 어떻게 나눌지
  • 데이터 접근 — 실제로 읽는 방법

카탈로그 이름으로 소스를 구분해 완전 수식명으로 참조합니다.

SELECT * FROM hive.analytics.orders;
SELECT * FROM mysql.shop.customers;
SELECT * FROM iceberg.warehouse.events;

이 구조 덕분에 소스가 다른 테이블을 한 쿼리에서 조인할 수 있습니다.

쿼리 실행

Stage, Task, Split, Driver의 관계를 설명해보세요

Query
 └── Stage        교환(exchange) 경계로 분리
       └── Task   워커 하나에서 도는 Stage 인스턴스
             └── Driver  파이프라인 실행 단위
                   └── Split  처리할 데이터 조각

Stage 사이에서 데이터 교환이 일어납니다. 교환 방식이 성능을 크게 좌우합니다.

분산 조인과 브로드캐스트 조인을 비교해보세요

브로드캐스트 조인은 오른쪽(작은) 테이블을 모든 워커에 복제합니다. 왼쪽은 그대로 두므로 큰 테이블의 셔플이 없습니다. 작은 테이블이 워커 메모리에 들어가야 합니다.

분산(파티션) 조인은 양쪽을 조인 키로 해시 파티셔닝해 재분배합니다. 큰 테이블끼리 조인할 때 쓰지만 네트워크 비용이 큽니다.

SET SESSION join_distribution_type = 'BROADCAST';   -- 또는 'PARTITIONED', 'AUTOMATIC'

조인 순서가 중요합니다. Trino는 관례적으로 오른쪽 테이블을 빌드 사이드로 씁니다. 통계가 없으면 옵티마이저가 잘못 판단하므로, 작은 테이블을 오른쪽에 두는 습관이 도움이 됩니다.

-- 권장: 작은 쪽을 오른쪽에
SELECT * FROM huge_facts f JOIN small_dim d ON f.dim_id = d.id;

비용 기반 최적화(CBO)는 어떻게 동작하나요

테이블 통계(행 수, 컬럼별 distinct 수, NULL 비율, min/max)를 보고 조인 순서와 분배 방식을 정합니다.

통계가 없으면 휴리스틱에 의존해 잘못된 계획을 세우기 쉽습니다. Hive 테이블은 ANALYZE로 통계를 수집해야 하고, Iceberg는 메타데이터에 통계를 갖고 있어 유리합니다.

ANALYZE hive.analytics.orders;
EXPLAIN (TYPE DISTRIBUTED) SELECT ...;

동적 필터링은 무엇인가요

조인의 작은 쪽을 먼저 읽어 실제 조인 키 값의 범위·집합을 알아낸 뒤, 큰 쪽 스캔에 그 조건을 밀어 넣는 최적화입니다.

-- 작은 dim 에서 date_id 가 3개뿐이라면,
-- fact 스캔 시 그 3개에 해당하는 파티션·파일만 읽음
SELECT * FROM fact f JOIN dim d ON f.date_id = d.date_id
WHERE d.year = 2026;

파티셔닝된 큰 테이블에서 효과가 매우 큽니다. 스캔량이 수십 배 줄어드는 경우도 있습니다.

메모리

Trino의 메모리 관리 구조를 설명해보세요

메모리를 풀로 나눠 관리하고, 쿼리별 상한을 둡니다.

설정 의미
query.max-memory-per-node 워커 하나에서 쿼리 하나가 쓸 수 있는 최대
query.max-memory 클러스터 전체에서 쿼리 하나가 쓸 수 있는 최대
query.max-total-memory 시스템 메모리 포함

한도를 넘으면 쿼리가 kill됩니다. Trino가 “메모리 부족” 오류를 자주 내는 이유는, 인터랙티브 지향이라 기본적으로 중간 결과를 메모리에 들고 있기 때문입니다.

메모리 부족 오류가 날 때 어떻게 접근하나요

무작정 한도를 올리기 전에 원인을 봅니다.

  1. SELECT *와 큰 결과 — 필요한 컬럼만, LIMIT 사용
  2. ORDER BY 전역 정렬 — 전체를 한 곳에 모읍니다. 정말 필요한지 확인
  3. DISTINCT / GROUP BY 고카디널리티approx_distinct()로 대체 가능한지
  4. 브로드캐스트 조인 오판 — 작다고 판단한 테이블이 실제로는 큼. PARTITIONED로 전환
  5. skew — 특정 키에 몰려 워커 하나가 초과

spill-enabled를 켜면 일부 연산(집계, 조인, 정렬)이 디스크로 스필해 메모리 부족을 피할 수 있습니다. 대신 크게 느려지므로 최후 수단입니다.

리소스 그룹은 무엇인가요

쿼리를 그룹으로 나눠 동시 실행 수와 메모리를 제한하는 기능입니다. 애드혹 쿼리가 클러스터를 독차지해 대시보드가 멈추는 상황을 막습니다.

{
  "rootGroups": [{
    "name": "global",
    "softMemoryLimit": "80%",
    "maxQueued": 100,
    "hardConcurrencyLimit": 50,
    "subGroups": [
      {"name": "dashboard", "hardConcurrencyLimit": 30, "softMemoryLimit": "40%"},
      {"name": "adhoc",     "hardConcurrencyLimit": 10, "softMemoryLimit": "30%"},
      {"name": "etl",       "hardConcurrencyLimit": 5,  "softMemoryLimit": "30%"}
    ]
  }]
}

사용자·소스·클라이언트 태그로 그룹을 매칭합니다.

성능

쿼리 성능을 높이는 방법을 설명해보세요

Trino는 저장소를 갖지 않으므로, 성능의 상당 부분이 데이터 레이아웃에 달려 있습니다.

  • 컬럼너 포맷 — Parquet/ORC. 필요한 컬럼만 읽고 통계로 블록을 건너뜀
  • 파티셔닝 — 조회 조건에 맞는 파티션만 스캔
  • 파일 크기 — 작은 파일이 많으면 스플릿 오버헤드와 메타데이터 조회가 폭증
  • 통계 수집 — CBO가 제대로 동작하려면 필수
  • 동적 필터링 활용 — 조인 구조를 그에 맞게

쿼리 쪽에서는 SELECT * 지양, 조기 필터링, 불필요한 ORDER BY 제거, approx_distinct() 활용이 기본입니다.

EXPLAIN ANALYZE에서 무엇을 보나요

EXPLAIN ANALYZE SELECT ...;
  • 각 Stage의 입력·출력 행 수 — 어디서 데이터가 폭발하는지
  • 소요 시간 분포 — 특정 Stage에 몰려 있는지
  • 워커별 시간 편차 — 크게 벌어지면 skew
  • 스캔한 바이트 — 파티션 프루닝이 동작했는지

Web UI(기본 8080 포트)에서 실행 중인 쿼리의 Stage별 진행 상황을 실시간으로 볼 수 있습니다.

페더레이션 쿼리에서 주의할 점은 무엇인가요

여러 소스를 조인할 수 있다는 것이 Trino의 매력이지만, 함정이 있습니다.

푸시다운이 안 되면 소스 전체를 끌어옵니다. MySQL 커넥터가 필터를 푸시다운하지 못하면 테이블 전체를 네트워크로 가져와 Trino에서 거릅니다. 운영 DB에 부하를 주고 쿼리도 느립니다.

-- 위험 — 운영 DB 전체를 끌어올 수 있음
SELECT * FROM hive.logs.events e
JOIN mysql.prod.users u ON e.user_id = u.id;

운영 DB를 직접 조인 대상으로 삼는 것은 신중해야 합니다. 정기적으로 웨어하우스에 복제해두고 그것과 조인하는 편이 안전합니다.

EXPLAIN으로 어떤 조건이 소스로 내려가는지 확인할 수 있습니다.

Iceberg·Delta 같은 테이블 포맷과 함께 쓸 때 이점은 무엇인가요

  • 메타데이터 기반 프루닝 — 파일 리스팅 없이 통계로 건너뜀
  • 스냅샷 격리 — 쓰기 중에도 일관된 읽기
  • 타임 트래블 — 과거 시점 조회
  • 통계 내장 — CBO가 바로 활용

Hive 테이블에서 파티션이 수만 개일 때 메타스토어 조회가 병목이 되는데, Iceberg는 이 문제를 구조적으로 해결합니다.

언제 Trino가 적합하지 않나요

  • 장시간 ETL — 실패 시 처음부터. Spark가 맞습니다
  • 매우 큰 조인·집계 — 메모리 한계에 부딪힙니다
  • 초저지연 단건 조회 — OLTP DB나 키-값 저장소의 영역입니다
  • 스트리밍 — 배치 쿼리 엔진입니다

관련 글