GaiaEx AcademyGaiaEx Academy
SQL dla hurtowni danych finansowych
DeweloperProgramowanie9 min read

SQL dla hurtowni danych finansowych

Odpytuj, agreguj i analizuj ustrukturyzowane dane finansowe

Udostępnij posty

Czemu SQL jest językiem danych finansowych

Każda transakcja, każdy tick, każda aktualizacja księgi zleceń — rynki finansowe produkują oszałamiające wolumeny ustrukturyzowanych danych. I od ponad czterech dekad SQL (Structured Query Language) jest dominującym narzędziem do odpytywania, przekształcania i analizowania tych danych. Nie jest to efektowne. Nie jest to modne. Ale kiedy zarządzający portfelem pyta „Jaka była moja średnia cena wypełnienia na transakcjach ETH we wtorek między 14:00 a 15:00 UTC?”, odpowiedź pochodzi z SQL.

Dane finansowe są z natury relacyjne. Transakcja odnosi się do zlecenia, które odnosi się do konta, które należy do użytkownika. Pozycje odnoszą się do instrumentów; instrumenty odnoszą się do rynków. Model relacyjny — tabele z wierszami i kolumnami, połączone kluczami obcymi — naturalnie odwzorowuje te relacje. Dlatego bazy relacyjne, takie jak PostgreSQL, MySQL i SQL Server, pozostają szkieletem infrastruktury finansowej, od Goldman Sachs po GaiaEx.

Siła SQL leży w jego deklaratywnej naturze: opisujesz co chcesz, nie jak to zdobyć. Silnik bazy danych sam wymyśla optymalny plan wykonania. To ma ogromne znaczenie, gdy twoja tabela historii transakcji ma 500 milionów wierszy, a wynik potrzebujesz w poniżej sekundy. Napisz właściwe zapytanie, zbuduj właściwy indeks, a SQL dostarczy — niezależnie od tego, czy uruchamiasz go na lokalnej instancji PostgreSQL, czy na hurtowni danych chmurowej skali.

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.
Transakcje wiszą na kontach i użytkownikach: to te same łącza, po których poruszają się twoje JOINy w SQL.

SELECT, JOIN i WHERE: odpytywanie danych transakcyjnych

Zacznijmy od konkretnego schematu. Wyobraź sobie tabelę trades na platformie takiej jak 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
);

Najbardziej podstawową operacją jest instrukcja SELECT z filtrowaniem. Aby znaleźć wszystkie transakcje kupna ETH-USD powyżej 3000 USD w ciągu ostatnich 24 godzin:

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;

JOINy łączą powiązane tabele. Załóżmy, że masz tabelę accounts i chcesz zobaczyć wolumen handlu na poziom konta:

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;

Te zapytania to codzienny chleb analityków finansowych, zespołów ryzyka i oficerów compliance. Opanuj je, a odpowiesz na każde pytanie, które zawierają twoje dane.

Funkcje okna: sekretna broń analityka

Funkcje okna (window functions) to to, co oddziela początkujących w SQL od praktyków SQL w finansach. Pozwalają wykonywać obliczenia na zbiorze wierszy powiązanych z aktualnym wierszem — bez zwijania wyniku w jedną zagregowaną wartość. Myśl o nich jako o „obliczeniach biegnących” po twoich danych.

Najczęściej wykorzystywane funkcje okna w analizie finansowej:

  • ROW_NUMBER() — przypisuje sekwencyjną liczbę całkowitą każdemu wierszowi w partycji. Przydatne do deduplikacji lub wybierania N-tej transakcji na symbol.
  • LAG() i LEAD() — dostęp do wartości poprzedniego lub następnego wiersza. Niezbędne do obliczania zwrotów transakcja-po-transakcji albo wykrywania przerw w danych szeregów czasowych.
  • SUM() OVER () — sumy narastające. Obliczaj skumulowany wolumen, skumulowany P&L albo narastającą wielkość pozycji.
  • AVG() OVER (ROWS BETWEEN) — średnie ruchome bezpośrednio w SQL. Bez potrzeby Pythona.

Praktyczny przykład — obliczanie zmiany ceny transakcja-po-transakcji i narastającej pozycji dla 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;

Funkcje okna nie zmniejszają liczby wierszy — w odróżnieniu od GROUP BY, każdy wiersz wejściowy produkuje wiersz wyjściowy. To sprawia, że są idealne do dodawania kolumn analitycznych do szczegółowych logów transakcji bez utraty granularności. Myśl o nich jako o „wzbogacaniu” danych, nie „podsumowywaniu” ich.

Dla agregacji OHLCV — fundamentu wykresów świecowych — możesz połączyć GROUP BY z podziałem na przedziały czasowe (time-bucketing) i standardowymi agregatami:

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: komponowanie złożonych zapytań analitycznych

Common Table Expressions (CTE) pozwalają rozłożyć złożone zapytanie na nazwane, czytelne etapy — jak funkcje w programowaniu. Wprowadza się je słowem kluczowym WITH i mogą one odnosić się do siebie sekwencyjnie.

Załóżmy, że chcesz zidentyfikować swoje 10 najbardziej dochodowych dni handlowych, ale rentowność wymaga obliczenia netto P&L na dzień, wliczając opłaty. Za pomocą CTE możesz zbudować to krok po kroku:

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;

Każde CTE czyta się jak paragraf. daily_trades agreguje surowe transakcje w dzienne podsumowania. daily_pnl obliczaj netto P&L i sumę narastającą. Końcowe SELECT wybiera 10 najlepszych dni. Porównaj to z jednym monolitycznym podzapytaniem — wersja CTE jest łatwa w utrzymaniu, testowalna i sama się dokumentuje.

W produkcyjnych systemach finansowych CTE są używane od raportowania regulacyjnego (agregowanie wymogów depozytowych między kontami) do zapytań dashboardów w czasie rzeczywistym (obliczanie rolującego 24-godzinnego wolumenu na GaiaEx). Komponują się tak samo, jak komponuje się dobrze wyfaktoryzowany kod — każda warstwa nadbudowuje poprzednią.

Indeksowanie i partycjonowanie dla wydajności szeregów czasowych

Zapytanie jest tak szybkie, jak indeks, który je wspiera. Bez właściwego indeksu nawet proste zdanie WHERE wymusza skan sekwencyjny — odczytanie każdego wiersza w tabeli. Przy 500 milionach wierszy to minuty, nie milisekundy.

Dla danych finansowych typu szereg czasowy najbardziej krytycznym wzorcem indeksu jest złożony indeks B-tree na (symbol, executed_at):

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

Ten jeden indeks przyspiesza większość zapytań analitycznych: „wszystkie transakcje ETH w ostatniej godzinie”, „transakcje BTC między dwoma znacznikami czasu” albo „ostatnia transakcja na symbol”. Kolejność kolumn ma znaczenie — symbol na pierwszym miejscu umożliwia filtrowanie po równości, a executed_at umożliwia efektywne skanowanie zakresów w ramach tej partycji indeksu.

Dla tabel, które rosną powyżej setek milionów wierszy, partycjonowanie tabel jest niezbędne. PostgreSQL wspiera deklaratywne partycjonowanie po zakresie — idealne dla szeregów czasowych:

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

Z partycjonowaniem zapytanie o dane z marca 2026 skanuje tylko partycję Q1 — silnik całkowicie pomija Q2, Q3 i Q4. Ta technika, zwana przycinaniem partycji (partition pruning), dostarcza przyspieszenia o rząd wielkości na dużych historycznych zbiorach danych.

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
Partycje ograniczają zakresy czasu; przycinanie pomija całe fragmenty, które nie mogą się dopasować.

PostgreSQL vs ClickHouse vs BigQuery: wybór właściwego silnika

Nie wszystkie bazy danych obsługują analitykę finansową równie dobrze. Wybór zależy od wzorców twoich zapytań, wolumenu danych i wymagań co do opóźnienia.

PostgreSQL jest koniem roboczym. Wybija się w obciążeniach transakcyjnych (zapisywanie transakcji w czasie rzeczywistym), wspiera gwarancje ACID, obsługuje złożone JOINy z gracją i skaluje się do setek milionów wierszy z właściwym indeksowaniem i partycjonowaniem. Jest właściwym wyborem dla baz operacyjnych — systemu ewidencji. GaiaEx, na przykład, opiera się na infrastrukturze kompatybilnej z PostgreSQL dla swoich kluczowych danych handlowych, z gwarancjami wiarygodności, jakich wymagają systemy finansowe.

ClickHouse jest bazą kolumnową zaprojektowaną do zapytań analitycznych na miliardach wierszy. Gdzie PostgreSQL przechowuje dane wiersz po wierszu (świetne do wstawiania pojedynczych transakcji), ClickHouse przechowuje dane kolumna po kolumnie (świetne do agregowania jednej kolumny na milionach wierszy). Zapytanie takie jak „średni godzinny wolumen dla BTC z 3 lat”, które zajmuje 30 sekund w PostgreSQL, może wykonać się w 200 milisekund w ClickHouse. Kompromis: ClickHouse nie wspiera efektywnie UPDATE ani DELETE — z projektu jest tylko do dopisywania (append-only).

BigQuery (Google Cloud) jest bezserwerową kolumnowa hurtownią danych. Bez infrastruktury do zarządzania, płatność za zapytanie, skalowalność do petabajtów. Idealny do analiz ad-hoc, badań i eksploracji — ale opóźnienie zapytania mierzy się w sekundach, nie milisekundach, co czyni go nienadającym się do zastosowań w czasie rzeczywistym.

  • Zapisywanie transakcji w czasie rzeczywistym i zarządzanie zleceniami → PostgreSQL
  • Analityka historyczna i backtesting na miliardach wierszy → ClickHouse
  • Eksploracja ad-hoc i wymiana danych między zespołami → BigQuery

Wiele profesjonalnych firm handlowych używa wszystkich trzech w architekturze warstwowej: PostgreSQL jako gorący magazyn operacyjny, ClickHouse jako ciepła warstwa analityczna, a BigQuery jako chłodne archiwum. Umiejętność pisania dobrego SQL przenosi się na wszystkie z nich — składnia jest w 90% identyczna, a myślenie analityczne w 100% takie samo.