GaiaEx AcademyGaiaEx Academy
SQL para Data Warehousing Financeiro
DesenvolvedorProgramação9 min read

SQL para Data Warehousing Financeiro

Consultando, agregando e analisando dados financeiros estruturados

Compartilhar Posts

Por que o SQL é a linguagem dos dados financeiros

Cada negociação, cada tick, cada atualização do livro de ofertas — os mercados financeiros produzem volumes impressionantes de dados estruturados. E, por mais de quatro décadas, o SQL (Structured Query Language) tem sido a ferramenta dominante para consultar, transformar e analisar esses dados. Não é glamouroso. Não é moderno. Mas quando um gestor de carteira pergunta “Qual foi meu preço médio de execução em negociações de ETH na terça-feira passada, entre 14h e 15h UTC?”, a resposta vem do SQL.

Os dados financeiros são inerentemente relacionais. Uma negociação referencia uma ordem, que referencia uma conta, que pertence a um usuário. Posições se relacionam com instrumentos; instrumentos se relacionam com mercados. O modelo relacional — tabelas com linhas e colunas, conectadas por chaves estrangeiras — mapeia naturalmente esses relacionamentos. É por isso que bancos de dados relacionais como PostgreSQL, MySQL e SQL Server continuam sendo a espinha dorsal da infraestrutura financeira, do Goldman Sachs à GaiaEx.

O poder do SQL está em sua natureza declarativa: você descreve o que quer, não como obtê-lo. O motor do banco de dados descobre o plano de execução ideal. Isso importa enormemente quando sua tabela de histórico de negociações tem 500 milhões de linhas e você precisa de resultados em menos de um segundo. Escreva a consulta certa, construa o índice certo, e o SQL entrega — não importa se você está executando contra uma instância local do PostgreSQL ou um data warehouse em escala de nuvem.

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.
As negociações se conectam a contas e usuários: os mesmos links que seus JOINs de SQL percorrem.

SELECT, JOIN e WHERE: consultando dados de negociação

Vamos começar com um esquema concreto. Imagine uma tabela trades em uma plataforma como a 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
);

A operação mais fundamental é a instrução SELECT com filtragem. Para encontrar todas as negociações de compra de ETH-USD acima de $3,000 nas últimas 24 horas:

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;

Os JOINs conectam tabelas relacionadas. Suponha que você tenha uma tabela accounts e queira ver o volume de negociação por camada (tier) de conta:

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;

Essas consultas são o pão de cada dia de analistas financeiros, equipes de risco e responsáveis por compliance. Domine-as e você conseguirá responder a qualquer pergunta que seus dados contenham.

Funções de janela: a arma secreta do analista

As funções de janela (window functions) são o que separa os principiantes em SQL dos praticantes de SQL em finanças. Elas permitem fazer cálculos sobre um conjunto de linhas relacionadas à linha atual — sem colapsar o resultado em um único valor agregado. Pense nelas como “cálculos contínuos” sobre seus dados.

As funções de janela mais usadas em análise financeira:

  • ROW_NUMBER() — Atribui um número inteiro sequencial a cada linha dentro de uma partição. Útil para deduplicação ou para selecionar a N-ésima negociação por símbolo.
  • LAG() e LEAD() — Acessam o valor da linha anterior ou seguinte. Essenciais para calcular retornos entre negociações consecutivas ou detectar lacunas em dados de séries temporais.
  • SUM() OVER () — Totais acumulados. Calcule volume acumulado, PnL acumulado, ou o tamanho de posição em curso.
  • AVG() OVER (ROWS BETWEEN) — Médias móveis diretamente no SQL. Sem necessidade de Python.

Aqui vai um exemplo prático — calculando a variação de preço entre negociações consecutivas e uma posição em curso para o 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;

As funções de janela não reduzem a quantidade de linhas — diferentemente do GROUP BY, cada linha de entrada produz uma linha de saída. Isso as torna perfeitas para adicionar colunas analíticas a registros detalhados de negociação sem perder granularidade. Pense nelas como “enriquecendo” seus dados, em vez de “resumi-los”.

Para a agregação OHLCV — a base dos gráficos de candlestick — você pode combinar GROUP BY com agrupamento por intervalo de tempo (time-bucketing) e agregados padrão:

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;

CTEs: compondo consultas analíticas complexas

As Common Table Expressions (CTEs) permitem que você divida uma consulta complexa em estágios nomeados e legíveis — como funções em programação. Elas são introduzidas com a palavra-chave WITH e podem se referenciar umas às outras em sequência.

Suponha que você queira identificar seus 10 dias de negociação mais rentáveis, mas a rentabilidade exige calcular o PnL líquido por dia, incluindo taxas. Com CTEs, você pode construir isso passo a 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;

Cada CTE se lê como um parágrafo. daily_trades agrega negociações brutas em resumos diários. daily_pnl calcula o PnL líquido e um total acumulado. O SELECT final escolhe os 10 melhores dias. Compare isso a uma única subconsulta monolítica — a versão com CTE é mais fácil de manter, testar e autodocumentada.

Em sistemas financeiros de produção, as CTEs são usadas para tudo, desde relatórios regulatórios (agregando requisitos de margem entre contas) até consultas de dashboard em tempo real (calculando o volume móvel de 24 horas na GaiaEx). Elas se compõem da mesma forma que um código bem estruturado se compõe — cada camada se apoia na anterior.

Indexação e particionamento para desempenho em séries temporais

Uma consulta só é tão rápida quanto o índice que a sustenta. Sem o índice certo, até uma cláusula WHERE simples força uma varredura sequencial (sequential scan) — lendo cada linha da tabela. Com 500 milhões de linhas, isso são minutos em vez de milissegundos.

Para dados financeiros de séries temporais, o padrão de índice mais crítico é um índice B-tree composto em (symbol, executed_at):

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

Esse único índice acelera a maioria das consultas analíticas: “todas as negociações de ETH na última hora”, “negociações de BTC entre dois timestamps”, ou “última negociação por símbolo”. A ordem das colunas importa — symbol primeiro habilita a filtragem por igualdade, depois executed_at habilita varreduras de intervalo eficientes dentro dessa partição do índice.

Para tabelas que crescem além de centenas de milhões de linhas, o particionamento de tabelas é essencial. O PostgreSQL suporta particionamento declarativo por intervalo — perfeito para séries temporais:

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

Com o particionamento, uma consulta para dados de março de 2026 varre apenas a partição do primeiro trimestre — o motor pula completamente o segundo, terceiro e quarto trimestres. Essa técnica, chamada de partition pruning (eliminação de partições), entrega ganhos de velocidade de ordens de magnitude em grandes conjuntos de dados históricos.

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
As partições delimitam intervalos de tempo; o pruning pula blocos inteiros que não podem corresponder.

PostgreSQL vs ClickHouse vs BigQuery: escolhendo o motor certo

Nem todos os bancos de dados lidam com análise financeira da mesma forma. A escolha depende dos seus padrões de consulta, volume de dados e requisitos de latência.

O PostgreSQL é o cavalo de batalha. Ele se destaca em cargas transacionais (registrando negociações em tempo real), suporta garantias ACID, lida bem com JOINs complexos e escala para centenas de milhões de linhas com indexação e particionamento adequados. É a escolha certa para bancos de dados operacionais — o sistema de registro. A GaiaEx, por exemplo, depende de infraestrutura compatível com PostgreSQL para seus dados centrais de negociação, com as garantias de confiabilidade que sistemas financeiros exigem.

O ClickHouse é um banco de dados colunar projetado para consultas analíticas sobre bilhões de linhas. Onde o PostgreSQL armazena dados linha por linha (ótimo para inserir negociações individuais), o ClickHouse armazena dados coluna por coluna (ótimo para agregar uma única coluna em milhões de linhas). Uma consulta como “volume médio por hora do BTC ao longo de 3 anos”, que leva 30 segundos no PostgreSQL, pode terminar em 200 milissegundos no ClickHouse. O trade-off: o ClickHouse não suporta UPDATE ou DELETE de forma eficiente — ele é somente-anexação (append-only) por design.

O BigQuery (Google Cloud) é um data warehouse colunar sem servidor (serverless). Sem infraestrutura para gerenciar, precificação por consulta e capacidade em escala de petabytes. É ideal para análises pontuais, pesquisa e exploração — mas a latência de consulta é medida em segundos, não em milissegundos, o que o torna inadequado para aplicações em tempo real.

  • Registro de negociações em tempo real e gerenciamento de ordens → PostgreSQL
  • Análise histórica e backtesting sobre bilhões de linhas → ClickHouse
  • Exploração pontual e compartilhamento de dados entre equipes → BigQuery

Muitas mesas de negociação profissionais usam os três em uma arquitetura em camadas: PostgreSQL como o armazenamento operacional “quente”, ClickHouse como a camada analítica “morna”, e BigQuery como o arquivo “frio”. A habilidade de escrever bom SQL se transfere entre todos eles — a sintaxe é 90% idêntica, e o raciocínio analítico é 100% o mesmo.