GaiaEx AcademyGaiaEx Academy
SQL для фінансових сховищ даних
РозробникПрограмування9 min read

SQL для фінансових сховищ даних

Запити, агрегація та аналіз структурованих фінансових даних

Поділитися

Чому SQL — це мова фінансових даних

Кожна угода, кожен тик, кожне оновлення книги ордерів — фінансові ринки генерують колосальні обсяги структурованих даних. І вже понад чотири десятиліття SQL (Structured Query Language) є домінантним інструментом для запитів, перетворення та аналізу цих даних. Це не гламурно. Це не модно. Але коли портфельний менеджер запитує «Яка була моя середня ціна виконання по угодах ETH минулого вівторка між 14:00 і 15:00 UTC?», відповідь надходить із SQL.

Фінансові дані за своєю природою реляційні. Угода посилається на ордер, який посилається на рахунок, що належить користувачу. Позиції пов’язані з інструментами; інструменти пов’язані з ринками. Реляційна модель — таблиці з рядками й стовпцями, з’єднані зовнішніми ключами — природно відображає ці зв’язки. Саме тому реляційні бази даних, такі як PostgreSQL, MySQL і SQL Server, залишаються основою фінансової інфраструктури — від Goldman Sachs до GaiaEx.

Сила SQL полягає в його декларативній природі: ви описуєте що хочете отримати, а не як це отримати. Механізм бази даних сам визначає оптимальний план виконання. Це має колосальне значення, коли ваша таблиця історії угод містить 500 мільйонів рядків, а результат потрібен менш ніж за секунду. Напишіть правильний запит, побудуйте правильний індекс — і SQL справляється, незалежно від того, чи ви запускаєте його на локальному екземплярі PostgreSQL, чи на хмарному сховищі даних масштабу.

Реляційна модель (спрощено) users user_id PK accounts user_id FK → users tier, limits … trades user_id FK → accounts symbol, price, time … JOIN-и слідують за зовнішніми ключами — форма фінансових даних уже реляційна.
Угоди прив’язані до рахунків і користувачів: ті самі зв’язки, які ваш SQL JOIN проходить.

SELECT, JOIN і WHERE: запити до даних про угоди

Почнемо з конкретної схеми. Уявіть таблицю trades на платформі, подібній до 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
);

Найфундаментальніша операція — це оператор SELECT із фільтрацією. Щоб знайти всі угоди на купівлю ETH-USD вище $3,000 за останні 24 години:

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() — Присвоює послідовне ціле число кожному рядку в межах партиції. Корисно для дедуплікації або вибору 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 — основи свічкових графіків — можна поєднати 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, Common Table Expressions) дозволяють розбити складний запит на названі, читабельні етапи — подібно до функцій у програмуванні. Вони вводяться ключовим словом 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 обчислює чистий P&L і накопичувальну суму. Фінальний SELECT обирає топ-10 днів. Порівняйте це з єдиним монолітним підзапитом — версія з CTE є зручнішою для підтримки, тестованою й самодокументованою.

У продуктивних фінансових системах CTE використовують для всього — від нормативної звітності (агрегація вимог до маржі по рахунках) до запитів для дашбордів у реальному часі (обчислення ковзного 24-годинного обсягу на GaiaEx). Вони компонуються так само, як добре структурований код — кожен рівень будується на попередньому.

Індексація та партиціонування для продуктивності часових рядів

Запит настільки швидкий, наскільки швидкий індекс, що його підтримує. Без потрібного індексу навіть простий WHERE змушує виконувати послідовне сканування — читання кожного рядка таблиці. При 500 мільйонах рядків це хвилини замість мілісекунд.

Для фінансових часових рядів найважливіший шаблон індексу — це складений B-дерево-індекс на (symbol, executed_at):

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

Цей єдиний індекс прискорює більшість аналітичних запитів: «усі угоди ETH за останню годину», «угоди BTC між двома часовими метками» або «остання угода по символу». Порядок стовпців важливий — symbol на першому місці уможливлює фільтрацію за рівністю, а потім executed_at уможливлює ефективне сканування діапазонів у межах цієї партиції індексу.

Для таблиць, що виростають до сотень мільйонів рядків, партиціонування таблиць є необхідним. 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 сканує лише партицію Q1 — механізм повністю пропускає Q2, Q3 та Q4. Ця техніка, яку називають відсіканням партицій (partition pruning), забезпечує прискорення на порядки на великих історичних наборах даних.

Відсікання партицій за часовим діапазоном Q1 2026 рядки Jan–Mar Q2 2026 пропущено Q3 2026 Q4 2026 Запит: WHERE executed_at у березні → планувальник читає лише Q1 Складений індекс (symbol, executed_at) усе одно допомагає в межах кожної партиції
Партиції обмежують часові діапазони; відсікання пропускає цілі блоки, що не можуть відповідати запиту.

PostgreSQL проти ClickHouse проти BigQuery: вибір правильного механізму

Не всі бази даних однаково добре справляються з фінансовою аналітикою. Вибір залежить від шаблонів ваших запитів, обсягу даних і вимог до латентності.

PostgreSQL — це робочий кінь. Він чудово справляється з транзакційними навантаженнями (запис угод у реальному часі), підтримує гарантії ACID, витончено обробляє складні JOIN-и й масштабується до сотень мільйонів рядків за правильної індексації та партиціонування. Це правильний вибір для операційних баз даних — системи обліку. GaiaEx, наприклад, покладається на PostgreSQL-сумісну інфраструктуру для своїх основних торгових даних із гарантіями надійності, яких вимагають фінансові системи.

ClickHouse — це стовпчикова база даних, розроблена для аналітичних запитів над мільярдами рядків. Там, де PostgreSQL зберігає дані рядок за рядком (чудово для вставки окремих угод), ClickHouse зберігає дані стовпець за стовпцем (чудово для агрегації одного стовпця по мільйонах рядків). Запит типу «середній годинний обсяг для BTC за 3 роки», що займає 30 секунд у PostgreSQL, може виконатися за 200 мілісекунд у ClickHouse. Компроміс: ClickHouse не підтримує ефективно UPDATE чи DELETE — за задумом він призначений лише для додавання даних.

BigQuery (Google Cloud) — це безсерверне стовпчикове сховище. Немає інфраструктури для управління, оплата за запит і петабайтна ємність. Він ідеальний для ad-hoc аналітики, дослідження та розвідки — але латентність запитів вимірюється секундами, а не мілісекундами, що робить його непридатним для застосунків у реальному часі.

  • Запис угод у реальному часі та управління ордерами → PostgreSQL
  • Історична аналітика та бектестинг на мільярдах рядків → ClickHouse
  • Ad-hoc дослідження та обмін даними між командами → BigQuery

Багато професійних торгових фірм використовують усі три в багаторівневій архітектурі: PostgreSQL як гаряче операційне сховище, ClickHouse як теплий аналітичний рівень, і BigQuery як холодний архів. Навичка писати добрий SQL переноситься на всі три — синтаксис збігається на 90%, а аналітичне мислення однакове на 100%.