GaiaEx AcademyGaiaEx Academy
SQL para sa Financial Data Warehouses
DeveloperProgramming9 min read

SQL para sa Financial Data Warehouses

Query, aggregate, at analisahin ang structured financial data

Ibahagi ang mga Post

Bakit SQL ang Wika ng Financial Data

Bawat trade, bawat tick, bawat order book update — gumagawa ang financial markets ng nakamamanghang volume ng structured data. At sa loob ng mahigit apat na dekada, ang SQL (Structured Query Language) ang naging dominanteng tool para sa pag-query, pag-transform, at pag-analisa ng datos na iyon. Hindi ito flashy. Hindi ito trendy. Pero kapag tinanong ng isang portfolio manager na “Ano ang average fill price ko sa ETH trades noong huling Tuesday sa pagitan ng 2 PM at 3 PM UTC?”, mula sa SQL ang sagot.

Likas na relational ang financial data. Tumutukoy ang isang trade sa isang order, na tumutukoy sa isang account, na pag-aari ng isang user. Nauugnay ang positions sa instruments; nauugnay ang instruments sa markets. Ang relational model — mga tables na may rows at columns, na nag-uugnay gamit ang foreign keys — natural na nag-map sa mga relasyon na ito. Ito ang dahilan kung bakit ang mga relational database gaya ng PostgreSQL, MySQL, at SQL Server ang nananatiling backbone ng financial infrastructure, mula sa Goldman Sachs hanggang GaiaEx.

Ang kapangyarihan ng SQL ay nakasalalay sa declarative na kalikasan nito: inilalarawan mo ano ang gusto mo, hindi paano ito makukuha. Alamin na ng database engine ang optimal na execution plan. Sobrang mahalaga ito kapag ang trade history table mo ay may 500 milyong rows at kailangan mo ng resulta sa mababa sa isang segundo. Isulat ang tamang query, buuin ang tamang index, at maghahatid ang SQL — kahit patakbo ka man ito sa isang local PostgreSQL instance o sa isang cloud-scale data warehouse.

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.
Nauugnay ang trades sa accounts at users: ang parehong link na tinatraverse ng iyong SQL JOINs.

SELECT, JOIN, at WHERE: Pag-query ng Trade Data

Magsimula tayo sa isang concrete schema. Isipin mo ang isang trades table sa isang platform gaya ng GaiaEx:

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
);

Ang pinaka-pundamental na operasyon ay ang SELECT statement na may filtering. Para hanapin ang lahat ng ETH-USD buy trades na higit sa $3,000 sa nakaraang 24 oras:

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;

Kumokonekta ang JOINs sa mga related tables. Ipagpalagay na may accounts table ka at gustong makita ang trading volume kada account tier:

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;

Ang mga query na ito ang pang-araw-araw na tinapay ng financial analysts, risk teams, at compliance officers. Bihasahin ang mga ito at masasagot mo ang kahit anong tanong na nasa datos mo.

Window Functions: Ang Sikretong Sandata ng Analyst

Ang Window functions ang naghihiwalay sa SQL beginners mula sa SQL practitioners sa finance. Pinapayagan ka nitong magsagawa ng mga kalkulasyon sa isang set ng rows na nauugnay sa kasalukuyang row — nang hindi bumagsak ang resulta sa isang solong aggregated na value. Isipin mo itong parang “running computations” sa iyong datos.

Ang pinaka-karaniwang gamit na window functions sa financial analysis:

  • ROW_NUMBER() — Nagbibigay ng sunod-sunod na integer sa bawat row sa loob ng isang partition. Kapaki-pakinabang para sa deduplication o sa pagpili ng Nth trade kada symbol.
  • LAG() at LEAD() — Kunin ang value ng nakaraang o susunod na row. Mahalaga para sa pag-compute ng trade-to-trade returns o pag-detect ng gaps sa time-series data.
  • SUM() OVER () — Running totals. I-compute ang cumulative volume, cumulative P&L, o running position size.
  • AVG() OVER (ROWS BETWEEN) — Moving averages direkta sa SQL. Hindi na kailangan ng Python.

Ito ang isang praktikal na halimbawa — pag-compute ng trade-to-trade price change at running position para sa 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;

Hindi binabawasan ng window functions ang bilang ng iyong rows — hindi tulad ng GROUP BY, ang bawat input row ay gumagawa ng isang output row. Ito ang gumagawa sa mga ito na perpekto para magdagdag ng analytical columns sa detalyadong trade logs nang hindi nawawala ang granularity. Isipin mo itong “pinayayaman” ang iyong datos kaysa “pinagbubuod” ito.

Para sa OHLCV aggregation — ang pundasyon ng candlestick charts — puwede mong pagsamahin ang GROUP BY sa time-bucketing at standard aggregates:

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;

CTEs: Pagbuo ng Kumplikadong Analytical Queries

Pinapayagan ka ng Common Table Expressions (CTEs) na hatiin ang isang kumplikadong query sa mga pinangalanan, madaling-basahing stages — tulad ng functions sa programming. Ipinapakilala ang mga ito gamit ang WITH keyword at puwedeng mag-reference sa isa't isa nang sunod-sunod.

Ipagpalagay na gusto mong tukuyin ang iyong top 10 pinaka-kumikitang araw ng trading, pero kinakailangan ng profitability ang pag-compute ng net P&L kada araw kasama ang fees. Sa CTEs, puwede mong buuin ito step by step:

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;

Ang bawat CTE ay nababasa gaya ng isang paragraph. Ang daily_trades ay nag-aggregate ng raw trades sa daily summaries. Ang daily_pnl ay nag-compute ng net P&L at running total. Ang final SELECT ay pumili ng top 10 araw. Ihambing ito sa isang solo, monolithic na subquery — ang CTE version ay maintainable, testable, at self-documenting.

Sa production financial systems, ginagamit ang CTEs para sa lahat mula sa regulatory reporting (pag-aggregate ng margin requirements sa lahat ng accounts) hanggang sa real-time dashboard queries (pag-compute ng rolling 24-oras na volume sa GaiaEx). Nagkakasama sila nang tulad ng pagsasama ng magandang-factored na code — ang bawat layer ay itinatayo sa ibabaw ng nauna.

Indexing at Partitioning para sa Time-Series Performance

Ang isang query ay kasing-bilis lang ng index na nagsusuporta dito. Kung wala ang tamang index, kahit isang simpleng WHERE clause ay pipilitin ang isang sequential scan — pagbabasa ng bawat row sa table. Sa 500 milyong rows, minuto ito sa halip na milliseconds.

Para sa financial time-series data, ang pinaka-kritikal na index pattern ay ang isang composite B-tree index sa (symbol, executed_at):

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

Ang solong index na ito ay pinabilis ang karamihan sa mga analytical queries: “lahat ng ETH trades sa nakaraang oras,” “BTC trades sa pagitan ng dalawang timestamps,” o “pinakahuling trade kada symbol.” Mahalaga ang order ng columns — ang symbol muna ang nagbibigay-daan sa equality filtering, tapos ang executed_at ang nagbibigay-daan sa episyenteng range scans sa loob ng partition na iyon ng index.

Para sa mga tables na lumalaki nang lampas sa daan-daang milyong rows, mahalaga ang table partitioning. Sinusuportahan ng PostgreSQL ang declarative partitioning ayon sa range — perpekto para sa time-series:

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');

Sa partitioning, ang isang query para sa datos ng March 2026 ay iiskan lang ang Q1 partition — ganap na nililaktawan ng engine ang Q2, Q3, at Q4. Ang teknik na ito, na tinatawag na partition pruning, ay naghahatid ng order-of-magnitude na pagbilis sa malaking historical datasets.

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
Ang partitions ay naglilimita ng time ranges; ang pruning ay nagliliskip ng buong chunks na hindi puwedeng tumugma.

PostgreSQL kumpara sa ClickHouse kumpara sa BigQuery: Pagpili ng Tamang Engine

Hindi pantay ang paghawak ng lahat ng databases sa financial analytics. Nakasalalay ang pagpili sa iyong query patterns, data volume, at latency requirements.

Ang PostgreSQL ang workhorse. Napakahusay ito sa transactional workloads (pagrekord ng trades sa real time), sinusuportahan ang ACID guarantees, mahusay na hinahawakan ang mga kumplikadong JOINs, at nag-scale sa daan-daang milyong rows na may tamang indexing at partitioning. Ito ang tamang pagpili para sa operational databases — ang system of record. Ang GaiaEx, halimbawa, ay umaasa sa PostgreSQL-compatible na infrastructure para sa core trading data nito na may reliability guarantees na kinakailangan ng financial systems.

Ang ClickHouse ay isang columnar database na idinisenyo para sa analytical queries sa bilyon-bilyong rows. Kung saan ang PostgreSQL ay nag-iimbak ng datos row by row (mahusay para sa pag-insert ng indibidwal na trades), ang ClickHouse ay nag-iimbak ng datos column by column (mahusay para sa pag-aggregate ng isang column sa milyon-milyong rows). Ang isang query gaya ng “average hourly volume para sa BTC sa loob ng 3 taon” na tumatagal ng 30 segundo sa PostgreSQL ay puwedeng matapos sa 200 milliseconds sa ClickHouse. Ang trade-off: hindi episyente ang UPDATE o DELETE sa ClickHouse — append-only ito ayon sa disenyo.

Ang BigQuery (Google Cloud) ay isang serverless na columnar warehouse. Walang infrastructure na kailangang pamahalaan, pay-per-query pricing, at petabyte-scale na kapasidad. Ideal ito para sa ad-hoc analytics, research, at exploration — pero ang query latency ay sinusukat sa segundo, hindi millisecond, na gumagawa nito na hindi angkop para sa real-time applications.

  • Real-time trade recording at order management → PostgreSQL
  • Historical analytics at backtesting sa bilyon-bilyong rows → ClickHouse
  • Ad-hoc exploration at cross-team na data sharing → BigQuery

Ginagamit ng maraming propesyonal na trading firms ang tatlo sa isang tiered architecture: PostgreSQL bilang hot operational store, ClickHouse bilang warm analytical layer, at BigQuery bilang cold archive. Ang kahusayan sa pagsulat ng magandang SQL ay applicable sa lahat ng ito — halos 90% pareho ang syntax, at 100% pareho ang analytical thinking.