
面向金融資料倉儲的 SQL
查詢、聚合並分析結構化金融資料
為什麼說 SQL 是金融資料的語言
每一筆成交、每一個 tick、每一次訂單簿更新——金融市場源源不斷地產生著海量結構化資料。而四十多年來,SQL(結構化查詢語言)一直是查詢、轉換和分析這些資料的主流工具。它不光鮮,也不時髦。但當一位投資組合經理問出「上週二 UTC 時間下午 2 點到 3 點之間,我的 ETH 成交均價是多少?」時,答案就來自 SQL。
金融資料天然是關係型的。一筆成交關聯著一張訂單,訂單關聯著一個帳戶,帳戶又歸屬於某個使用者。持倉關聯著標的;標的關聯著市場。關係模型——由行和列組成、並透過外來鍵相互連線的表——能自然地對映這些關係。正因如此,PostgreSQL、MySQL、SQL Server 這類關係型資料庫至今仍是金融基礎設施的骨幹,從 Goldman Sachs 到 GaiaEx 概莫能外。
SQL 的威力在於它的宣告式特性:你只需描述你想要什麼,而不必說明如何去拿到它。資料庫引擎會自行算出最優的執行計劃。當你的成交歷史表有 5 億行、卻需要在一秒內拿到結果時,這一點至關重要。寫對查詢、建對索引,SQL 就能交付結果——無論你是在本地的 PostgreSQL 例項上跑,還是在雲端規模的資料倉儲上跑。
SELECT、JOIN 與 WHERE:查詢成交資料
我們先從一個具體的 schema 開始。設想 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;
這些查詢是金融分析師、風險團隊和合規人員的家常便飯。把它們掌握好,你就能回答資料裡所蘊含的任何問題。
視窗函式:分析師的秘密武器
在金融領域,視窗函式正是區分 SQL 新手和 SQL 實戰派的分水嶺。它們讓你能在與當前行相關的一組行上進行計算——而不會把結果坍縮成單一的聚合值。可以把它們想象成在資料上「滾動運算」。
金融分析中最常用的視窗函式:
- ROW_NUMBER() —— 在一個分割槽內為每一行分配一個遞增的整數。常用於去重,或選出每個 symbol 的第 N 筆成交。
- LAG() 與 LEAD() —— 訪問上一行或下一行的值。計算逐筆成交收益率,或檢測時間序列資料中的缺口時不可或缺。
- SUM() OVER () —— 累計求和。計算累計成交量、累計盈虧(P&L),或滾動的持倉規模。
- AVG() OVER (ROWS BETWEEN) —— 直接在 SQL 裡算移動平均線。無需 Python。
來看一個實用的例子——計算 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 不同,每一個輸入行都會產出一個輸出行。這使得它們非常適合在詳盡的成交日誌上增加分析列,又不損失粒度。可以把它們看作是在「豐富」你的資料,而不是在「彙總」資料。
對於 OHLCV 聚合——也就是 K 線圖的基礎——你可以把 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:組織複雜的分析型查詢
公共表表示式(CTE)讓你能把一個複雜查詢拆分成一個個命名清晰、可讀性強的階段——就像程式設計裡的函式一樣。它們用 WITH 關鍵字引入,並且可以按順序相互引用。
假設你想找出自己最賺錢的 10 個交易日,但盈利能力需要計算每天扣除手續費後的淨盈虧(P&L)。藉助 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-tree 索引:
CREATE INDEX idx_trades_symbol_time
ON trades (symbol, executed_at DESC);
這一個索引就能加速大多數分析型查詢:「過去一小時所有 ETH 成交」「兩個時間戳之間的 BTC 成交」或「每個 symbol 的最新一筆成交」。列的順序很重要——symbol 放在前面以支援等值過濾,隨後 executed_at 在索引的該分段內支援高效的範圍掃描。
對於增長到數億行以上的表,表分割槽(table partitioning)必不可少。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),能在大型歷史資料集上帶來數量級的提速。
PostgreSQL vs ClickHouse vs BigQuery:選對引擎
並不是所有資料庫在處理金融分析時都旗鼓相當。如何選擇,取決於你的查詢模式、資料量和延遲要求。
PostgreSQL 是主力。它擅長事務型負載(實時記錄成交),支援 ACID 保證,能從容處理複雜的 JOIN,並在配以恰當的索引和分割槽後擴充套件到數億行。它是運營型資料庫——也就是「記錄系統」——的正確選擇。舉例來說,GaiaEx 就依託相容 PostgreSQL 的基礎設施承載其核心交易資料,並具備金融系統所要求的可靠性保證。
ClickHouse 是一種列式資料庫,專為在數十億行上做分析型查詢而設計。PostgreSQL 按行儲存資料(很適合插入單筆成交),而 ClickHouse 按列儲存資料(很適合在數百萬行上聚合某一列)。像「BTC 三年間的小時平均成交量」這樣一條查詢,在 PostgreSQL 上可能要 30 秒,在 ClickHouse 上也許 200 毫秒就跑完了。代價是:ClickHouse 並不能高效地支援 UPDATE 或 DELETE——它在設計上是僅追加(append-only)的。
BigQuery(Google Cloud)是一種無伺服器的列式資料倉儲。無需管理基礎設施,按查詢計費,具備 PB 級容量。它非常適合臨時分析、研究和探索——但查詢延遲以秒計、而非以毫秒計,因此不適合實時應用。
- 實時成交記錄與訂單管理 → PostgreSQL
- 在數十億行上做歷史分析與回測 → ClickHouse
- 臨時探索與跨團隊資料共享 → BigQuery
許多專業交易公司會在一套分層架構裡同時使用這三者:PostgreSQL 作為熱的運營儲存,ClickHouse 作為溫的分析層,BigQuery 作為冷的歸檔。寫好 SQL 的能力可以在三者之間通用——語法有 90% 是一致的,而分析的思路則是 100% 相同。