Arquitetura de Banco de Dados
Um guia completo e agnóstico de fornecedor: modelagem, indexação, transações, alta disponibilidade, escalabilidade e segurança — os fundamentos que sustentam qualquer banco de dados, relacional ou não-relacional. Cada tópico traz teoria, boas práticas e links para referências de qualidade. Estudos aprofundados de bancos específicos (relacionais e não-relacionais) vivem em páginas próprias, ligadas a partir daqui conforme forem publicadas.
Fundamentos
O vocabulário e os trade-offs que sustentam todo o resto
Um Sistema Gerenciador de Banco de Dados (SGBD) é o software que fica entre a aplicação e os dados em disco, garantindo que leituras e escritas sejam corretas, duráveis e (na maioria dos casos) simultâneas para múltiplos clientes. Entender suas camadas internas ajuda a raciocinar sobre qualquer decisão de arquitetura mais adiante.
Não existe um "melhor" modelo de dados — existe o modelo mais adequado ao formato dos dados e ao padrão de acesso da aplicação. Conhecer as cinco famílias principais evita forçar um problema no modelo errado.
ACID e BASE são dois conjuntos de garantias opostos que um banco pode oferecer. Nenhum dos dois é "certo" — são pontos diferentes no mesmo espectro entre consistência imediata e disponibilidade/desempenho sob carga distribuída.
O teorema CAP diz que um sistema distribuído não pode garantir simultaneamente Consistência, Disponibilidade e Tolerância a Partição de rede — quando a rede falha (e ela vai falhar), é preciso escolher entre consistência e disponibilidade.
Partição → escolha entre Availability e Consistency; Else (operação normal) → escolha entre Latency e Consistency.Modelagem de Dados
Desenhar o schema para o problema certo
Normalização é o processo de organizar colunas e tabelas para reduzir redundância e evitar anomalias de atualização — quando o mesmo dado precisa ser alterado em vários lugares ao mesmo tempo para continuar correto.
-- Não normalizado: nome e telefone do cliente repetidos em cada pedido -- pedido(id, cliente_nome, cliente_telefone, produto, valor) -- Normalizado (3FN): cliente e pedido separados, sem redundância CREATE TABLE cliente (id INTEGER PRIMARY KEY, nome TEXT, telefone TEXT); CREATE TABLE pedido (id INTEGER PRIMARY KEY, cliente_id INTEGER REFERENCES cliente(id), produto TEXT, valor NUMERIC);
Uma chave primária identifica cada registro de forma única; uma chave estrangeira referencia a chave primária de outra tabela, formando o relacionamento entre entidades e a base da integridade referencial.
CASCADE, RESTRICT, SET NULL). A cardinalidade de um relacionamento (1:1, 1:N, N:N) determina onde a chave estrangeira vive: numa relação 1:N a chave fica do lado "N"; numa relação N:N é preciso uma tabela de associação intermediária guardando um par de chaves estrangeiras. Escolher mal a cardinalidade no desenho inicial é uma das causas mais comuns de retrabalho de schema mais tarde.Desnormalizar é aceitar redundância controlada em troca de menos joins e leituras mais rápidas. É uma escolha deliberada — nunca o resultado de simplesmente não ter modelado o schema.
Modelagem orientada a acesso (access pattern modeling) inverte a ordem tradicional: em vez de modelar entidades e depois escrever queries, você lista as queries que a aplicação vai fazer e desenha o schema para atendê-las bem.
Indexação e Performance
Fazer o banco encontrar dados sem varrer tudo
Um índice é uma estrutura de dados extra, guardada ao lado da tabela, que permite encontrar registros por um valor de coluna sem varrer a tabela inteira — na prática, o mesmo papel do índice remissivo de um livro.
BETWEEN, >) porque não preserva ordem. Todo índice tem um custo: cada escrita na tabela precisa também atualizar cada índice que cobre a coluna alterada — mais índices significa leituras mais rápidas e escritas mais lentas.-- Sem índice: varredura completa da tabela (custo O(n)) SELECT * FROM pedido WHERE cliente_id = 12345; -- Com índice na coluna de busca: custo O(log n) CREATE INDEX idx_pedido_cliente ON pedido (cliente_id);
Nem toda coluna se beneficia de um índice. Quanto mais uma coluna diferencia linhas entre si (alta seletividade/cardinalidade), mais útil é indexá-la — colunas com poucos valores distintos raramente compensam o custo do índice.
ANALYZE) após grandes cargas de dados devolve ao otimizador uma visão real da distribuição de valores.
O comando EXPLAIN (com variações de nome entre bancos) mostra o plano que o otimizador de query escolheu para executar
uma consulta — a única forma confiável de saber se um índice está realmente sendo usado.
A maior parte dos problemas de performance em produção não vem de configuração de servidor — vem de um punhado de padrões de acesso conhecidos e evitáveis desde o design.
JOIN ou carregamento em lote.SELECT *: busca colunas que a aplicação não usa, aumenta I/O e impede covering indexes — sempre liste as colunas necessárias.OFFSET: fica progressivamente mais lenta em páginas altas, porque o banco ainda processa e descarta todas as linhas anteriores — prefira paginação por cursor/chave (keyset pagination).WHERE YEAR(data) = 2026 impede o uso do índice em data — reescreva como um intervalo (data >= '2026-01-01').Um índice composto guarda mais de uma coluna, na ordem em que foi definido — e essa ordem determina quais queries conseguem realmente usar o índice.
(a, b, c) funciona como um dicionário ordenado primeiro por a, depois por b dentro de cada valor de a, e assim por diante — por isso ele acelera buscas por a, por a+b ou por a+b+c, mas não uma busca só por b ou só por c. Um covering index é um índice que inclui todas as colunas que a query pede, permitindo que o banco responda inteiramente pelo índice, sem tocar a tabela — a otimização de leitura mais poderosa disponível, porque elimina o segundo acesso a disco (o chamado "table lookup").=), depois as usadas em intervalo (>, BETWEEN), e por último as usadas só em ORDER BY.Além de indexar uma coluna inteira, é possível indexar apenas um subconjunto de linhas (índice parcial) ou o resultado de uma expressão computada sobre a coluna (índice funcional).
-- Índice parcial: só cobre as linhas realmente consultadas com frequência CREATE INDEX idx_pedido_pendente ON pedido (cliente_id) WHERE status = 'pendente'; -- Índice funcional: indexa o resultado da expressão, não a coluna crua CREATE INDEX idx_usuario_email_lower ON usuario (LOWER(email));
A B-tree cobre a maioria dos casos de uso, mas alguns padrões de consulta têm estruturas de índice dedicadas, mais eficientes para o problema específico que resolvem.
AND/OR — comum em cargas analíticas, raro em bancos transacionais de alta escrita, porque atualizar um bitmap é caro. Índice de texto completo (full-text) tokeniza o conteúdo textual e mantém uma estrutura invertida (palavra → lista de registros que a contêm), permitindo busca por relevância em vez de correspondência exata de substring. Índice espacial (como R-tree) organiza dados geométricos por proximidade, acelerando consultas do tipo "o que está dentro deste raio" — impossível de responder eficientemente com uma B-tree comum, que só entende ordem linear, não posição no espaço.Índices não são "configure e esqueça": eles fragmentam com o tempo, ocupam espaço em disco e desaceleram toda escrita na tabela que cobrem.
INSERT, UPDATE ou DELETE precisa atualizar não só a tabela, mas cada índice que cobre uma coluna alterada — esse custo extra, chamado write amplification, cresce linearmente com o número de índices na tabela. Índices também fragmentam ao longo do tempo, especialmente após muitas exclusões e atualizações, degradando a eficiência da B-tree até uma reconstrução periódica ser necessária. Um problema comum e silencioso é o "índice não usado": criado para uma query que não existe mais, ou redundante com outro índice composto que já cobre o mesmo caso — sem revisão periódica das estatísticas de uso de índice, esses índices acumulam custo de escrita sem nenhum benefício de leitura em troca.Transações e Consistência
O que acontece quando dois clientes escrevem ao mesmo tempo
Uma transação agrupa várias operações num bloco que deve ser tratado como uma unidade única — ou tudo é aplicado, ou nada é.
COMMIT), a mudança sobrevive a uma queda do processo ou do servidor — tipicamente porque já foi escrita no write-ahead log antes da confirmação retornar para o cliente.BEGIN; UPDATE conta SET saldo = saldo - 100 WHERE id = 1; UPDATE conta SET saldo = saldo + 100 WHERE id = 2; -- se qualquer UPDATE falhar, nenhum dos dois é aplicado COMMIT;
O nível de isolamento define quanto uma transação pode "ver" do trabalho não confirmado de outra — quanto mais isolamento, mais correção, mas menos concorrência e desempenho.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN; -- operações que exigem a garantia mais forte de isolamento COMMIT;
Controle de concorrência é o mecanismo interno que implementa os níveis de isolamento. As duas abordagens dominantes são bloqueio pessimista e controle multiversão (MVCC).
Quando dados são replicados entre múltiplos nós, é preciso decidir se toda leitura reflete a escrita mais recente (consistência forte) ou se réplicas podem ficar temporariamente desatualizadas (consistência eventual).
Alta Disponibilidade
Continuar operando quando uma parte falha
Na replicação líder-seguidor (leader-follower, também chamada primário-réplica), todas as escritas passam por um único nó líder, que propaga as mudanças para um ou mais seguidores.
Antes de escolher uma estratégia de backup, é preciso definir quanto dado a empresa tolera perder e quanto tempo tolera ficar fora do ar — sem essas duas metas, qualquer estratégia é um chute.
Quando um único líder vira gargalo geográfico ou de escrita, existem duas alternativas — cada uma trocando simplicidade por mais disponibilidade de escrita.
W + R > N, garante-se que toda leitura enxergue ao menos uma cópia da escrita mais recente.Failover é o processo de promover um seguidor a novo líder quando o líder atual falha. Parece simples, mas é uma das operações mais arriscadas de um sistema distribuído.
Escalabilidade e Distribuição
Crescer além do que uma máquina aguenta
Escalar verticalmente significa dar mais CPU, memória e I/O a um único servidor. Escalar horizontalmente significa distribuir carga entre múltiplos servidores. Um sempre tem um teto; o outro tem mais complexidade.
Na maioria das aplicações, leituras superam escritas por uma ordem de grandeza ou mais — por isso escalar leitura com réplicas costuma ser o primeiro passo antes de qualquer sharding.
Um cache guarda resultados já computados em memória, na frente do banco, para responder às consultas mais frequentes sem tocar o disco. O padrão de invalidação escolhido é o que determina a corretude do sistema.
Particionamento é o conceito geral: dividir os dados de uma tabela grande em partes menores e mais gerenciáveis, chamadas partições. Sharding é o nome dado especificamente ao particionamento distribuído — quando cada partição vive num nó físico diferente. Todo sharding é particionamento, mas nem todo particionamento é sharding.
DELETE lento linha a linha. O sharding (particionamento distribuído) vai além: cada partição é hospedada num nó físico diferente, permitindo que o volume de dados e a carga de escrita cresçam além do que uma única máquina suportaria — ao custo de exigir coordenação entre nós para qualquer consulta que precise tocar mais de um shard, e de tornar joins entre shards caros ou impraticáveis.Segurança
Parte do desenho da arquitetura, não uma etapa final
Injeção de código acontece quando entrada do usuário é inserida diretamente numa query, permitindo que o atacante altere a estrutura da consulta em vez de apenas fornecer um valor.
-- Inseguro: concatenação de entrada do usuário na query query = "SELECT * FROM usuario WHERE email = '" + entrada + "'" -- Seguro: query parametrizada — o valor nunca é interpretado como SQL query = "SELECT * FROM usuario WHERE email = ?" executar(query, [entrada])
Autenticação prova quem está se conectando; autorização decide o que essa identidade pode fazer. São camadas distintas e ambas precisam de desenho deliberado desde o primeiro schema de permissões.
CREATE ROLE app_leitura; GRANT SELECT ON pedido, cliente TO app_leitura; CREATE ROLE app_escrita; GRANT SELECT, INSERT, UPDATE ON pedido TO app_escrita; -- app_escrita não recebe DELETE nem acesso a outras tabelas
Criptografia em repouso protege dados armazenados em disco caso o meio físico seja comprometido; criptografia em trânsito protege dados enquanto trafegam entre a aplicação e o banco.
Duas camadas complementares de segurança: registrar quem fez o quê (para investigar depois) e restringir quem consegue sequer se conectar (para reduzir a superfície de ataque antes de qualquer investigação ser necessária).
Operação e Observabilidade
Manter tudo saudável depois de colocar em produção
Um pequeno conjunto de métricas, observado continuamente, detecta a maioria dos problemas antes que virem indisponibilidade — o objetivo é alertar cedo, não só diagnosticar depois do fato.
Abrir e fechar uma conexão com o banco tem um custo real de rede e de processo — um pool de conexões mantém um conjunto de conexões já estabelecidas e as reutiliza entre requisições.
Capacity planning é projetar, a partir de métricas de crescimento histórico, quando a infraestrutura atual deixará de suportar a carga — com folga suficiente para agir antes que isso vire incidente.
Um runbook é um procedimento documentado e testado para responder a um tipo específico de falha — escrito com calma antes do incidente, para ser seguido sob pressão durante ele.