Jhonatan Pinheiro
Carregando página...
Jhonatan Pinheiro
Carregando página...
Jhonatan Pinheiro
Carregando página...
Window functions, CTEs, performance, concorrência, segurança e administração: 40 tópicos para quem trabalha sério com dados.
Aqui mora a diferença entre "funciona" e "funciona rápido e seguro em produção". São 40 tópicos de window functions, CTEs, performance, concorrência, segurança e administração — o kit de quem trabalha sério com dados.
Agrega sem colapsar as linhas. Você mantém o detalhe E vê o total do grupo na mesma linha.
SELECT nome, cidade, total,
SUM(total) OVER (PARTITION BY cidade) AS total_cidade
FROM pedidos;Dá um número sequencial por grupo. Base para 'o mais recente de cada'.
SELECT * FROM (
SELECT p.*,
ROW_NUMBER() OVER (PARTITION BY cliente_id ORDER BY data DESC) AS rn
FROM pedidos p
) t
WHERE rn = 1; -- último pedido de cada clienteRanqueiam com empates. RANK pula posições; DENSE_RANK não.
SELECT nome, total,
RANK() OVER (ORDER BY total DESC) AS rank,
DENSE_RANK() OVER (ORDER BY total DESC) AS dense
FROM pedidos;Compare uma linha com a vizinha. Perfeito para variação mês a mês.
SELECT mes, receita,
LAG(receita) OVER (ORDER BY mes) AS mes_anterior,
receita - LAG(receita) OVER (ORDER BY mes) AS variacao
FROM receita_mensal;Soma correndo linha a linha usando ORDER BY na janela.
SELECT data, valor,
SUM(valor) OVER (ORDER BY data
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS acumulado
FROM movimentacoes;Divide as linhas em N grupos de tamanho parecido (quartis, decis).
SELECT nome, total,
NTILE(4) OVER (ORDER BY total DESC) AS quartil
FROM clientes_gasto;Pega o primeiro/último valor da janela. Cuidado com o frame no LAST_VALUE.
SELECT cidade, nome, total,
FIRST_VALUE(nome) OVER (
PARTITION BY cidade ORDER BY total DESC) AS maior_comprador
FROM pedidos;Nomeia subconsultas e deixa queries complexas legíveis, de cima para baixo.
WITH faturamento AS (
SELECT cidade, SUM(total) AS total FROM pedidos GROUP BY cidade
)
SELECT * FROM faturamento WHERE total > 10000;CTE que se chama: percorre árvores (organograma, categorias, grafos).
WITH RECURSIVE subordinados AS (
SELECT id, nome, gerente_id FROM funcionarios WHERE id = 1
UNION ALL
SELECT f.id, f.nome, f.gerente_id
FROM funcionarios f
JOIN subordinados s ON f.gerente_id = s.id
)
SELECT * FROM subordinados;Transforma valores em colunas com agregação condicional (funciona em qualquer banco).
SELECT produto,
SUM(CASE WHEN mes = 1 THEN qtd END) AS jan,
SUM(CASE WHEN mes = 2 THEN qtd END) AS fev,
SUM(CASE WHEN mes = 3 THEN qtd END) AS mar
FROM vendas
GROUP BY produto;O inverso do pivot: normaliza colunas em pares (chave, valor).
SELECT produto, 'jan' AS mes, jan AS qtd FROM vendas_wide
UNION ALL
SELECT produto, 'fev', fev FROM vendas_wide
UNION ALL
SELECT produto, 'mar', mar FROM vendas_wide;Insere se não existe, atualiza se existe — em um comando só.
-- PostgreSQL / SQLite
INSERT INTO estoque (produto_id, qtd) VALUES (10, 5)
ON CONFLICT (produto_id)
DO UPDATE SET qtd = estoque.qtd + EXCLUDED.qtd;Versão do UPSERT no MySQL.
INSERT INTO estoque (produto_id, qtd) VALUES (10, 5)
ON DUPLICATE KEY UPDATE qtd = qtd + VALUES(qtd);Um índice em várias colunas pode responder a query sem tocar na tabela.
-- A ordem importa: filtra por cliente_id e ordena por data
CREATE INDEX idx_ped_cli_data ON pedidos(cliente_id, data);
-- Covering: inclui colunas do SELECT (PostgreSQL)
CREATE INDEX idx_cover ON pedidos(cliente_id) INCLUDE (total);Mostra COMO o banco vai executar. É a ferramenta nº1 de tuning.
EXPLAIN ANALYZE
SELECT * FROM pedidos WHERE cliente_id = 42;
-- Procure por 'Seq Scan' (ruim em tabela grande) vs 'Index Scan' (bom)Escreva o WHERE de um jeito que o índice possa ser usado. Não envolva a coluna em função.
-- RUIM: função na coluna impede o índice
WHERE YEAR(data) = 2024
-- BOM: usa o índice em 'data'
WHERE data >= '2024-01-01' AND data < '2025-01-01'Como uma VIEW, mas armazena o resultado. Rápida para ler; precisa ser atualizada.
CREATE MATERIALIZED VIEW mv_dashboard AS
SELECT cidade, SUM(total) AS total FROM pedidos GROUP BY cidade;
REFRESH MATERIALIZED VIEW mv_dashboard;Bloco de SQL nomeado e reutilizável, com parâmetros e lógica no banco.
CREATE PROCEDURE dar_desconto(IN pid INT, IN pct DECIMAL)
BEGIN
UPDATE produtos SET preco = preco * (1 - pct/100) WHERE id = pid;
END;
CALL dar_desconto(10, 15);Retorna um valor e pode ser usada dentro de queries.
CREATE FUNCTION preco_com_iva(preco DECIMAL)
RETURNS DECIMAL
RETURN preco * 1.23;
SELECT nome, preco_com_iva(preco) FROM produtos;Executa código automaticamente em INSERT/UPDATE/DELETE. Ótimo para auditoria.
CREATE TRIGGER trg_log_preco
AFTER UPDATE ON produtos
FOR EACH ROW
INSERT INTO log_precos (produto_id, preco_antigo, preco_novo)
VALUES (OLD.id, OLD.preco, NEW.preco);Controlam o que uma transação enxerga das outras. Trade-off entre consistência e concorrência.
-- READ COMMITTED (padrão em muitos bancos), REPEATABLE READ, SERIALIZABLE
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- leituras consistentes, sem 'phantom reads'
COMMIT;Duas transações se travam esperando uma à outra. Previna acessando recursos na mesma ordem.
-- Bloqueia as linhas até o COMMIT (evita corrida)
BEGIN;
SELECT * FROM contas WHERE id = 1 FOR UPDATE;
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
COMMIT;Divide uma tabela gigante em pedaços (ex.: por mês). Consultas varrem só a partição certa.
CREATE TABLE pedidos (id BIGINT, data DATE, total DECIMAL)
PARTITION BY RANGE (data);
CREATE TABLE pedidos_2024 PARTITION OF pedidos
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');Organiza dados para evitar repetição e anomalias. Cada fato em um só lugar.
-- RUIM (repete cliente em cada pedido):
-- pedidos(id, cliente_nome, cliente_email, total)
-- BOM (3NF): separa em duas tabelas ligadas por FK
-- clientes(id, nome, email)
-- pedidos(id, cliente_id, total)De propósito, repetir dado para ganhar velocidade de leitura (comum em BI/analytics).
-- Guarda o total já calculado para não recalcular a cada leitura
ALTER TABLE clientes ADD COLUMN total_gasto DECIMAL(12,2) DEFAULT 0;
-- (mantido por trigger ou job)Indexa só as linhas que importam. Menor e mais rápido.
-- Só pedidos em aberto (PostgreSQL)
CREATE INDEX idx_abertos ON pedidos(cliente_id) WHERE status = 'aberto';Vários níveis de totalização numa query só (subtotais + total geral).
SELECT cidade, categoria, SUM(total)
FROM pedidos
GROUP BY ROLLUP (cidade, categoria);JOIN onde o lado direito enxerga o esquerdo. Ótimo para 'top N por grupo'.
SELECT c.nome, p.*
FROM clientes c
CROSS JOIN LATERAL (
SELECT * FROM pedidos p
WHERE p.cliente_id = c.id ORDER BY data DESC LIMIT 3
) p; -- 3 últimos pedidos de cada clienteBancos modernos guardam e consultam JSON nativamente.
-- PostgreSQL
SELECT dados->>'email' AS email
FROM eventos
WHERE dados->>'tipo' = 'login';
-- Índice em campo JSON
CREATE INDEX idx_tipo ON eventos ((dados->>'tipo'));Busca textual de verdade (relevância, stemming), muito além do LIKE '%x%'.
-- PostgreSQL
SELECT * FROM artigos
WHERE to_tsvector('portuguese', corpo) @@ to_tsquery('portuguese', 'banco & dados');SELECT * em produção, N+1 e funções no WHERE matam a performance.
-- Evite: SELECT * (traz colunas demais, quebra covering index)
-- Evite: consultar dentro de loop no app (N+1) -> use JOIN
-- Evite: WHERE LOWER(email) = ... -> use coluna/índice apropriadoSemânticas parecidas, performance diferente. EXISTS costuma vencer com subconjuntos grandes; cuidado com NOT IN e NULL.
-- Prefira EXISTS para 'tem pelo menos um'
SELECT c.* FROM clientes c
WHERE EXISTS (SELECT 1 FROM pedidos p WHERE p.cliente_id = c.id);
-- NOT IN quebra se a subconsulta tiver NULL -> use NOT EXISTSCTE é ótima para legibilidade; tabela temporária materializa e pode ser indexada (bom para reuso pesado).
CREATE TEMP TABLE tmp_top AS
SELECT cliente_id, SUM(total) AS total
FROM pedidos GROUP BY cliente_id;
CREATE INDEX ON tmp_top(total);
SELECT * FROM tmp_top ORDER BY total DESC LIMIT 10;OFFSET grande é lento. Pagine pelo último id/valor visto (keyset/seek).
-- LENTO em páginas altas:
SELECT * FROM pedidos ORDER BY id LIMIT 20 OFFSET 100000;
-- RÁPIDO (keyset):
SELECT * FROM pedidos WHERE id > 100000 ORDER BY id LIMIT 20;Atualize/apague em lotes para não travar a tabela nem estourar o log.
-- Apaga 10 mil por vez, em loop no app
DELETE FROM logs
WHERE id IN (
SELECT id FROM logs WHERE criado_em < '2023-01-01' LIMIT 10000
);Nunca concatene entrada do usuário na query. Use sempre queries parametrizadas.
-- PERIGOSO (injeção!):
-- "SELECT * FROM users WHERE email = '" + input + "'"
-- SEGURO (placeholder / prepared statement):
SELECT * FROM users WHERE email = ?; -- valor passado à parteMontar SQL em tempo de execução é poderoso, mas perigoso. Valide identificadores e parametrize valores.
-- Se precisar de coluna/tabela dinâmica, use whitelist:
-- if (col not in ['nome','email']) throw;
-- e SEMPRE parametrize os VALORES, nunca o input direto.Princípio do menor privilégio: cada usuário só acessa o que precisa.
GRANT SELECT, INSERT ON pedidos TO app_user;
REVOKE DELETE ON pedidos FROM app_user;Sem backup testado, não há dados. Automatize e teste a restauração.
-- PostgreSQL
-- pg_dump -Fc meubanco > backup.dump
-- pg_restore -d meubanco backup.dump
-- Point-in-time recovery com WAL para restaurar até um instanteEscale leituras com réplicas; escale escrita/volume dividindo os dados (sharding) por uma chave.
-- Réplica de leitura: app manda SELECTs para o replica, writes para o primary.
-- Sharding: cliente_id % 4 decide em qual shard a linha vive.
-- (configuração de infraestrutura, não um comando único)