
SQL para Data Warehousing Financeiro
Consultando, agregando e analisando dados financeiros estruturados
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.
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.
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.