GaiaExGaiaEx
금융 데이터 웨어하우스를 위한 SQL
개발자프로그래밍9 min read

금융 데이터 웨어하우스를 위한 SQL

정형화된 금융 데이터를 조회, 집계, 분석하는 방법

게시물 공유

SQL이 금융 데이터의 언어인 이유

모든 거래, 모든 틱, 모든 오더북 업데이트 — 금융 시장은 엄청난 양의 구조화된 데이터를 만들어냅니다. 그리고 40년이 넘도록, SQL(Structured Query Language)은 그 데이터를 조회하고, 변환하고, 분석하는 데 지배적인 도구였습니다. 화려하지도, 트렌디하지도 않습니다. 하지만 포트폴리오 매니저가 “지난 화요일 UTC 오후 2시에서 3시 사이 ETH 거래의 평균 체결가는 얼마였나?”라고 물으면, 그 답은 SQL에서 나옵니다.

금융 데이터는 본질적으로 관계형(relational)입니다. 거래는 주문을 참조하고, 주문은 계정을 참조하고, 계정은 사용자에게 속합니다. 포지션은 상품과 관계가 있고, 상품은 시장과 관계가 있습니다. 관계형 모델 — 외래키로 연결된 행과 열로 이루어진 테이블 — 은 이런 관계에 자연스럽게 대응됩니다. 이것이 PostgreSQL, MySQL, SQL Server 같은 관계형 데이터베이스가 Goldman Sachs부터 GaiaEx까지 금융 인프라의 뼈대로 남아 있는 이유입니다.

SQL의 힘은 그 선언적(declarative) 성격에 있습니다: 여러분은 무엇을 원하는지를 기술할 뿐, 어떻게 얻을지는 기술하지 않습니다. 데이터베이스 엔진이 최적의 실행 계획을 알아냅니다. 이는 거래 기록 테이블에 5억 개의 행이 있고 1초 이내에 결과가 필요할 때 엄청나게 중요해집니다. 올바른 쿼리를 작성하고, 올바른 인덱스를 구축하면, SQL은 결과를 내놓습니다 — 로컬 PostgreSQL 인스턴스에서 실행하든, 클라우드 규모의 데이터 웨어하우스에서 실행하든 상관없이 말입니다.

Relational model (simplified) users user_id PK accounts user_id FK → users tier, limits … trades user_id FK → accounts symbol, price, time … JOINs follow foreign keys — the shape of finance data is already relational.
거래는 계정과 사용자에 매달려 있습니다: SQL의 JOIN이 따라가는 것과 같은 연결입니다.

SELECT, JOIN, WHERE: 거래 데이터 조회하기

구체적인 스키마부터 시작해 봅시다. GaiaEx 같은 플랫폼의 trades 테이블을 상상해 보세요:

CREATE TABLE trades (
    trade_id    BIGINT PRIMARY KEY,
    user_id     INT NOT NULL,
    symbol      VARCHAR(20) NOT NULL,
    side        VARCHAR(4) NOT NULL,  -- 'buy' or 'sell'
    price       NUMERIC(18,8) NOT NULL,
    quantity    NUMERIC(18,8) NOT NULL,
    fee         NUMERIC(18,8) DEFAULT 0,
    executed_at TIMESTAMPTZ NOT NULL
);

가장 근본적인 연산은 필터링을 동반한 SELECT 문입니다. 지난 24시간 동안 $3,000 이상의 모든 ETH-USD 매수 거래를 찾으려면:

SELECT trade_id, price, quantity, executed_at
FROM trades
WHERE symbol = 'ETH-USD'
  AND side = 'buy'
  AND price > 3000
  AND executed_at > NOW() - INTERVAL '24 hours'
ORDER BY executed_at DESC;

JOIN은 관련된 테이블들을 연결합니다. accounts 테이블이 있고 계정 등급별 거래량을 보고 싶다고 가정해 봅시다:

SELECT a.tier, COUNT(*) AS trade_count,
       SUM(t.price * t.quantity) AS total_volume
FROM trades t
JOIN accounts a ON t.user_id = a.user_id
WHERE t.executed_at > NOW() - INTERVAL '30 days'
GROUP BY a.tier
ORDER BY total_volume DESC;

이러한 쿼리들은 금융 분석가, 리스크 팀, 컴플라이언스 담당자들의 일상 업무입니다. 이를 능숙하게 다루면 여러분의 데이터가 담고 있는 어떤 질문에도 답할 수 있습니다.

윈도우 함수: 분석가의 비밀 무기

윈도우 함수(Window functions)는 금융 분야에서 SQL 초보자와 SQL 숙련자를 가르는 기준입니다. 이는 결과를 하나의 집계된 값으로 축소하지 않으면서, 현재 행과 관련된 행들의 집합에 걸쳐 계산을 수행할 수 있게 해줍니다. 이를 데이터에 걸친 “실행 중인 계산(running computation)”이라고 생각하세요.

금융 분석에서 가장 흔히 쓰이는 윈도우 함수들:

  • ROW_NUMBER() — 파티션 내에서 각 행에 순차적인 정수를 할당합니다. 중복 제거나 심볼당 N번째 거래를 선택하는 데 유용합니다.
  • LAG()와 LEAD() — 이전 또는 다음 행의 값에 접근합니다. 거래 간 수익률을 계산하거나 시계열 데이터의 간극을 탐지하는 데 필수적입니다.
  • SUM() OVER () — 누적 합계. 누적 거래량, 누적 손익, 실행 중인 포지션 규모를 계산합니다.
  • AVG() OVER (ROWS BETWEEN) — SQL 안에서 직접 계산하는 이동평균. 파이썬이 필요 없습니다.

실용적인 예시입니다 — BTC의 거래 간 가격 변화와 실행 중인 포지션을 계산해 봅시다:

SELECT executed_at, price, quantity, side,
       price - LAG(price) OVER (ORDER BY executed_at) AS price_change,
       SUM(CASE WHEN side = 'buy' THEN quantity ELSE -quantity END)
           OVER (ORDER BY executed_at) AS running_position
FROM trades
WHERE symbol = 'BTC-USD'
ORDER BY executed_at;

윈도우 함수는 행 개수를 줄이지 않습니다 — GROUP BY와 달리, 모든 입력 행이 출력 행을 만듭니다. 이는 세부 거래 로그의 그레인(granularity)을 잃지 않으면서 분석용 컬럼을 추가하는 데 완벽합니다. 데이터를 “요약”하는 것이 아니라 “보강”한다고 생각하세요.

캔들스틱 차트의 기초인 OHLCV 집계를 위해서는, GROUP BY를 시간 버킷팅과 표준 집계 함수와 결합할 수 있습니다:

SELECT date_trunc('hour', executed_at) AS bucket,
       (ARRAY_AGG(price ORDER BY executed_at))[1] AS open,
       MAX(price) AS high,
       MIN(price) AS low,
       (ARRAY_AGG(price ORDER BY executed_at DESC))[1] AS close,
       SUM(quantity) AS volume
FROM trades
WHERE symbol = 'ETH-USD'
GROUP BY bucket
ORDER BY bucket;

CTE: 복잡한 분석 쿼리 구성하기

공통 테이블 표현식(Common Table Expressions, CTE)은 복잡한 쿼리를 이름이 붙은, 읽기 쉬운 단계들로 나눌 수 있게 해줍니다 — 프로그래밍의 함수와 비슷합니다. WITH 키워드로 도입되며, 순차적으로 서로를 참조할 수 있습니다.

가장 수익성이 높은 상위 10개 거래일을 식별하고 싶지만, 수익성 계산에는 수수료를 포함한 일일 순손익 계산이 필요하다고 가정해 봅시다. CTE를 사용하면 단계별로 구축할 수 있습니다:

WITH daily_trades AS (
    SELECT DATE(executed_at) AS trade_date,
           SUM(CASE WHEN side = 'sell' THEN price * quantity
                    ELSE -price * quantity END) AS gross_pnl,
           SUM(fee) AS total_fees
    FROM trades
    WHERE user_id = 42
    GROUP BY DATE(executed_at)
),
daily_pnl AS (
    SELECT trade_date,
           gross_pnl - total_fees AS net_pnl,
           SUM(gross_pnl - total_fees) OVER (ORDER BY trade_date) AS cumulative_pnl
    FROM daily_trades
)
SELECT trade_date, net_pnl, cumulative_pnl
FROM daily_pnl
ORDER BY net_pnl DESC
LIMIT 10;

각 CTE는 하나의 문단처럼 읽힙니다. daily_trades는 원시 거래를 일일 요약으로 집계합니다. daily_pnl은 순손익과 누적 합계를 계산합니다. 마지막 SELECT는 상위 10일을 골라냅니다. 이를 하나의 거대한 단일 서브쿼리와 비교해 보면 — CTE 버전은 유지보수하기 쉽고, 테스트하기 쉽고, 스스로 설명이 됩니다.

실제 운영 중인 금융 시스템에서 CTE는 규제 보고(계정 전반의 마진 요구사항 집계)부터 실시간 대시보드 쿼리(GaiaEx에서 롤링 24시간 거래량 계산)까지 모든 곳에서 사용됩니다. 이들은 잘 구조화된 코드가 구성되는 것과 같은 방식으로 구성됩니다 — 각 레이어가 이전 레이어 위에 쌓입니다.

시계열 성능을 위한 인덱싱과 파티셔닝

쿼리는 그것을 지원하는 인덱스만큼만 빠릅니다. 올바른 인덱스가 없으면, 단순한 WHERE 절조차 순차 스캔(sequential scan)을 강제합니다 — 테이블의 모든 행을 읽는 것입니다. 5억 개의 행이면, 이는 밀리초가 아니라 몇 분이 걸립니다.

금융 시계열 데이터에서 가장 중요한 인덱스 패턴은 (symbol, executed_at)에 대한 복합 B-트리 인덱스입니다:

CREATE INDEX idx_trades_symbol_time
ON trades (symbol, executed_at DESC);

이 하나의 인덱스는 대부분의 분석 쿼리를 가속화합니다: “지난 한 시간의 모든 ETH 거래”, “두 타임스탬프 사이의 BTC 거래”, 또는 “심볼별 최신 거래”. 컬럼 순서가 중요합니다 — symbol이 먼저 오면 동등 필터링이 가능해지고, 그다음 executed_at이 그 인덱스 파티션 내에서 효율적인 범위 스캔을 가능하게 합니다.

수억 개의 행을 넘어서는 테이블에는 테이블 파티셔닝이 필수적입니다. PostgreSQL은 범위 기준 선언적 파티셔닝을 지원하는데 — 시계열에 완벽합니다:

CREATE TABLE trades (
    trade_id BIGINT, symbol VARCHAR(20),
    price NUMERIC(18,8), executed_at TIMESTAMPTZ
) PARTITION BY RANGE (executed_at);

CREATE TABLE trades_2026_q1 PARTITION OF trades
    FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE trades_2026_q2 PARTITION OF trades
    FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');

파티셔닝을 사용하면, 2026년 3월 데이터에 대한 쿼리는 Q1 파티션만 스캔합니다 — 엔진은 Q2, Q3, Q4를 완전히 건너뜁니다. 파티션 프루닝(partition pruning)이라 불리는 이 기법은 대규모 과거 데이터셋에서 자릿수 단위의 속도 향상을 가져다줍니다.

Time-range partition pruning Q1 2026 Jan–Mar rows Q2 2026 skipped Q3 2026 Q4 2026 Query: WHERE executed_at in March → planner reads only Q1 Composite index (symbol, executed_at) still helps within each partition
파티션은 시간 범위를 경계 짓고, 프루닝은 일치할 수 없는 청크 전체를 건너뜁니다.

PostgreSQL 대 ClickHouse 대 BigQuery: 올바른 엔진 선택하기

모든 데이터베이스가 금융 분석을 똑같이 처리하지는 않습니다. 선택은 여러분의 쿼리 패턴, 데이터 규모, 지연 시간 요구사항에 달려 있습니다.

PostgreSQL은 만능 일꾼입니다. 트랜잭션 워크로드(실시간 거래 기록)에서 탁월하고, ACID 보장을 지원하며, 복잡한 JOIN을 매끄럽게 처리하고, 적절한 인덱싱과 파티셔닝을 갖추면 수억 개의 행까지 확장됩니다. 운영 데이터베이스 — 시스템 오브 레코드(system of record) — 에 적합한 선택입니다. 예를 들어 GaiaEx는 금융 시스템이 요구하는 신뢰성 보장을 갖춘 PostgreSQL 호환 인프라에 핵심 거래 데이터를 의존합니다.

ClickHouse는 수십억 개 행에 대한 분석 쿼리를 위해 설계된 컬럼형 데이터베이스입니다. PostgreSQL이 데이터를 행 단위로 저장하는 반면(개별 거래를 삽입하는 데 훌륭함), ClickHouse는 데이터를 열 단위로 저장합니다(수백만 개 행에 걸쳐 단일 컬럼을 집계하는 데 훌륭함). PostgreSQL에서 30초가 걸리는 “3년간 BTC의 시간당 평균 거래량” 같은 쿼리가 ClickHouse에서는 200밀리초에 끝날 수 있습니다. 트레이드오프는: ClickHouse는 UPDATE나 DELETE를 효율적으로 지원하지 않습니다 — 설계상 추가만 가능(append-only)합니다.

BigQuery(Google Cloud)는 서버리스 컬럼형 웨어하우스입니다. 관리할 인프라가 없고, 쿼리당 과금이며, 페타바이트 규모의 용량을 갖습니다. 임시 분석, 연구, 탐색에 이상적입니다 — 하지만 쿼리 지연 시간이 밀리초가 아니라 초 단위로 측정되므로, 실시간 애플리케이션에는 적합하지 않습니다.

  • 실시간 거래 기록 및 주문 관리 → PostgreSQL
  • 수십억 행에 걸친 과거 분석 및 백테스팅 → ClickHouse
  • 임시 탐색 및 팀 간 데이터 공유 → BigQuery

많은 전문 트레이딩 회사들은 계층화된 아키텍처에서 이 세 가지를 모두 사용합니다: 뜨거운 운영 저장소로 PostgreSQL, 따뜻한 분석 레이어로 ClickHouse, 차가운 아카이브로 BigQuery입니다. 좋은 SQL을 작성하는 능력은 이 세 가지 모두에 그대로 이전됩니다 — 문법은 90%가 동일하고, 분석적 사고는 100% 동일합니다.