Blog
trinosqlwindow-functionudfdata-platform

Trino SQL 실전 패턴 — Window, UNNEST, Lambda, 그리고 UDF

Trino 에서 실무 분석에 자주 쓰는 SQL 패턴을 정리합니다. 윈도우 함수로 순위·세션화, UNNEST 로 배열/맵 펼치기, 람다와 고차 함수로 컬렉션 가공, 페더레이션 조인, 그리고 SQL·Python UDF 까지.

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

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_extractJSON 조각(추가 파싱용)
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 엔지니어링 팀