Show HN: Trilogy – 재사용 가능하고 조합 가능한 SQL 실험
요약
본 글은 재사용 가능하고 조합 가능한 SQL 실험 구문인 Trilogy를 소개하며, TPC-DS 데이터셋을 활용한 벤치마킹 데모를 제공합니다. Trilogy는 Postgres, BigQuery, Snowflake 등 다양한 백엔드에 구애받지 않는(database-agnostic) 방식으로 작동하여 개발 유연성을 높입니다. 모델링은 개념과 관계를 정의하는 핵심 과정이며, `trilogy integration` 명령을 통해 데이터베이스의 준수 여부 검증 및 드리프트 감지에 활용될 수 있습니다.
핵심 포인트
- Trilogy는 다양한 DB에 구애받지 않는(database-agnostic) SQL 실험 구문입니다.
- TPC-DS를 사용하여 벤치마킹 데모가 가능하며, 재사용성을 입증합니다.
- 모델링은 데이터의 개념과 관계를 정의하는 핵심 과정이자 계약 역할을 합니다.
- `trilogy integration` 명령으로 데이터베이스 준수 여부 검증 및 드리프트 감지가 가능합니다.
Demo - TPC-DS 데이터셋 탐색
이 데모는 데이터베이스 성능 벤치마킹에 자주 사용되는 인기 있는 TPC-DS 데이터셋을 사용합니다.
여기서는 이를 조금 다르게 사용할 것입니다. DuckDB를 사용하여 간단한 확장 기능을 통해 TPC-DS로부터 대표적인 데이터 웨어하우스를 생성할 수 있으며, 우리는 이것을 활용하여 Trilogy 구문을 탐색할 것입니다. Trilogy와 이 벤치마크 사용에 대한 자세한 내용은 여기에서 읽을 수 있습니다.
팁
이 데모는 콜드 스타트(cold-start)되는 데 몇 초가 걸릴 수 있는 백엔드 서비스를 사용합니다. 후속 쿼리는 빨라야 합니다. 이 서비스는 DuckDB를 사용합니다. Trilogy는 데이터베이스에 구애받지 않습니다 (database-agnostic): Postgres, BigQuery, Snowflake와 같은 모든 백엔드에서 동일한 구문이 작동합니다.
TPC-DS는 깔끔하고 정돈된 웨어하우스를 제공합니다: 17개의 차원 테이블(dimension tables)과 7개의 사실 테이블(fact tables).
작업할 것이 정말 많습니다! 다행히도 누군가가 이미 TPC-DS에 대한 Trilogy 모델을 여기에서 정의해 두었으며, 우리는 그것을 직접 사용할 것입니다.
Trilogy의 imports에 초점을 맞춰봅시다
. 이들은 일반적으로 사실 테이블(fact tables)과 밀접하게 매핑됩니다—설계상 그렇습니다. 쿼리는 종종 사실 테이블을 중심으로 이루어지므로, 모델링을 위한 자연스러운 진입점입니다.
여기서 보여주는 쿼리에서는 import 구문을 숨길 것입니다—이들이 이미 환경에 로드되었다고 가정할 수 있습니다.
팁
아래 섹션에서는 실제 Trilogy 쿼리를 실행해 볼 기회가 주어집니다. 각 쿼리 아래의 깜빡이는 '실행(run)' 버튼을 클릭하여 결과를 확인하고 SQL을 살펴보세요.
기본 Select
select
customer.id,
customer.full_name,
...
유사한 데이터의 다른 뷰 (View)
# 모든 주문에 유효한 고객 ID가 있는 것은 아니므로, 유효한 ID를 가진 것만 살펴보겠습니다
where store_sales.customer.id is not null
select
...
고객별 판매액 (Sales By Customer)
where store_sales.customer.state is not null
select
store_sales.customer.id,
...
앞서 우리는 이러한 쿼리를 실행할 수 있게 해주는 모델에 대해 언급했습니다. 좀 더 자세히 살펴보겠습니다.
모델링은 Trilogy의 핵심이자 예술입니다; 데이터에 대한 직관적인 접근을 가능하게 하는 이름과 관계를 정의하는 곳입니다.
모델은 개념을 정의하고 바인딩하며, 종종 다른 모델(역할극 차원 등)을 가져옵니다. 테이블과 1:1 대응될 수도 있지만, 반드시 그럴 필요는 없으며, 이는 로직 리팩토링, 확장 또는 단순화에 유용합니다.
팁
Trilogy는 쿼리 및 모델링에 동일한 언어를 사용합니다. SQL처럼 임시 워크플로우의 일부로 모델을 정의하거나 확장할 수 있습니다. 모델은 Trilogy 개념과 기본 데이터 간의 관계를 정의하며, 쿼리를 생성하기 위한 계약(contract) 역할을 합니다. 실제로 모델은 검증에 사용될 수 있습니다. trilogy integration 명령은 데이터베이스가 모델을 준수하는지 검증합니다. 이는 작성 단계에서 데이터베이스를 표현했는지 확인하거나, 운영 환경에서 드리프트(drift)를 감지하는 데 사용할 수 있습니다.
여기에 우리가 쿼리해 온 store_sales 모델이 있습니다. 전체 TPC-DS 모델에는 이와 같은 파일들이 많이 포함되어 있습니다.
import std.money; # usd와 같은 풍부한 통화 유형
import item as item;
import date as date;
...
상단을 보시면 가져오기(imports)가 있습니다. 이것이 Trilogy에서 재사용의 핵심입니다. 모델의 조합을 가능하게 합니다.
다른 상태에 따른 반품
where store_sales.return_customer.state is not null
and store_sales.return_customer.state != store_sales.customer.state
select
...
실제로 고객(customers)으로 파고들면, 해당 모델 자체에 가져오기가 있다는 것을 알 수 있습니다. 이 전체 확장된 모델은 두 번 가져와졌습니다. 한 번은 구매한 고객을 위해, 다른 한 번은 반품한 고객을 위해 사용되어, 동일한 로직이 두 키를 가로질러 쉽게 재사용될 수 있도록 합니다.
재사용 외에도 SQL은 추상화(abstraction)의 이점을 얻을 수 있습니다.
테이블을 리팩토링함에 따라 데이터 소스는 소비하는 쿼리에 투명하게 진화하고 분할될 수 있습니다.
심지어 동일한 출력을 가진 상위 레벨 테이블로 계산할 때(예: 세션이 쿼리 결과를 저장하기 위해 persist를 실행할 때) 집계(aggregates)를 동적으로 교체하는 데 이 기능을 사용할 수도 있습니다.
import customer_demographic as demographics;
key id int;
unique property id.text_id string;
...
파생(derivation), 집계(aggregation), 필터링(filtering)을 깊이 파고들어 보겠습니다. `store_sales`부터 시작합니다.
### 상태별 집계 (State Aggregation)
select
store_sales.customer.state,
sum(store_sales.sales_price) as total_sales,
...
### 혼합 집계 (Mixed aggregate)
import std.display;
select
--store_sales.customer.id,
...
### 필터링 (Filtering)
import std.display;
WHERE store_sales.customer_demographic.education_status = 'College'
SELECT
...
### 미묘한 필터 (Nuanced Filter)
import std.display;
SELECT
store_sales.customer.state,
...
### 품목별 총 매장 판매액을 기준으로 주(State)별 평균 순위 찾기
with ranked_states as
select
store_sales.item.name,
...
준비되셨나요? 쿼리를 직접 시도해 보세요!
팁 (Tips)
기본적인 쿼리의 경우, Trilogy는 SQL과 거의 동일해야 합니다. 확신이 서지 않을 때는 SQL 구문을 사용해 보세요!
- CA 주에서 연도별 판매액은 얼마였나요? >
- CA 주의 평균 연간 판매액은 얼마였나요? >
- 2001년에 가장 많은 매장 판매액을 기록한 고객 인구 통계학적 그룹은 무엇이며, 하와이에서의 해당 그룹 판매액은 얼마였나요? >
- 주문 건수가 5건을 초과하는 고객들이 매사추세츠(Massachusetts)와 켄터키(Kentucky)에서 가장 많이 구매한 품목은 무엇이었나요? >
다음 개념들은 미리 정의되어 있으며 이름으로 참조할 수 있습니다.
합리적으로 보여줄 수 있는 것보다 더 많은 개념이 존재합니다. 영감을 얻으려면 이 상자에서 검색해 보세요:
경고 (Warning)
우선은 동일한 루트를 가진 개념(예: store_sales.x 및 store_sales.y)을 쿼리하는 것에 집중하세요. 아니면, 모형들을 병합하여 교차 네임스페이스 쿼리를 활성화하는 방법을 알아보려면 계속 읽어보세요.
저희는 매장 판매액에 재미를 느꼈지만, 데이터셋의 나머지 부분은 어떨까요?
일반적인 Trilogy 스크립트는 기존 모델을 가져오는 것(importing)을 기반으로 하며, 이 모델들 자체도 가져오기를 포함할 수 있습니다. 저희는 store_sales가 서로 다른 역할 놀이 차원(role-playing dimensions) 하에서 동일한 고객 모델을 어떻게 가져오는지 확인했습니다.
하지만 때로는 예상치 못한 방식으로 데이터를 병합해야 할 필요가 있습니다. SQL은 유연합니다. 임의의 키를 선택하여 병합할 수 있습니다.
두 개의 모델을 연결하고 싶다면 어떻게 할 수 있을까요?
Trilogy는 조인(joins) 기능을 제공합니다. 하지만 테이블이 없다면, 조인이란 무엇일까요?
Trilogy에서 조인은 의미론적 집합 연산(semantic set operation)을 나타냅니다. 두 개의 독립적인 개념을 더 큰 전체의 부분집합으로 선언할 수 있습니다—이를 "full outer join"이라고 부르며, 우리는 이를 "union join"이라고 부릅니다.
또는 하나의 개념 집합이 다른 개념의 부분집합임을 선언할 수도 있습니다—이는 "left outer join"이며, 우리는 이를 "subset"이라고 부릅니다.
진단 또는 임시적인 이유로 비정상적인 키(unnatural keys)를 가진 필드를 연결하고 싶을 수도 있습니다. 이것은 그러한 기능을 쿼리 범위 내에서 수행할 수 있게 해줍니다.
팁
조인은 표현식(expressions)에 대해 작동할 수 있습니다! `union join x + 1 = x`는 하나의 개념 범위 내 값들 간의 조합을 생성하는 유효한 방법입니다.
쿼리 범위 내 조인은 select 목록 뒤에 배치하십시오. subset, union 및 composite-key 예시는 join 참조를 참고하십시오.
예시로, 아마도 귀하의 의미론적 모델은 "Edgewood 지역에서 이 소득 범위에 있는 고객 중, 오프라인 매장에서 구매한 상품을 반품한 사람들과 동일한 인구통계학적 그룹을 가진 고객은 누구인가?"라는 질문에 답하도록 설정되어 있지 않았을 것입니다.
조인은 SQL에서처럼 이러한 임시 교집합(ad hoc intersection)을 쉽게 표현할 수 있게 해줍니다.
import customer as customer;
import store_sales as ss;
where
...
팁
쿼리 수준의 조인(Query-level joins)은 쿼리의 범위에만 지속됩니다. 이는 유용한 탐색 도구이지만, 소비를 위해 모델을 구조화할 때는 일반적으로 사전 의미론적 동등성 모델링(upfront semantic equivalence modeling) 및 부분 바인딩(partial bindings)을 선호하여 이를 피하는 것이 좋습니다.
개념 연결이 쿼리를 넘어 재사용 가능한 방식으로 지속되어야 한다면, 모델 수준 병합(model-level merge)을 사용할 수 있습니다. 모델 수준 병합은 개념적으로 조인과 동일하지만, 모델을 사용하는 모든 select에 기본적으로 적용되며 독립적인 구문으로 수정된 문법을 가집니다.
이것은 Trilogy에게 이 두 필드가 "같다"고 알려줍니다. 예를 들어, 판매 데이터셋과 공휴일 데이터셋이 있다고 가정해 봅시다. 이들은 다음과 같은 필드를 가질 수 있습니다:
Sales: 'order_date', 'ship_date', 'returned_date', 'order_id'
Holidays: 'date', 'holiday_name'
휴일 동안 주문된 판매 내역을 알고 싶다면, holidays의 date를 sales.order_date에 병합(merge)하면 됩니다. 그러면 이제 `select holiday_name, count(order_id)`와 같이 쉽게 쿼리할 수 있습니다.
만약 휴일에 배송된 주문 내역을 보고 싶다면, ship date 기준으로 병합하면 되고—두 가지 모두를 쿼리할 수 있도록 하려면, holidays 데이터셋을 두 개의 다른 이름으로 가져와 독립적으로 병합할 수 있습니다.
Model merges는 dataset bindings와 동일한 수정자(modifier) 구문을 사용합니다: `merge a into ~b;`
이는 a가 b의 부분집합임을 의미합니다 (부분집합 조인과 유사).
팁
유일하게, model merges는 `merge a into b;`를 *정확한* 동등성 연산(exact equivalence operation)으로 지원합니다. a 또는 b를 요청하는 모든 것은 두 테이블에 바인딩된 데이터로부터 충족될 수 있습니다. 주의하세요! 이것이 유효하려면 두 개념이 정확히 동일한 범위를 가져야 합니다.
하나로 병합: `MERGE <concept1> into <modifiers?><concept2>`
여러 개로 병합: `MERGE <namespace1>.* into <modifiers?><namespace2>.*`
둘 다 시도해 봅시다:
### Union Join
UNION JOIN web_sales.date.year = store_sales.date.year
SELECT
coalesce(store_sales.date.year, web_sales.date.year) as report_year,
...
### Model Merge
MERGE store_sales.date.* into ~date.;
MERGE web_sales.date. into ~date.*;
SELECT
...
팁
여러 병합 구문(merge statements)을 두 모델 사이에 정의할 수 있습니다. 쿼리는 쿼리에서 참조된 모든 개념에 걸쳐 병합됩니다.
대시보드를 구동하기 위해 쿼리를 물질화(materialize)하고 싶다고 상상해 보세요. 선호되는 패턴은 그 결과를 관리형 데이터 소스(managed datasource)로 설명하고, Trilogy가 언제 재구축해야 하는지 결정하도록 하는 것입니다.
외부에서 관리되는 표준 입력(canonical inputs)을 `root`로 표시하세요.
각 파생 데이터 소스(derived datasource)에 대해 `freshness by`를 사용하세요.
물표(watermark)를 식별하여 Trilogy가 상위 입력값과 비교할 수 있도록 합니다. 데이터 소스는 시맨틱 모델의 일부로 유지되며, 물리적 테이블은 원본 물표가 진행될 때마다 재생성될 수 있는 캐시로 취급됩니다.
팁
Trilogy는 또한 명령형 업데이트(imperative updates)와 외부 호출을 제공합니다. 따라서 사용 사례에 가장 적합한 방식으로 선언적(declarative), 자산 기반 새로고침과 명시적 실행을 혼합하여 사용할 수 있습니다.
key customer_id int;
property customer_id.sold_at datetime;
property <customer_id, sold_at>.sales_price float;
...
하나의 파일 또는 모델 디렉터리에 대해 새로고침을 실행합니다:
trilogy refresh .
trilogy refresh . --dry-run
trilogy refresh . --interactive
`refresh`
모델 의존성 그래프를 스캔하고, 데이터 소스 물표를 비교하며, 오래된 관리 자산만 재구축합니다. 추가(append)-지향 모델의 경우 `freshness by` 대신 `incremental by`를 사용하십시오.
팁
실제로는 대부분의 웨어하우스가 보고, 분석 및 성능을 구동하기 위해 주기적으로 새로고침되는 유한한 수의 '루트'와 그로부터 파생된 여러 캐시를 갖게 됩니다. Trilogy는 이러한 캐시들을 데이터 소스로 명시적으로 정의하고 `refresh` 명령어를 통해 관리할 수 있게 합니다.
`persist`
일회성 쿼리의 출력을 의도적으로 저장하려는 경우 여전히 유용합니다. 반복 가능한 파이프라인의 경우, 관리되는 데이터 소스와 `refresh`를 선호하십시오. 이렇게 하면 정의(definition), 계보(lineage), 그리고 새로고침 정책이 모델 내에서 함께 존재하게 됩니다.
오늘의 주제입니다. 실망시키지 않겠습니다: Trilogy는 에이전트와 매우 잘 작동합니다.
Claude Code 또는 다른 어떤 하네스라도 Trilogy CLI를 가리키기만 하면 됩니다. `trilogy agent-info`는 자체 포함된 안내서를 제공하도록 설계되었으며, 전역 `--output-format json --agent` 옵션은 에이전트에 적합한 실패 동작과 함께 기계가 읽을 수 있는 출력을 제공합니다. Studio는 또한 서브프로세스보다 도구 프로토콜을 선호하는 클라이언트를 위해 MCP 서버도 제공합니다.
CLI에는 또한 큐레이션된 Trilogy 도구 세트를 갖춘 네이티브 에이전트 루프가 포함되어 있습니다. 구성된 Trilogy 작업 공간에서 단일 쿼리 대신 결과를 전달하세요:
trilogy agent "판매 추세를 분석하고 대시보드를 생성해줘"
trilogy agent -i "웹 판매 감소 원인을 조사해줘"
대화 내용은 기본적으로 저장됩니다. CLI는 제어권을 반환할 때 세션 ID를 출력하므로, 나중에 같은 컨텍스트로 명령을 계속할 수 있습니다:
trilogy agent --list-sessions
trilogy agent --resume last "이제 이걸 월별로 분리해줘"
trilogy agent -r <세션-id> "결과를 차트로 만들어줘"
Provider 및 모델 기본값은 `trilogy.toml`의 `[agent]` 섹션에 속합니다.
; `--provider`
그리고 `--model`로 단일 호출 시 이를 재정의할 수 있습니다. API 키는 provider의 환경 변수에서 읽어옵니다. `--context <경로>`를 사용하여 컨텍스트 파일을 추가하고, 일시적인 세션을 위해 `--no-save`를 사용하며, 전체 에이전트 기반 CLI 및 언어 레퍼런스를 위해 `trilogy agent-info`를 실행하세요.
아래에서 TPC-DS 데이터셋 쿼리 실험을 해볼 수 있습니다 - 단발성 쿼리로!
언어와 철학에 대한 더 깊은 탐색을 원한다면 concepts 페이지로 이동하세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 HN AI Engineering의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기