GaiaEx AcademyGaiaEx Academy
SQL per i Data Warehouse Finanziari
SviluppatoreProgrammazione9 min read

SQL per i Data Warehouse Finanziari

Interroga, aggrega, e analizza dati finanziari strutturati

Condividi Post

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.

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.
I trade dipendono da conti e utenti: gli stessi collegamenti che le tue JOIN SQL percorrono.

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.

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
Le partizioni delimitano intervalli temporali; il pruning salta interi blocchi che non possono corrispondere.

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%.