Blog
trinosqlwindow-functionudfdata-platform

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

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

Data Dynamics2026년 6월 5일15 min read

Trino 는 ANSI SQL 을 충실히 따르면서도, 배열·맵·JSON 같은 반정형 데이터와 고차 함수를 다루는 강력한 확장을 제공합니다. 이 도구들을 익혀 두면 애플리케이션 코드로 후처리하던 일을 SQL 한 방에 끝낼 수 있거든요.

이 글은 Trino 에서 실무 분석에 자주 쓰는 SQL 패턴을 윈도우 함수 → UNNEST → 람다/고차 함수 → 페더레이션 → UDF 순으로, 바로 복사해 쓸 수 있는 예제와 함께 정리합니다.

이 글에서 배우는 것

  • 윈도우 함수로 순위, 누적합, 이동평균, 세션화를 SQL 만으로 구현하는 방법
  • UNNEST 와 고차 함수(transform, filter, reduce)로 배열·맵을 자유롭게 가공하는 법
  • 페더레이션 쿼리로 이종 시스템 데이터를 한 SQL 에 묶는 법
  • SQL UDF 와 Python UDF 로 반복 로직을 캡슐화하는 법
  • 성능을 망치는 안티패턴과 그 대안

1. 윈도우 함수 — 행을 넘나드는 계산

윈도우 함수는 GROUP BY 처럼 행을 합치지 않으면서, 각 행에서 "관련된 행들의 집합"을 계산해 줍니다. 순위·누적합·이동평균처럼 "이웃 행을 보면서 계산"해야 하는 경우에 딱 맞는 도구죠.

한 문장으로: 윈도우 함수는 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 만으로 구현할 수 있습니다. 별도 파이프라인 없이 쿼리 하나로 세션 ID 를 붙일 수 있어서 편리하죠.

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 문자열로 들어오는 경우가 흔하죠. Trino 는 JSON 을 직접 파싱하는 함수를 제공하므로, 별도 전처리 없이 SQL 안에서 필드를 꺼낼 수 있습니다.

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 의 진짜 강점 중 하나가 바로 이 고차 함수입니다. 배열을 굳이 행으로 풀지 않아도, 함수를 직접 넘겨서 변형·필터·집계를 한 번에 처리할 수 있거든요.

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 를 Trino 답게 만드는 기능이 바로 페더레이션입니다. 서로 다른 시스템에 흩어진 데이터를 한 쿼리에서 조인할 수 있어, 별도 ETL 없이 바로 분석에 들어갈 수 있죠.

한 문장으로: 카탈로그 이름만 앞에 붙이면 이종 시스템끼리 JOIN 이 됩니다.

-- 오브젝트 스토리지의 이벤트(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 — 표준 함수로 부족할 때

여러 쿼리에서 반복되는 로직이 있다면 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 로 컬럼을 감싸면 프루닝이 깨지니, "필터는 표준 함수로, 가공은 자유롭게"를 꼭 기억해 두세요.

마치며 — 핵심 요약

  • 윈도우 함수는 행을 유지하면서 이웃 행을 참조합니다. PARTITION BY + ORDER BY + 프레임 구문을 조합하면 순위·누적합·이동평균·세션화까지 한 쿼리로 처리할 수 있습니다.
  • UNNEST 로 배열·맵을 행으로 펼치고, array_agg 로 다시 배열로 모을 수 있습니다. WITH ORDINALITY 옵션으로 순번도 함께 얻을 수 있죠.
  • 고차 함수 (transform, filter, reduce, any_match 등)를 사용하면 UNNEST + GROUP BY 없이 배열을 그 자리에서 변환·필터·집계할 수 있습니다.
  • 페더레이션 쿼리catalog.schema.table 표기만으로 이종 시스템을 한 SQL 에 묶습니다. ETL 없이 바로 분석이 가능해지죠.
  • approx_distinct / approx_percentile 같은 근사 집계 함수는 대규모 데이터에서 정확한 집계보다 훨씬 빠르고 메모리 효율적입니다.
  • WHERE 절에서 컬럼을 함수나 UDF 로 감싸면 pushdown 과 파티션 프루닝이 깨집니다. 필터 조건에는 반드시 표준 범위 비교를 쓰세요.

이 패턴들을 하나씩 실제 쿼리에 적용해 보면, 복잡한 파이프라인 코드가 깔끔한 SQL 몇 줄로 줄어드는 경험을 하게 될 겁니다.


이 글은 Trino 440번대 기준으로 작성되었습니다. 복잡한 분석 쿼리 설계나 SQL 성능 튜닝이 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀