Trino SQL 실전 패턴 — Window, UNNEST, Lambda, 그리고 UDF
Trino 에서 실무 분석에 자주 쓰는 SQL 패턴을 정리합니다. 윈도우 함수로 순위·세션화, UNNEST 로 배열/맵 펼치기, 람다와 고차 함수로 컬렉션 가공, 페더레이션 조인, 그리고 SQL·Python UDF 까지.
Trino 는 ANSI SQL 을 충실히 따르면서도, 배열·맵·JSON 같은 반정형 데이터와 고차 함수를 다루는 강력한 확장을 제공합니다. 이 도구들을 알면 애플리케이션 코드로 후처리하던 일을 SQL 한 방에 끝낼 수 있습니다.
이 글은 Trino 에서 실무 분석에 자주 쓰는 SQL 패턴을 윈도우 함수 → UNNEST → 람다/고차 함수 → 페더레이션 → UDF 순으로, 바로 복사해 쓸 수 있는 예제와 함께 정리합니다.
1. 윈도우 함수 — 행을 넘나드는 계산
윈도우 함수는 GROUP BY 처럼 행을 합치지 않으면서, 각 행에서 "관련된 행들의 집합"을 계산합니다.
그룹별 순위 / Top-N
-- 카테고리별 매출 상위 3개 상품
SELECT category, product, revenue
FROM (
SELECT category, product, revenue,
row_number() OVER (PARTITION BY category ORDER BY revenue DESC) AS rn
FROM sales
)
WHERE rn <= 3;| 함수 | 동작 |
|---|---|
row_number() | 동순위 없이 1,2,3… |
rank() | 동순위 허용, 다음 순위 건너뜀(1,1,3) |
dense_rank() | 동순위 허용, 건너뛰지 않음(1,1,2) |
누적 합계 / 이동 평균
SELECT
d,
revenue,
sum(revenue) OVER (ORDER BY d ROWS UNBOUNDED PRECEDING) AS running_total,
avg(revenue) OVER (ORDER BY d ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7
FROM daily_sales;ROWS BETWEEN ... PRECEDING AND CURRENT ROW 프레임으로 이동 윈도우를 정의합니다. 7일 이동평균, 누적합 같은 시계열 지표에 필수입니다.
LAG / LEAD — 이전/다음 행 참조
-- 직전 이벤트와의 시간 간격
SELECT
user_id, event_time,
event_time - lag(event_time) OVER (PARTITION BY user_id ORDER BY event_time) AS gap
FROM events;세션화(Sessionization) 패턴
LAG + 조건부 누적합으로 "30분 이상 비활동 시 새 세션" 같은 세션 구분을 SQL 만으로 구현할 수 있습니다.
WITH marked AS (
SELECT user_id, event_time,
CASE WHEN event_time - lag(event_time)
OVER (PARTITION BY user_id ORDER BY event_time)
> INTERVAL '30' MINUTE
THEN 1 ELSE 0 END AS is_new_session
FROM events
)
SELECT user_id, event_time,
sum(is_new_session) OVER (PARTITION BY user_id ORDER BY event_time) AS session_id
FROM marked;2. UNNEST — 배열/맵을 행으로 펼치기
Trino 는 ARRAY, MAP, ROW 같은 복합 타입을 1급으로 다룹니다. UNNEST 는 컬렉션을 행으로 전개합니다.
-- 한 주문에 여러 상품(배열)이 든 행을 상품 단위 행으로
SELECT order_id, item
FROM orders
CROSS JOIN UNNEST(items) AS t(item);
-- 인덱스(순번)와 함께 펼치기 (WITH ORDINALITY)
SELECT order_id, idx, item
FROM orders
CROSS JOIN UNNEST(items) WITH ORDINALITY AS t(item, idx);
-- MAP 펼치기 (키, 값)
SELECT id, k, v
FROM tbl
CROSS JOIN UNNEST(attributes) AS t(k, v);반대로 행을 배열로 모을 때는 array_agg:
SELECT order_id, array_agg(item ORDER BY item) AS items
FROM order_items
GROUP BY order_id;3. JSON 다루기
로그·이벤트 페이로드가 JSON 문자열로 들어오는 경우가 흔합니다.
SELECT
json_extract_scalar(payload, '$.user.id') AS user_id,
json_extract_scalar(payload, '$.event.type') AS event_type,
cast(json_extract(payload, '$.items') AS ARRAY(VARCHAR)) AS items
FROM raw_events;| 함수 | 반환 |
|---|---|
json_extract_scalar | 스칼라 값(문자열) |
json_extract | JSON 조각(추가 파싱용) |
json_parse / cast(... AS JSON) | 문자열 → JSON 타입 |
4. 람다와 고차 함수 — 컬렉션을 SQL 로 가공
Trino 의 진짜 강점 중 하나는 배열에 함수를 적용하는 고차 함수입니다. UNNEST 없이 배열을 그대로 변형·필터·집계할 수 있습니다.
SELECT
transform(prices, x -> x * 1.1) AS with_tax, -- map
filter(scores, x -> x >= 60) AS passed, -- filter
reduce(nums, 0, (s, x) -> s + x, s -> s) AS total, -- fold/sum
array_sort(tags) AS sorted_tags,
any_match(flags, x -> x = true) AS has_true,
cardinality(filter(scores, x -> x >= 60)) AS pass_count
FROM t;| 고차 함수 | 역할 |
|---|---|
transform(arr, x -> ...) | 원소 변환(map) |
filter(arr, x -> ...) | 조건 필터 |
reduce(arr, init, (s,x)->..., s->...) | 누적(fold) |
any_match / all_match / none_match | 조건 충족 여부 |
zip_with(a, b, (x,y)->...) | 두 배열 결합 |
예: UNNEST + GROUP BY 로 풀어야 했던 "배열에서 60점 이상 개수"를 cardinality(filter(...)) 한 줄로 끝냅니다.
5. 페더레이션 — 카탈로그를 넘나드는 조인
Trino 의 정체성인 기능입니다. 서로 다른 시스템의 데이터를 한 쿼리에서 조인합니다.
-- 오브젝트 스토리지의 이벤트(Iceberg) × RDBMS 의 고객 마스터(PostgreSQL)
SELECT e.event_type, c.tier, count(*) AS cnt
FROM iceberg.analytics.events e
JOIN postgresql.crm.customers c ON e.user_id = c.id
WHERE e.event_time >= TIMESTAMP '2026-06-01 00:00:00 UTC'
GROUP BY e.event_type, c.tier;
-- 한 시스템의 데이터를 다른 시스템으로 적재 (CTAS 페더레이션)
CREATE TABLE iceberg.analytics.customer_snapshot AS
SELECT * FROM postgresql.crm.customers;성능 팁: RDBMS 커넥터는 predicate/aggregate pushdown 으로 WHERE·집계를 소스 DB 로 밀어 내립니다. 작은 차원 테이블은 broadcast 조인이 자연스럽고, 큰 테이블끼리는 통계가 있어야 CBO 가 올바른 분배를 고릅니다.
6. 자주 쓰는 실무 함수 모음
-- 안전한 형변환 (실패 시 NULL)
try_cast(value AS INTEGER)
-- NULL 안전 비교 / 치환
coalesce(a, b, 0)
nullif(a, 0) -- a=0 이면 NULL (0으로 나누기 방지)
-- 날짜 (단위가 첫 인자!)
date_add('day', 7, current_date)
date_diff('hour', t1, t2)
date_trunc('month', event_time)
format_datetime(event_time, 'yyyy-MM-dd')
-- 문자열
split(path, '/') -- 배열로
regexp_extract(ua, '(\d+)\.(\d+)', 1)
url_extract_host(url)
-- 근사 집계 (대용량에서 빠름)
approx_distinct(user_id) -- 근사 distinct count
approx_percentile(latency, 0.95) -- p95
approx_distinct/approx_percentile는 대규모 데이터에서 정확한count(distinct)보다 훨씬 빠르고 메모리를 적게 씁니다. 대시보드 지표처럼 약간의 오차가 허용되는 곳에서 적극 활용하세요.
7. UDF — 표준 함수로 부족할 때
반복되는 로직을 함수로 캡슐화할 수 있습니다.
SQL UDF (인라인 / 세션·카탈로그 등록)
-- 쿼리 안에서 즉석 정의 (WITH FUNCTION)
WITH FUNCTION to_won(usd DOUBLE)
RETURNS DOUBLE
RETURN usd * 1330.0
SELECT product, to_won(price_usd) AS price_krw
FROM catalog.schema.products;복잡한 로직도 SQL UDF 로 표현할 수 있고, 카탈로그에 영구 등록하면 팀 전체가 재사용할 수 있습니다.
CREATE FUNCTION catalog.schema.mask_email(email VARCHAR)
RETURNS VARCHAR
RETURN regexp_replace(email, '(^.).*(@.*$)', '$1***$2');Python UDF
표준·SQL 함수로 표현하기 어려운 로직은 Python UDF 로 작성할 수 있습니다(샌드박스 실행). 문자열 정규화, 커스텀 파싱처럼 절차적 코드가 자연스러운 작업에 유용합니다.
CREATE FUNCTION catalog.schema.py_slug(s VARCHAR)
RETURNS VARCHAR
LANGUAGE PYTHON
WITH (handler = 'slug')
AS $$
def slug(s):
return "-".join(s.lower().split()) if s else None
$$;주의: UDF, 특히 Python UDF 는 강력하지만 옵티마이저 입장에서 블랙박스입니다. WHERE 절에서 컬럼을 UDF 로 감싸면 pushdown·프루닝이 깨질 수 있으니, 대용량 필터 조건에는 표준 함수를 우선하고 UDF 는 투영(SELECT) 단계에서 쓰는 것이 안전합니다.
8. 안티패턴 — 성능을 망치는 SQL
| 안티패턴 | 문제 | 대안 |
|---|---|---|
WHERE CAST(ts AS DATE) = ... | pushdown·프루닝 깨짐 | 범위 비교 ts >= ... AND ts < ... |
WHERE year(ts) = 2026 | 함수 래핑 → 풀스캔 | 범위 비교 |
SELECT * | 불필요 컬럼까지 스캔/전송 | 필요한 컬럼만 |
count(distinct big_col) 남발 | 메모리·시간 폭증 | approx_distinct |
| 큰 테이블 먼저 조인 | 중간 결과 폭증 | 통계 + CBO, 작은 쪽 먼저 |
| WHERE 에 UDF 래핑 | 옵티마이저 블랙박스 | SELECT 단계로 이동 |
9. 정리
| 도구 | 쓰임 | 대표 함수/구문 |
|---|---|---|
| 윈도우 함수 | 순위·누적·이동평균·세션화 | row_number, sum() OVER, lag/lead |
| UNNEST | 배열/맵 → 행 | CROSS JOIN UNNEST ... WITH ORDINALITY |
| 고차 함수 | 배열 변환/필터/집계 | transform, filter, reduce |
| JSON | 반정형 파싱 | json_extract_scalar |
| 페더레이션 | 이종 소스 조인 | catalog.schema.table 조인 |
| 근사 집계 | 대용량 고속 지표 | approx_distinct, approx_percentile |
| UDF | 로직 캡슐화 | WITH FUNCTION, CREATE FUNCTION |
Trino SQL 의 핵심은 "애플리케이션으로 빼던 후처리를 SQL 안으로 끌어오는 것"입니다. 윈도우 함수와 고차 함수만 익숙해져도 파이프라인 코드가 크게 줄고, 페더레이션과 근사 집계는 다른 엔진으로는 번거로운 분석을 한 줄로 만들어 줍니다. 다만 WHERE 절에서의 함수·UDF 래핑은 프루닝을 깨뜨리니, "필터는 표준 함수로, 가공은 자유롭게"를 기억하세요.
이 글은 Trino 440번대 기준으로 작성되었습니다. 복잡한 분석 쿼리 설계나 SQL 성능 튜닝이 필요하시면 언제든 문의해 주세요.
— Data Dynamics 엔지니어링 팀