
SQL per i Data Warehouse Finanziari
Interroga, aggrega, e analizza dati finanziari strutturati
Perché SQL è il linguaggio dei dati finanziari
Ogni trade, ogni tick, ogni aggiornamento dell'order book — i mercati finanziari producono volumi impressionanti di dati strutturati. E per più di quattro decenni, SQL (Structured Query Language) è stato lo strumento dominante per interrogare, trasformare, e analizzare quei dati. Non è affascinante. Non è di tendenza. Ma quando un portfolio manager chiede «Qual era il mio prezzo medio di esecuzione sui trade ETH martedì scorso tra le 14 e le 15 UTC?», la risposta arriva da SQL.
I dati finanziari sono intrinsecamente relazionali. Un trade fa riferimento a un ordine, che fa riferimento a un conto, che appartiene a un utente. Le posizioni si relazionano con gli strumenti; gli strumenti si relazionano con i mercati. Il modello relazionale — tabelle con righe e colonne, collegate da chiavi esterne — mappa naturalmente queste relazioni. Ecco perché i database relazionali come PostgreSQL, MySQL, e SQL Server restano la spina dorsale dell'infrastruttura finanziaria, da Goldman Sachs a GaiaEx.
La potenza di SQL sta nella sua natura dichiarativa: descrivi cosa vuoi, non come ottenerlo. Il motore del database calcola il piano di esecuzione ottimale. Questo conta enormemente quando la tua tabella dello storico dei trade ha 500 milioni di righe e hai bisogno dei risultati in meno di un secondo. Scrivi la query giusta, costruisci l'indice giusto, e SQL li fornisce — indipendentemente dal fatto che tu lo stia eseguendo contro un'istanza PostgreSQL locale o un data warehouse su scala cloud.
SELECT, JOIN, e WHERE: interrogare i dati dei trade
Iniziamo con uno schema concreto. Immagina una tabella trades su una piattaforma come 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
);
L'operazione più fondamentale è l'istruzione SELECT con filtro. Per trovare tutti i trade di acquisto ETH-USD sopra $3,000 nelle ultime 24 ore:
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;
Le JOIN collegano tabelle correlate. Supponi di avere una tabella accounts e di voler vedere il volume di trading per livello di conto:
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;
Queste query sono il pane quotidiano degli analisti finanziari, dei team di rischio, e degli addetti alla conformità. Padroneggiale e potrai rispondere a qualsiasi domanda che i tuoi dati contengono.
Window function: l'arma segreta dell'analista
Le window function sono ciò che separa i principianti di SQL dai praticanti di SQL in finanza. Ti permettono di eseguire calcoli attraverso un insieme di righe correlate alla riga corrente — senza collassare il risultato in un singolo valore aggregato. Pensaci come «calcoli in esecuzione» attraverso i tuoi dati.
Le window function più usate nell'analisi finanziaria:
- ROW_NUMBER() — Assegna un intero sequenziale a ogni riga all'interno di una partizione. Utile per la deduplicazione o per selezionare l'N-esimo trade per simbolo.
- LAG() e LEAD() — Accedono al valore della riga precedente o successiva. Essenziali per calcolare i rendimenti trade-per-trade o rilevare gap nei dati di serie temporali.
- SUM() OVER () — Totali cumulativi. Calcola il volume cumulativo, il P&L cumulativo, o la dimensione della posizione in corso.
- AVG() OVER (ROWS BETWEEN) — Medie mobili direttamente in SQL. Nessun bisogno di Python.
Ecco un esempio pratico — calcolare la variazione di prezzo trade-per-trade e una posizione in corso per 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;
Le window function non riducono il numero di righe — a differenza di GROUP BY, ogni riga in input produce una riga in output. Questo le rende perfette per aggiungere colonne analitiche a log dettagliati dei trade senza perdere granularità. Pensaci come un modo di «arricchire» i tuoi dati piuttosto che «riassumerli».
Per l'aggregazione OHLCV — il fondamento dei grafici a candele — puoi combinare GROUP BY con il time-bucketing e aggregati standard:
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: comporre query analitiche complesse
Le Common Table Expression (CTE) ti permettono di scomporre una query complessa in fasi nominate e leggibili — come le funzioni nella programmazione. Vengono introdotte con la parola chiave WITH e possono fare riferimento l'una all'altra in sequenza.
Supponi di voler identificare i tuoi 10 giorni di trading più profittevoli, ma la profittabilità richiede calcolare il P&L netto per giorno incluse le commissioni. Con le CTE, puoi costruire questo passo per passo:
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;
Ogni CTE si legge come un paragrafo. daily_trades aggrega i trade grezzi in riepiloghi giornalieri. daily_pnl calcola il P&L netto e un totale in corso. Il SELECT finale seleziona i primi 10 giorni. Confronta questo con una singola subquery monolitica — la versione con CTE è mantenibile, testabile, e si autodocumenta.
Nei sistemi finanziari in produzione, le CTE sono usate per tutto, dal reporting regolatorio (aggregare i requisiti di margine attraverso i conti) alle query dei dashboard in tempo reale (calcolare il volume mobile a 24 ore su GaiaEx). Si compongono nello stesso modo in cui si compone il codice ben fattorizzato — ogni livello si costruisce sul precedente.
Indicizzazione e partizionamento per le prestazioni delle serie temporali
Una query è rapida solo quanto l'indice che la supporta. Senza l'indice giusto, anche una semplice clausola WHERE forza una scansione sequenziale — leggendo ogni riga della tabella. A 500 milioni di righe, sono minuti invece di millisecondi.
Per i dati di serie temporali finanziari, il pattern di indice più critico è un indice B-tree composto su (symbol, executed_at):
CREATE INDEX idx_trades_symbol_time
ON trades (symbol, executed_at DESC);
Questo singolo indice accelera la maggior parte delle query analitiche: «tutti i trade ETH nell'ultima ora», «trade BTC tra due timestamp», o «ultimo trade per simbolo». L'ordine delle colonne conta — symbol prima abilita il filtro per uguaglianza, poi executed_at abilita scansioni per intervallo efficienti all'interno di quella partizione dell'indice.
Per le tabelle che crescono oltre le centinaia di milioni di righe, il partizionamento delle tabelle è essenziale. PostgreSQL supporta il partizionamento dichiarativo per intervallo — perfetto per le serie temporali:
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');
Con il partizionamento, una query per i dati di marzo 2026 esamina solo la partizione Q1 — il motore salta completamente Q2, Q3, e Q4. Questa tecnica, chiamata partition pruning, offre accelerazioni di un ordine di grandezza su grandi dataset storici.
PostgreSQL contro ClickHouse contro BigQuery: scegliere il motore giusto
Non tutti i database gestiscono l'analitica finanziaria allo stesso modo. La scelta dipende dai tuoi pattern di query, dal volume di dati, e dai requisiti di latenza.
PostgreSQL è il cavallo di battaglia. Eccelle nei carichi di lavoro transazionali (registrare i trade in tempo reale), supporta garanzie ACID, gestisce con eleganza le JOIN complesse, e scala a centinaia di milioni di righe con indicizzazione e partizionamento adeguati. È la scelta giusta per i database operativi — il sistema di registrazione. GaiaEx, per esempio, si affida a un'infrastruttura compatibile con PostgreSQL per i suoi dati di trading principali con le garanzie di affidabilità che i sistemi finanziari richiedono.
ClickHouse è un database a colonne progettato per query analitiche su miliardi di righe. Dove PostgreSQL memorizza i dati riga per riga (ottimo per inserire trade individuali), ClickHouse memorizza i dati colonna per colonna (ottimo per aggregare una singola colonna attraverso milioni di righe). Una query come «volume medio orario per BTC negli ultimi 3 anni» che richiede 30 secondi in PostgreSQL potrebbe finire in 200 millisecondi in ClickHouse. Il compromesso: ClickHouse non supporta UPDATE o DELETE in modo efficiente — è append-only per design.
BigQuery (Google Cloud) è un data warehouse a colonne serverless. Nessuna infrastruttura da gestire, prezzo pay-per-query, e capacità su scala petabyte. È ideale per l'analitica ad hoc, la ricerca, e l'esplorazione — ma la latenza delle query si misura in secondi, non millisecondi, il che lo rende inadatto per applicazioni in tempo reale.
- Registrazione dei trade in tempo reale e gestione degli ordini → PostgreSQL
- Analitica storica e backtesting su miliardi di righe → ClickHouse
- Esplorazione ad hoc e condivisione di dati tra team → BigQuery
Molte società di trading professionali usano tutti e tre in un'architettura a livelli: PostgreSQL come archivio operativo caldo, ClickHouse come livello analitico tiepido, e BigQuery come archivio freddo. L'abilità di scrivere buon SQL si trasferisce attraverso tutti loro — la sintassi è identica al 90%, e il pensiero analitico è identico al 100%.