Quase todo software que você usa guarda alguma coisa em algum lugar. O carrinho de compras que continua lá quando você volta no dia seguinte, o saldo da conta digital, a conversa de ontem no aplicativo de mensagens, o placar do jogo, o histórico de corridas do aplicativo de transporte. Esse "algum lugar" é, quase sempre, um banco de dados.
Existe um detalhe que pouca gente percebe no começo da carreira: o banco de dados costuma ser a parte mais duradoura de um sistema. Frameworks vêm e vão, o front-end é reescrito a cada poucos anos, a arquitetura migra de monólito para microsserviços (e às vezes volta). Mas os dados, e o esquema que os organiza, atravessam todas essas gerações. É comum encontrar empresas rodando, hoje, consultas escritas contra tabelas criadas há mais de uma década.
Por isso, entender banco de dados de verdade, e não só "saber fazer um SELECT", é uma das habilidades com melhor retorno para quem desenvolve. Um índice bem escolhido transforma uma consulta de vários segundos em poucos milissegundos. Uma transação mal delimitada cobra duas vezes o cartão de um cliente. Uma concatenação de string ingênua abre a porta para um SQL injection. Um backup que nunca foi testado vira um pesadelo exatamente no pior dia possível.
Este guia é longo de propósito. Ele começa pelos fundamentos, o que é um banco, como ele funciona por dentro, o modelo relacional e a normalização,, passa por SQL em profundidade nos três bancos relacionais mais usados no mercado (PostgreSQL, MySQL e SQL Server), entra no mundo NoSQL com MongoDB e Redis e termina com um guia de decisão e cheat sheets enormes, no estilo "pôster", para consulta rápida.
O que você vai aprender
- O que é um banco de dados e um SGBD, e por que eles existem (em vez de simplesmente salvarmos arquivos);
- Como um banco funciona por dentro: páginas, buffer pool, log de transações, otimizador de consultas e índices;
- O modelo relacional, chaves, constraints e normalização, com um exemplo resolvido passo a passo;
- Transações, ACID, níveis de isolamento, anomalias de concorrência e deadlocks;
- SQL em profundidade: DDL, DML, consultas, JOINs, subconsultas, CTEs e funções de janela;
- Índices e performance: planos de execução, consultas SARGable, o problema N+1 e paginação eficiente;
- Views, procedures, funções, triggers, segurança (incluindo SQL injection) e backup;
- NoSQL: por que surgiu, teorema CAP, PACELC, consistência eventual e as famílias de bancos;
- MongoDB e Redis na prática, com dezenas de comandos comentados;
- Como decidir entre SQL e NoSQL, e por que, muitas vezes, a resposta é "os dois";
- Cheat sheets de SQL, PostgreSQL, MySQL, SQL Server, MongoDB e Redis.
O que é um banco de dados (e o que é um SGBD)
Um banco de dados é uma coleção organizada de dados relacionados, guardada de forma persistente e estruturada para ser consultada e atualizada com eficiência. "Organizada" é a palavra que importa: uma pasta cheia de planilhas soltas também guarda dados, mas não é isso que chamamos de banco de dados.
O software que cria, mantém e dá acesso a esse banco é o SGBD, Sistema Gerenciador de Banco de Dados (em inglês, DBMS, Database Management System). PostgreSQL, MySQL, SQL Server, Oracle, SQLite, MongoDB e Redis são SGBDs. No dia a dia as duas palavras se misturam, é comum dizer "o nosso banco é PostgreSQL",, e tudo bem, desde que você saiba separar as duas ideias quando precisar.
Vale conhecer também a hierarquia de nomes que aparece em quase todo SGBD relacional:
Servidor / instância → um processo do SGBD rodando (ex.: PostgreSQL na porta 5432)
└── Banco de dados → ex.: loja
└── Schema → um "namespace": public (PostgreSQL), dbo (SQL Server)
├── Tabelas → ex.: clientes, pedidos
├── Views
├── Índices
└── Funções e proceduresOs detalhes variam. No MySQL, DATABASE e SCHEMA são sinônimos (CREATE SCHEMA loja e CREATE DATABASE loja fazem a mesma coisa). No PostgreSQL, uma instância hospeda vários bancos, cada banco tem seus schemas, e uma consulta não enxerga tabelas de outro banco sem extensões como postgres_fdw. No SQL Server, dá para consultar outro banco da mesma instância com nomes de três partes, como loja.dbo.clientes.
Arquivos soltos vs. banco de dados
Imagine uma pequena loja que controla vendas numa planilha compartilhada, ou num arquivo CSV que dois sistemas leem e escrevem. Funciona... até o dia em que não funciona mais. A tabela abaixo resume os problemas que aparecem e como um SGBD os resolve.
O que um SGBD faz por você
Quando você escreve SELECT * FROM pedidos WHERE cliente_id = 42, um volume enorme de engenharia trabalha por trás dessa linha. Um SGBD moderno cuida de:
- Persistência e durabilidade: o que foi confirmado sobrevive a quedas de energia, travamentos e reinicializações.
- Concorrência: centenas ou milhares de conexões lendo e escrevendo ao mesmo tempo sem corromper nada.
- Integridade: tipos, chaves e constraints garantem que dados inválidos nem entrem.
- Linguagem de consulta e otimização: você descreve o que quer; o SGBD descobre como buscar da forma mais barata.
- Índices: estruturas auxiliares que evitam ler a tabela inteira a cada pergunta.
- Transações: grupos de operações que acontecem por completo ou não acontecem.
- Segurança: autenticação, autorização, criptografia e auditoria.
- Operação: backup, restauração, replicação, monitoramento.
- Independência de dados: a aplicação não precisa saber como os bytes estão organizados no disco. O SGBD pode mudar o armazenamento, criar um índice ou reorganizar arquivos, e o seu
SELECTcontinua funcionando igual. Essa ideia, que hoje parece óbvia, foi uma revolução, e é o assunto da próxima seção.
Uma breve história: de fitas magnéticas ao SQL distribuído
Anos 1960 Modelos navegacionais: hierárquico (IBM IMS) e em rede
(IDS, de Charles Bachman; padronizado pelo CODASYL)
1970 E. F. Codd publica "A Relational Model of Data for Large Shared Data Banks"
1974 Na IBM (projeto System R), nasce a SEQUEL, a linguagem que viraria SQL
1979 Oracle V2: um dos primeiros SGBDs relacionais comerciais baseados em SQL
1986 Primeiro padrão SQL da ANSI (adotado pela ISO em 1987)
1995-2000 Onda open source: MySQL (1995), PostgreSQL ganha esse nome (1996), SQLite (2000)
2006-2007 Artigos do Bigtable (Google) e do Dynamo (Amazon)
2009 O termo "NoSQL" se populariza; MongoDB e Redis são lançados
2011+ NewSQL / SQL distribuído: Spanner (artigo de 2012), CockroachDB, TiDB, YugabyteDB
Hoje Convergência: relacionais com JSON e busca vetorial, NoSQL com transaçõesNos anos 1960, os bancos eram navegacionais. No modelo hierárquico (como o IMS, da IBM, associado ao programa Apollo), os registros formavam árvores; no modelo em rede (padronizado pelo comitê CODASYL e inspirado no IDS de Charles Bachman), formavam grafos ligados por ponteiros. Para responder a uma pergunta, o programador escrevia código que percorria esses ponteiros, registro a registro. Uma pergunta nova exigia um programa novo, e mudar a estrutura física dos dados quebrava os programas existentes.
Em 1970, Edgar F. Codd, pesquisador da IBM, publicou o artigo que mudou a área: A Relational Model of Data for Large Shared Data Banks. A proposta era representar os dados como relações, conjuntos de tuplas, o que chamamos hoje de tabelas, e consultá-los de forma declarativa, com base em teoria dos conjuntos e lógica de predicados. O programa diz quais dados quer; o sistema decide como encontrá-los. Com isso, a organização física podia mudar sem quebrar as aplicações.
A própria IBM demorou a apostar na ideia, mas o projeto de pesquisa System R provou que ela funcionava na prática e criou a linguagem SEQUEL (Structured English Query Language), de Donald Chamberlin e Raymond Boyce. Por um problema de marca registrada, o nome virou SQL, e até hoje há quem pronuncie "síquel" e quem diga "és-quiú-él". Em paralelo, na Universidade da Califórnia em Berkeley, Michael Stonebraker liderava o Ingres, cujo sucessor, o Postgres (de "post-Ingres"), deu origem ao PostgreSQL. A Oracle lançou seu produto comercial em 1979, a IBM lançou o DB2 em 1983, e o SQL virou padrão ANSI em 1986.
Nas décadas seguintes, os bancos relacionais dominaram praticamente tudo. A virada veio com a web em escala planetária: empresas como Google e Amazon precisavam de sistemas espalhados por milhares de máquinas baratas, que continuassem funcionando com falhas constantes de hardware e rede. Os artigos do Bigtable (2006) e do Dynamo (2007) descreveram soluções que abriam mão de partes do modelo relacional, JOINs, transações completas, esquema fixo, em troca de escala horizontal e disponibilidade.
Em 2009, um encontro em São Francisco sobre bancos distribuídos não relacionais popularizou o termo NoSQL, mais tarde reinterpretado como "Not only SQL". No mesmo ano surgiram o MongoDB e o Redis; o Cassandra, criado no Facebook, já havia sido aberto em 2008. Pouco depois veio o movimento inverso: sistemas chamados de NewSQL ou SQL distribuído (o Spanner, do Google, e depois CockroachDB, TiDB e YugabyteDB) mostraram que dá para ter SQL, transações ACID e escala horizontal, com outros custos.
Hoje as fronteiras estão borradas: o PostgreSQL guarda documentos JSON com índices e faz busca vetorial com extensões; o MongoDB tem transações ACID multi-documento e validação de esquema. A pergunta deixou de ser "qual tecnologia é melhor" e passou a ser "qual modelo de dados e quais garantias o meu problema exige".
Como um banco de dados funciona por dentro
Você não precisa conhecer a engenharia interna de um SGBD para escrever um SELECT. Mas conhecer o básico explica quase tudo o que parece "mágico" ou "misterioso" no dia a dia: por que um índice acelera uma consulta e atrasa outra, por que o banco adora memória RAM, por que um COMMIT pode demorar, por que duas transações às vezes se bloqueiam. Esta seção é um mapa; os detalhes de cada parte aparecem ao longo do guia.
┌──────────────────────────────────────────────────────────────┐
│ Cliente: sua aplicação, psql, DBeaver, um ORM... │
└──────────────────────────────┬───────────────────────────────┘
│ SQL em texto (pela rede ou socket local)
┌──────────────────────────────▼───────────────────────────────┐
│ Processador de consultas │
│ parser → analisador → otimizador → executor │
├──────────────────────────────────────────────────────────────┤
│ Gerenciador de transações Gerenciador de locks │
│ (ACID, MVCC, commit/rollback) (quem espera por quem) │
├──────────────────────────────────────────────────────────────┤
│ Motor de armazenamento (storage engine) │
│ buffer pool (páginas em memória) · índices · log (WAL) │
└──────────────────────────────┬───────────────────────────────┘
│ leituras e escritas de páginas
┌──────────────────────────────▼───────────────────────────────┐
│ Disco: arquivos de dados + arquivos de log │
└──────────────────────────────────────────────────────────────┘O motor de armazenamento (storage engine) é a camada que efetivamente grava e lê bytes. No MySQL ele é plugável: o padrão, desde a versão 5.5, é o InnoDB, transacional; o antigo MyISAM não tem transações nem chaves estrangeiras e hoje só aparece em sistemas legados. O PostgreSQL tem um armazenamento principal chamado heap, e o SQL Server combina o armazenamento tradicional por linhas com opções como índices columnstore e tabelas em memória.
Páginas: a unidade básica de armazenamento
Nenhum SGBD sério lê "uma linha" do disco. Discos e sistemas operacionais trabalham com blocos, então o banco organiza tudo em páginas (ou blocos) de tamanho fixo:
Cada página tem um cabeçalho, um vetor de ponteiros (os slots) e as linhas propriamente ditas. Os ponteiros crescem a partir de uma ponta e as linhas a partir da outra, com o espaço livre no meio:
┌──────────────────────────────────────────────┐
│ cabeçalho (LSN, espaço livre, checksum...) │
├──────────────────────────────────────────────┤
│ slots: [1] [2] [3] [4] → │
│ │
│ espaço livre │
│ │
│ ← [linha 4] [linha 3] │
│ [linha 2] [linha 1] │
└──────────────────────────────────────────────┘A consequência prática é enorme: o custo de uma consulta é, em primeira aproximação, o número de páginas que ela precisa tocar. Uma tabela com um milhão de linhas de uns 100 bytes ocupa algo na casa de 12 mil páginas de 8 KB. Ler a tabela inteira (um full scan) significa tocar todas elas; encontrar uma linha por um índice significa tocar três ou quatro. É daí que vem a diferença entre segundos e milissegundos.
Buffer pool: a memória que evita o disco
Ler do disco é lento, mesmo em SSDs NVMe, é ordens de grandeza mais lento que ler da RAM. Por isso todo SGBD mantém um cache de páginas em memória, chamado de buffer pool (ou buffer cache). Quando uma consulta precisa de uma página, o banco procura primeiro no buffer pool (cache hit); só vai ao disco se ela não estiver lá (cache miss).
- No PostgreSQL, o cache próprio se chama
shared_buffers, e o banco também se apoia no cache de arquivos do sistema operacional. - No MySQL/InnoDB, é o
innodb_buffer_pool_size, em servidores dedicados, costuma receber a maior parte da RAM. - No SQL Server, o buffer pool usa a memória do processo, limitada pela configuração
max server memory (MB).
Páginas alteradas em memória e ainda não gravadas no disco são chamadas de páginas sujas (dirty pages). Quando a memória acaba, o banco escolhe páginas pouco usadas para descartar (gravando antes as sujas). É por isso que bancos "gostam" de RAM: quanto mais do conjunto de dados quente couber em memória, menos idas ao disco.
Write-ahead log: como o banco sobrevive a uma queda de energia
Aqui está um problema difícil: se o banco confirmasse cada transação gravando imediatamente todas as páginas alteradas nos arquivos de dados, seria lentíssimo (escritas aleatórias por todo o disco) e perigoso (uma queda de energia no meio deixaria páginas pela metade).
A solução usada por praticamente todos os bancos relacionais é o write-ahead logging (WAL): antes de alterar os arquivos de dados, o banco descreve a alteração num log sequencial, só de acréscimo (append-only). No COMMIT, basta garantir que esse registro de log chegou fisicamente ao disco. As páginas de dados podem ser gravadas depois, com calma, em lote.
UPDATE contas SET saldo = saldo - 100 WHERE id = 1; COMMIT;
1. A página da conta 1 é carregada no buffer pool (se ainda não estiver lá)
2. A alteração é descrita num registro de log, em memória
3. A página é alterada em memória e marcada como "suja"
4. COMMIT → o registro de log é gravado e sincronizado no disco (fsync)
5. O cliente recebe a confirmação do COMMIT
6. Mais tarde, um checkpoint grava as páginas sujas nos arquivos de dadosSe o servidor cair entre os passos 5 e 6, nada se perde: ao reiniciar, o banco lê o log a partir do último checkpoint e refaz (redo) o que foi confirmado, descartando o que não foi. Cada SGBD dá um nome a esse log:
- PostgreSQL: WAL, no diretório
pg_wal; - MySQL/InnoDB: redo log (além do undo log, usado para desfazer alterações e montar versões antigas das linhas, e do binary log, usado em replicação);
- SQL Server: transaction log, o arquivo
.ldfde cada banco.
O log é também a base da replicação (réplicas recebem e aplicam o log do primário) e da recuperação até um ponto no tempo (point-in-time recovery), que você verá na seção de backup.
A vida de uma consulta: do texto SQL ao resultado
Quando o banco recebe uma consulta, ela passa por um pequeno "compilador" antes de ser executada:
"SELECT c.nome, SUM(...) FROM clientes c JOIN pedidos p ... WHERE ..."
│
▼
┌──────────────┐ Verifica a sintaxe e gera uma árvore sintática.
│ Parser │ Erros como "syntax error at or near FORM" nascem aqui.
└──────┬───────┘
▼
┌──────────────┐ Resolve nomes: a tabela existe? a coluna existe?
│ Analisador │ os tipos são compatíveis? você tem permissão?
└──────┬───────┘
▼
┌──────────────┐ Expande views e aplica regras de reescrita.
│ Reescritor │
└──────┬───────┘
▼
┌──────────────┐ Gera planos alternativos, estima o custo de cada um
│ Otimizador │ usando estatísticas das tabelas e escolhe o mais barato.
└──────┬───────┘
▼
┌──────────────┐ Executa o plano: varreduras, buscas em índice,
│ Executor │ junções, ordenações, agregações...
└──────┬───────┘
▼
linhas de resultado → clienteO coração desse processo é o otimizador (ou planejador). Para uma mesma consulta existem dezenas, às vezes milhares, de planos possíveis: ler a tabela inteira ou usar um índice? Juntar clientes com pedidos primeiro, ou pedidos com itens_pedido? Qual algoritmo de junção usar? O otimizador decide com base em estatísticas, quantas linhas cada tabela tem, quantos valores distintos cada coluna tem, histogramas da distribuição dos valores. Estatísticas desatualizadas levam a planos ruins, e é por isso que os bancos as recalculam periodicamente (e por isso existem comandos como ANALYZE e UPDATE STATISTICS).
Os três algoritmos clássicos de junção aparecem em todo plano de execução, então vale conhecê-los desde já:
O MySQL, por muito tempo, só tinha variações de nested loop; o hash join chegou na versão 8.0.18. PostgreSQL e SQL Server implementam os três há muitos anos.
Outro detalhe útil: planejar custa tempo. Por isso os bancos reaproveitam planos. O SQL Server mantém um cache de planos automático; o PostgreSQL reaproveita planos em prepared statements. Reaproveitar é ótimo, mas tem um efeito colateral famoso no SQL Server, o parameter sniffing: um plano gerado para um valor de parâmetro raro pode ser péssimo para um valor comum, e vice-versa.
Índices em uma frase
Um índice é uma estrutura auxiliar, mantida em ordem, que aponta para as linhas da tabela, exatamente como o índice remissivo no fim de um livro, que evita que você leia o livro inteiro para achar a página que fala de "transações". A estrutura mais comum é a B-tree (na prática, uma variação chamada B+tree), padrão em PostgreSQL, MySQL e SQL Server. Ela mantém as chaves ordenadas em poucos níveis, de modo que achar um valor entre milhões exige tocar só algumas páginas. A seção de performance explica os detalhes, os tipos de índice e quando eles atrapalham.
Concorrência: locks e MVCC
Um banco atende muitas conexões ao mesmo tempo, e duas delas podem querer a mesma linha. Existem duas grandes estratégias para lidar com isso:
- Locks (bloqueio pessimista): quem vai ler pede um lock compartilhado; quem vai escrever pede um lock exclusivo. Leitores e escritores se bloqueiam mutuamente. Simples de entender, mas gera espera.
- MVCC (Multi-Version Concurrency Control): cada escrita cria uma nova versão da linha em vez de sobrescrever a antiga. Cada transação enxerga um snapshot consistente, as versões que já estavam confirmadas quando ela (ou a instrução) começou. Resultado: leitores não bloqueiam escritores e escritores não bloqueiam leitores. Dois escritores na mesma linha ainda precisam esperar um pelo outro.
Tempo ↓ (conta 1 começa com saldo = 500)
T1: BEGIN;
T1: SELECT saldo FROM contas WHERE id = 1; → 500 (vê a versão v1)
T2: BEGIN;
T2: UPDATE contas SET saldo = 400 WHERE id = 1; (cria a versão v2, não confirmada)
T1: SELECT saldo FROM contas WHERE id = 1; → 500 (continua vendo v1; não espera T2)
T2: COMMIT;
T1: SELECT saldo FROM contas WHERE id = 1; → 400 em READ COMMITTED
→ 500 em REPEATABLE READ (snapshot fixo)Cada banco implementa isso de um jeito:
- PostgreSQL: MVCC com as versões antigas guardadas na própria tabela. Versões que ninguém mais enxerga viram "tuplas mortas" e são limpas pelo
VACUUM(normalmente pelo autovacuum, automático). - MySQL/InnoDB: MVCC em que as versões antigas são reconstruídas a partir do undo log.
- SQL Server: por padrão usa locks para leituras em
READ COMMITTED, mas pode usar versionamento de linhas se você ligarREAD_COMMITTED_SNAPSHOTou usar o nívelSNAPSHOT(as versões ficam notempdb, ou no próprio banco quando o Accelerated Database Recovery está ativo).
Os locks também têm granularidade: podem valer para uma linha, uma página ou uma tabela inteira. O SQL Server, por exemplo, faz lock escalation: quando uma única instrução acumula locks demais num mesmo objeto (o limiar documentado é da ordem de 5.000), ele troca milhares de locks de linha por um lock de tabela, o que economiza memória, mas pode bloquear outras sessões. Você vai ver como tudo isso se traduz em comportamento visível na seção de transações.
O modelo relacional
Em 1970, Edgar F. Codd, pesquisador da IBM, propôs organizar os dados em relações: o que chamamos hoje de tabelas. A ideia é simples e poderosa: dados organizados em linhas e colunas, ligados por valores, e uma linguagem declarativa em que você diz o que quer, não como buscar.
Tabelas, linhas e colunas
Chaves e restrições
- Chave primária (
PRIMARY KEY): identifica cada linha de forma única. Não pode repetir nem ser nula. - Chave estrangeira (
FOREIGN KEY): coluna que aponta para a chave primária de outra tabela, garantindo que a referência exista (integridade referencial). UNIQUE: o valor não pode se repetir (um e-mail por cliente, por exemplo).NOT NULL: o campo é obrigatório.CHECK: uma regra de validação (preco > 0).DEFAULT: valor usado quando nenhum é informado.
Restrições no banco são a última linha de defesa: mesmo que a aplicação tenha um bug, o banco recusa dados inválidos.
Relacionamentos
clientes 1 ──────< N pedidos 1 ──────< N itens_pedido N >────── 1 produtos
│ │ │ │
id (PK) id (PK) pedido_id (FK) id (PK)
nome cliente_id (FK) produto_id (FK) nome
email (UNIQUE) criado_em quantidade preco
status preco_unitario- 1:1 (um para um): um usuário tem um perfil. Normalmente a tabela filha usa a mesma chave da mãe.
- 1:N (um para muitos): um cliente tem muitos pedidos. A chave estrangeira fica no lado "muitos" (
pedidos.cliente_id). - N:N (muitos para muitos): um pedido tem muitos produtos, e um produto aparece em muitos pedidos. Resolve-se com uma tabela de junção (
itens_pedido), que também guarda dados da relação (quantidade, preço no momento da compra).
Chave natural ou substituta?
Uma chave natural vem do mundo real (CPF, e-mail); uma substituta (surrogate) é um identificador artificial gerado pelo banco. A prática mais comum é usar chave substituta como primária e garantir a unicidade dos dados naturais com UNIQUE: dados do mundo real mudam (pessoas trocam de e-mail) e às vezes se repetem de formas inesperadas.
Para gerar a chave substituta:
- Inteiro sequencial (
GENERATED ALWAYS AS IDENTITYno PostgreSQL e no padrão SQL,AUTO_INCREMENTno MySQL,IDENTITY(1,1)no SQL Server): pequeno, rápido e ordenado, mas revela o volume de registros e não pode ser gerado fora do banco. - UUID: pode ser gerado em qualquer lugar sem coordenação e não revela contagem, mas ocupa 16 bytes e, se for totalmente aleatório (v4), espalha as inserções pelo índice. O UUIDv7, ordenado pelo tempo, resolve boa parte desse problema.
Normalização
Normalizar é organizar as tabelas para que cada fato seja guardado em um único lugar. Isso evita as anomalias:
- De inserção: não dá para cadastrar um produto sem que alguém o compre.
- De atualização: o preço de um produto aparece em mil linhas; você atualiza 999 e o banco fica inconsistente.
- De exclusão: apagar o último pedido de um cliente apaga também o único registro do endereço dele.
Um exemplo, passo a passo
Imagine uma planilha de pedidos:
1ª forma normal (1FN): valores atômicos, sem listas dentro de uma célula. A coluna produtos guarda vários valores. Separamos em uma linha por produto:
2ª forma normal (2FN): cada coluna depende da chave inteira. A chave agora é (pedido, produto). Mas data e cliente dependem só do pedido, e preco depende só do produto: são dependências parciais. Separamos em pedidos, produtos e itens_pedido.
3ª forma normal (3FN): nenhuma coluna depende de outra coluna que não seja a chave. Em pedidos, email_cliente depende de cliente, não do pedido (dependência transitiva). Criamos clientes e deixamos só cliente_id em pedidos.
O resultado é exatamente o diagrama da seção anterior: clientes, pedidos, produtos e itens_pedido. A forma normal de Boyce-Codd (BCNF) é uma versão um pouco mais estrita da 3FN: todo determinante precisa ser uma chave candidata. Na prática, um modelo em 3FN bem feito quase sempre já está em BCNF.
Quando desnormalizar
Normalização otimiza para escrita e consistência. Às vezes, leituras muito frequentes justificam duplicar dados de propósito: guardar o total do pedido em vez de somar os itens a cada consulta, manter um contador de comentários, ou criar uma tabela de relatórios. Faça isso conscientemente, medindo o ganho, e com uma estratégia para manter a cópia correta (transação, trigger ou job). Repare também que itens_pedido.preco_unitario não é redundância: é o preço no momento da compra, um fato diferente do preço atual do produto.
ACID e transações
Uma transação é um grupo de operações que o banco trata como uma unidade. O exemplo clássico é uma transferência bancária:
BEGIN;
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
UPDATE contas SET saldo = saldo + 100 WHERE id = 2;
COMMIT;Se o servidor cair entre os dois UPDATE, o dinheiro não pode sumir. As garantias que evitam isso têm um nome, ACID:
- Atomicidade: ou tudo acontece, ou nada acontece. Se algo falhar,
ROLLBACKdesfaz o que já foi feito. - Consistência: a transação leva o banco de um estado válido para outro, respeitando todas as restrições.
- Isolamento: transações simultâneas não enxergam os estados intermediários umas das outras (no grau definido pelo nível de isolamento).
- Durabilidade: depois do
COMMIT, o dado sobrevive a quedas de energia (graças ao write-ahead log que vimos antes).
Controlando transações
BEGIN; -- PostgreSQL e MySQL (START TRANSACTION também vale)
-- BEGIN TRANSACTION; -- SQL Server
UPDATE estoque SET quantidade = quantidade - 1 WHERE produto_id = 10;
SAVEPOINT antes_do_frete; -- ponto de retorno parcial
UPDATE pedidos SET frete = 30 WHERE id = 55;
ROLLBACK TO SAVEPOINT antes_do_frete; -- desfaz só o frete (no SQL Server: ROLLBACK TRANSACTION antes_do_frete, com SAVE TRANSACTION antes)
COMMIT; -- confirma o restoSem um BEGIN explícito, a maioria dos clientes roda em autocommit: cada instrução é uma transação própria.
Níveis de isolamento e anomalias
- Leitura suja: ler um dado ainda não confirmado por outra transação.
- Leitura não repetível: ler a mesma linha duas vezes e obter valores diferentes.
- Leitura fantasma: repetir uma consulta e aparecerem ou sumirem linhas.
- Atualização perdida: duas transações leem o mesmo valor, calculam e gravam, e uma sobrescreve a outra.
Padrões de cada banco: PostgreSQL usa Read Committed (e seu Repeatable Read, baseado em snapshot, também impede fantasmas); MySQL/InnoDB usa Repeatable Read; SQL Server usa Read Committed.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- dentro da transação, antes da primeira consulta (PostgreSQL)Evitando a atualização perdida
-- Errado: ler, calcular na aplicação e gravar o valor absoluto
-- Certo: deixar o banco calcular de forma atômica
UPDATE produtos SET estoque = estoque - 1 WHERE id = 10 AND estoque > 0;
-- Ou travar a linha enquanto decide (PostgreSQL e MySQL)
BEGIN;
SELECT estoque FROM produtos WHERE id = 10 FOR UPDATE;
-- ... lógica ...
UPDATE produtos SET estoque = 4 WHERE id = 10;
COMMIT;Deadlocks
Um deadlock acontece quando a transação A espera por um lock de B, e B espera por um lock de A. O banco detecta o ciclo e aborta uma delas com erro. A aplicação deve tentar novamente a transação abortada. Para reduzir a chance: acesse as tabelas e linhas sempre na mesma ordem e mantenha as transações curtas.
A linguagem SQL
SQL (Structured Query Language) nasceu na IBM nos anos 1970 com o nome SEQUEL e virou padrão ANSI/ISO. Todos os bancos relacionais falam SQL, cada um com seu dialeto: o núcleo é o mesmo, mas há diferenças em tipos, funções e recursos.
As categorias de comandos
Tipos de dados nos três principais bancos
Para datas: guarde instantes em UTC (ou com fuso, como TIMESTAMPTZ) e converta para o fuso do usuário só na exibição.
Preparando o ambiente
SQLite: zero instalação
O SQLite é um banco inteiro num único arquivo, embutido em celulares, navegadores e aplicativos. Ótimo para estudar:
sqlite3 loja.dbPostgreSQL e MySQL com Docker
docker run --name pg -e POSTGRES_PASSWORD=senha123 -p 5432:5432 -d postgres
docker exec -it pg psql -U postgres
docker run --name mysql -e MYSQL_ROOT_PASSWORD=senha123 -p 3306:3306 -d mysql
docker exec -it mysql mysql -u root -pAs imagens oficiais postgres e mysql usam a versão estável mais recente quando você não informa uma tag. Em projetos reais, fixe a versão (por exemplo postgres:17) para que todos usem a mesma.
Ferramentas gráficas
- DBeaver: gratuito, conecta em praticamente qualquer banco.
- pgAdmin: o cliente oficial do PostgreSQL.
- SQL Server Management Studio (SSMS): o cliente oficial do SQL Server (Windows).
- Extensão SQL Server (mssql) para VS Code: consultas ao SQL Server dentro do editor.
DDL: criando a estrutura
O esquema de exemplo, usado no resto do guia, em PostgreSQL:
CREATE TABLE clientes (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
cidade VARCHAR(80),
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE produtos (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
preco NUMERIC(10,2) NOT NULL CHECK (preco > 0),
estoque INTEGER NOT NULL DEFAULT 0 CHECK (estoque >= 0)
);
CREATE TABLE pedidos (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
cliente_id BIGINT NOT NULL REFERENCES clientes(id),
status VARCHAR(20) NOT NULL DEFAULT 'aberto',
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE itens_pedido (
pedido_id BIGINT NOT NULL REFERENCES pedidos(id) ON DELETE CASCADE,
produto_id BIGINT NOT NULL REFERENCES produtos(id),
quantidade INTEGER NOT NULL CHECK (quantidade > 0),
preco_unitario NUMERIC(10,2) NOT NULL,
PRIMARY KEY (pedido_id, produto_id)
);As diferenças de dialeto mais comuns: no MySQL, BIGINT AUTO_INCREMENT PRIMARY KEY e DATETIME DEFAULT CURRENT_TIMESTAMP; no SQL Server, BIGINT IDENTITY(1,1) PRIMARY KEY e DATETIME2 DEFAULT SYSUTCDATETIME().
ON DELETE CASCADE apaga os itens quando o pedido é apagado. As outras opções são RESTRICT/NO ACTION (impede a exclusão, o padrão) e SET NULL.
Alterando tabelas
ALTER TABLE clientes ADD COLUMN telefone VARCHAR(20);
ALTER TABLE clientes DROP COLUMN telefone;
ALTER TABLE clientes RENAME COLUMN cidade TO municipio; -- PostgreSQL e MySQL 8
ALTER TABLE produtos ADD CONSTRAINT nome_unico UNIQUE (nome);
ALTER TABLE produtos ALTER COLUMN nome TYPE VARCHAR(200); -- PostgreSQL
-- MySQL: ALTER TABLE produtos MODIFY nome VARCHAR(200) NOT NULL;
-- SQL Server: ALTER TABLE produtos ALTER COLUMN nome NVARCHAR(200) NOT NULL;
-- SQL Server, renomear coluna: EXEC sp_rename 'clientes.cidade', 'municipio', 'COLUMN';DROP, TRUNCATE e DELETE
Índices e views
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
CREATE UNIQUE INDEX idx_clientes_email ON clientes (email);
CREATE VIEW pedidos_resumo AS
SELECT p.id, c.nome AS cliente, p.status, p.criado_em
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id;Uma view é uma consulta salva com nome: você a usa como se fosse uma tabela, e ela sempre reflete os dados atuais.
DML: inserindo, alterando e apagando
INSERT
INSERT INTO clientes (nome, email, cidade)
VALUES ('Ana Souza', 'ana@ex.com', 'São Paulo');
INSERT INTO produtos (nome, preco, estoque) VALUES
('Teclado', 200.00, 15),
('Mouse', 80.00, 40),
('Monitor', 900.00, 5);
-- Devolver o id gerado
INSERT INTO clientes (nome, email) VALUES ('Bruno', 'bruno@ex.com') RETURNING id; -- PostgreSQL
-- SQL Server: INSERT INTO clientes (nome, email) OUTPUT inserted.id VALUES ('Bruno', 'bruno@ex.com');
-- MySQL: SELECT LAST_INSERT_ID(); logo após o INSERT
-- Copiar de uma consulta
INSERT INTO clientes_arquivo (nome, email)
SELECT nome, email FROM clientes WHERE criado_em < '2025-01-01';UPDATE e DELETE
UPDATE produtos SET preco = preco * 1.10 WHERE id = 3;
DELETE FROM pedidos WHERE status = 'cancelado' AND criado_em < now() - INTERVAL '1 year';UPSERT: inserir ou atualizar
-- PostgreSQL
INSERT INTO produtos (id, nome, preco) OVERRIDING SYSTEM VALUE
VALUES (1, 'Teclado', 210.00)
ON CONFLICT (id) DO UPDATE SET preco = EXCLUDED.preco;
-- MySQL
INSERT INTO produtos (id, nome, preco) VALUES (1, 'Teclado', 210.00)
ON DUPLICATE KEY UPDATE preco = VALUES(preco);
-- SQL Server
MERGE produtos AS alvo
USING (VALUES (1, 'Teclado', 210.00)) AS origem (id, nome, preco)
ON alvo.id = origem.id
WHEN MATCHED THEN UPDATE SET preco = origem.preco
WHEN NOT MATCHED THEN INSERT (nome, preco) VALUES (origem.nome, origem.preco);No MySQL 8.0.20 em diante, a forma recomendada é usar um alias (INSERT ... AS novo ON DUPLICATE KEY UPDATE preco = novo.preco), porque VALUES() nessa posição foi descontinuada.
Soft delete
Em vez de apagar, marque como removido: uma coluna removido_em TIMESTAMPTZ nula para registros ativos. Preserva histórico e permite desfazer, mas exige lembrar de filtrar WHERE removido_em IS NULL em todas as consultas (views ajudam).
Consultas em profundidade
SELECT, WHERE e ordenação
SELECT nome, preco
FROM produtos
WHERE preco BETWEEN 50 AND 500
AND nome LIKE 'M%' -- começa com M (ILIKE ignora maiúsculas no PostgreSQL)
AND estoque > 0
ORDER BY preco DESC, nome ASC
LIMIT 10 OFFSET 20; -- PostgreSQL, MySQL e SQLitePaginação no SQL Server: SELECT TOP 10 ... ou, no padrão SQL, ORDER BY preco DESC OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY.
Operadores mais usados: =, <> (diferente), <, >, <=, >=, BETWEEN, IN ('a', 'b'), LIKE (% qualquer sequência, _ um caractere), IS NULL, IS NOT NULL, AND, OR, NOT.
NULL: o valor desconhecido
NULL não é zero nem texto vazio: é "desconhecido". Qualquer comparação com NULL resulta em desconhecido, que o WHERE trata como falso. Por isso WHERE cidade = NULL nunca retorna nada; o certo é WHERE cidade IS NULL.
SELECT nome, COALESCE(cidade, 'Não informada') AS cidade FROM clientes; -- primeiro valor não nulo
SELECT NULLIF(desconto, 0) FROM pedidos; -- NULL se for 0 (evita divisão por zero)CASE
SELECT nome,
CASE
WHEN estoque = 0 THEN 'esgotado'
WHEN estoque < 10 THEN 'acabando'
ELSE 'disponível'
END AS situacao
FROM produtos;Agregações, GROUP BY e HAVING
SELECT c.cidade,
COUNT(*) AS total_clientes,
COUNT(c.email) AS com_email -- COUNT(coluna) ignora NULLs
FROM clientes c
GROUP BY c.cidade
HAVING COUNT(*) >= 10 -- filtra os GRUPOS
ORDER BY total_clientes DESC;WHERE filtra linhas antes de agrupar; HAVING filtra os grupos depois. Funções de agregação: COUNT, SUM, AVG, MIN, MAX.
A ordem lógica de um SELECT
Você escreve o SELECT primeiro, mas o banco avalia nesta ordem:
1. FROM / JOIN → de onde vêm as linhas
2. WHERE → filtra linhas
3. GROUP BY → agrupa
4. HAVING → filtra grupos
5. SELECT → calcula as colunas (e os apelidos)
6. DISTINCT → remove duplicadas
7. ORDER BY → ordena
8. LIMIT / OFFSET → recortaIsso explica por que um apelido criado no SELECT não pode ser usado no WHERE (ainda não existe), mas pode ser usado no ORDER BY.
JOINs explicados com exemplos
Duas tabelas pequenas:
clientes pedidos
+----+-------+ +----+------------+--------+
| id | nome | | id | cliente_id | total |
+----+-------+ +----+------------+--------+
| 1 | Ana | | 10 | 1 | 280.00 |
| 2 | Bruno | | 11 | 1 | 900.00 |
| 3 | Carla | | 12 | 2 | 80.00 |
+----+-------+ | 13 | 9 | 50.00 | ← cliente 9 não existe
+----+------------+--------+(O pedido 13 só existe aqui para ilustrar; com a chave estrangeira do nosso esquema, o banco o recusaria.)
INNER JOIN: só o que casa dos dois lados
SELECT c.nome, p.id, p.total
FROM clientes c
INNER JOIN pedidos p ON p.cliente_id = c.id;Ana | 10 | 280.00
Ana | 11 | 900.00
Bruno | 12 | 80.00LEFT JOIN: tudo da esquerda, e o que casar da direita
SELECT c.nome, p.id, p.total
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id;Ana | 10 | 280.00
Ana | 11 | 900.00
Bruno | 12 | 80.00
Carla | NULL | NULL ← Carla não tem pedidosRIGHT JOIN e FULL OUTER JOIN
RIGHT JOIN é o espelho do LEFT (tudo da direita). FULL OUTER JOIN traz tudo dos dois lados, com NULL onde não houver correspondência: aqui, Carla (sem pedido) e o pedido 13 (sem cliente). O MySQL não tem FULL OUTER JOIN; simula-se com LEFT JOIN ... UNION ... RIGHT JOIN.
Anti-join: quem NÃO tem correspondência
-- Clientes que nunca compraram
SELECT c.nome
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
WHERE p.id IS NULL;
-- Mesma coisa com NOT EXISTS (costuma ser a forma mais clara)
SELECT c.nome
FROM clientes c
WHERE NOT EXISTS (SELECT 1 FROM pedidos p WHERE p.cliente_id = c.id);CROSS JOIN e SELF JOIN
CROSS JOIN combina cada linha de uma tabela com cada linha da outra (produto cartesiano): 3 clientes × 4 pedidos = 12 linhas. Útil para gerar combinações (tamanhos × cores). Um SELF JOIN junta a tabela com ela mesma, por exemplo funcionários e seus gestores na mesma tabela.
Subconsultas, CTEs e funções de janela
Subconsultas
-- Escalar: devolve um único valor
SELECT nome, preco, (SELECT AVG(preco) FROM produtos) AS media FROM produtos;
-- Com IN
SELECT nome FROM clientes
WHERE id IN (SELECT cliente_id FROM pedidos WHERE status = 'pago');
-- Correlacionada: executada (logicamente) para cada linha de fora
SELECT p.nome FROM produtos p
WHERE p.preco > (SELECT AVG(p2.preco) FROM produtos p2);CTEs com WITH
Uma CTE (Common Table Expression) dá nome a uma subconsulta e deixa consultas longas legíveis:
WITH totais AS (
SELECT cliente_id, SUM(ip.quantidade * ip.preco_unitario) AS gasto
FROM pedidos p
JOIN itens_pedido ip ON ip.pedido_id = p.id
GROUP BY cliente_id
)
SELECT c.nome, t.gasto
FROM totais t
JOIN clientes c ON c.id = t.cliente_id
WHERE t.gasto > 1000
ORDER BY t.gasto DESC;CTE recursiva: hierarquias
WITH RECURSIVE arvore AS ( -- no SQL Server, sem a palavra RECURSIVE
SELECT id, nome, pai_id, 1 AS nivel
FROM categorias
WHERE pai_id IS NULL
UNION ALL
SELECT c.id, c.nome, c.pai_id, a.nivel + 1
FROM categorias c
JOIN arvore a ON c.pai_id = a.id
)
SELECT * FROM arvore ORDER BY nivel;Funções de janela
Calculam valores sobre um conjunto de linhas sem agrupar o resultado:
SELECT
cliente_id,
id AS pedido_id,
total,
ROW_NUMBER() OVER (PARTITION BY cliente_id ORDER BY criado_em) AS numero_compra,
SUM(total) OVER (PARTITION BY cliente_id ORDER BY criado_em) AS acumulado,
LAG(total) OVER (PARTITION BY cliente_id ORDER BY criado_em) AS compra_anterior,
RANK() OVER (ORDER BY total DESC) AS ranking_geral
FROM pedidos;ROW_NUMBER: numeração sequencial dentro de cada partição.RANK/DENSE_RANK: ranking com empates (oRANKpula posições depois de um empate; oDENSE_RANKnão).LAG/LEAD: valor da linha anterior / seguinte.SUM() OVER (...): total acumulado.
Um uso clássico: o último pedido de cada cliente, filtrando numero_compra numa CTE.
Operações de conjunto
SELECT email FROM clientes
UNION -- junta e remove duplicados (UNION ALL mantém, e é mais rápido)
SELECT email FROM leads;
SELECT email FROM clientes
INTERSECT -- só o que aparece nos dois
SELECT email FROM newsletter;
SELECT email FROM clientes
EXCEPT -- está no primeiro e não no segundo (MINUS no Oracle)
SELECT email FROM descadastrados;Índices e desempenho
Como um índice B-tree funciona
Sem índice, achar um cliente pelo e-mail exige ler a tabela inteira (full table scan). Um índice B-tree mantém os valores ordenados numa árvore rasa e larga:
[ G | P ]
/ | \
[ B | D ] [ J | M ] [ S | V ]
/ | \ / | \ / | \
folhas ordenadas com o valor e o endereço da linha na tabelaCada nó ocupa uma página e aponta para centenas de filhos, então mesmo milhões de linhas ficam a 3 ou 4 níveis de distância: poucas leituras de página em vez de milhares. As folhas ficam encadeadas, o que torna buscas por intervalo (BETWEEN, >, ORDER BY) eficientes.
Quando um índice ajuda e quando atrapalha
Ajuda em colunas usadas em WHERE, JOIN e ORDER BY com boa seletividade (poucos resultados por valor). Atrapalha porque todo INSERT, UPDATE e DELETE precisa atualizar cada índice da tabela, e porque ocupa espaço. Índices em colunas com poucos valores distintos (um booleano, por exemplo) raramente são usados.
Índices compostos: a ordem importa
CREATE INDEX idx_pedidos_cliente_data ON pedidos (cliente_id, criado_em);Esse índice serve para WHERE cliente_id = 5 e para WHERE cliente_id = 5 AND criado_em > '2026-01-01', mas não para WHERE criado_em > '2026-01-01' sozinho: vale a regra do prefixo mais à esquerda, como numa lista telefônica ordenada por sobrenome e depois nome.
Um índice de cobertura contém todas as colunas que a consulta precisa, e o banco nem visita a tabela (no PostgreSQL e no SQL Server, INCLUDE (coluna) adiciona colunas só nas folhas).
Lendo o plano de execução
EXPLAIN ANALYZE -- PostgreSQL: executa e mostra tempos reais
SELECT * FROM pedidos WHERE cliente_id = 5;Index Scan using idx_pedidos_cliente on pedidos (cost=0.29..8.31 rows=3 width=40)
(actual time=0.018..0.021 rows=3 loops=1)
Index Cond: (cliente_id = 5)
Planning Time: 0.090 ms
Execution Time: 0.041 msSeq Scan numa tabela grande é o sinal de alerta. No MySQL, EXPLAIN (e EXPLAIN ANALYZE a partir do 8.0.18); no SQL Server, o plano de execução do SSMS e SET STATISTICS IO, TIME ON.
Consultas amigáveis a índices (SARGable)
-- Não usa o índice em criado_em: a função é aplicada em cada linha
SELECT * FROM pedidos WHERE EXTRACT(YEAR FROM criado_em) = 2026;
-- Usa o índice: a coluna fica "limpa" e o intervalo é comparado direto
SELECT * FROM pedidos WHERE criado_em >= '2026-01-01' AND criado_em < '2027-01-01';O mesmo vale para LIKE '%termo' (curinga no início impede o uso do B-tree) e conversões implícitas de tipo.
O problema N+1
Buscar 100 pedidos e, num laço, fazer uma consulta para o cliente de cada um gera 101 consultas. Um JOIN (ou um WHERE id IN (...) com todos os ids) resolve com uma ou duas. ORMs tornam esse erro fácil de cometer sem perceber.
Paginação por chave (keyset)
OFFSET 100000 obriga o banco a ler e descartar 100 mil linhas. Para listas longas, pagine pelo último valor visto:
SELECT id, criado_em, total
FROM pedidos
WHERE (criado_em, id) < ('2026-09-01 10:00', 5821) -- último item da página anterior (PostgreSQL e MySQL)
ORDER BY criado_em DESC, id DESC
LIMIT 20;Views materializadas, procedures, funções e triggers
- View materializada (PostgreSQL): guarda o resultado da consulta. Leitura instantânea, mas precisa ser atualizada com
REFRESH MATERIALIZED VIEW nome;. O SQL Server tem as indexed views; o MySQL não tem equivalente nativo. - Stored procedure: um bloco de código guardado no banco e executado com
CALL(PostgreSQL, MySQL) ouEXEC(SQL Server). - Função: devolve um valor e pode ser usada dentro de consultas.
- Trigger: código executado automaticamente antes ou depois de um
INSERT,UPDATEouDELETE.
-- PostgreSQL: atualiza a coluna atualizado_em automaticamente
CREATE FUNCTION marca_atualizacao() RETURNS trigger AS $$
BEGIN
NEW.atualizado_em := now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_produtos_atualizacao
BEFORE UPDATE ON produtos
FOR EACH ROW EXECUTE FUNCTION marca_atualizacao();Lógica no banco é rápida e garante regras para qualquer aplicação que acesse os dados, mas é mais difícil de versionar, testar e depurar. Use com moderação.
Segurança
Usuários, papéis e privilégios
-- PostgreSQL
CREATE ROLE app_leitura LOGIN PASSWORD 'troque-esta-senha';
GRANT CONNECT ON DATABASE loja TO app_leitura;
GRANT USAGE ON SCHEMA public TO app_leitura;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_leitura;
REVOKE SELECT ON clientes FROM app_leitura;No SQL Server existe também DENY, que proíbe explicitamente e vence qualquer GRANT. O princípio é o do menor privilégio: a aplicação conecta com um usuário que só pode fazer o necessário; nunca com o superusuário.
SQL injection
Montar SQL concatenando texto digitado pelo usuário permite que ele injete comandos:
// VULNERÁVEL: nunca faça isso
const sql = "SELECT * FROM clientes WHERE email = '" + email + "'";
// Se email for: ' OR '1'='1
// a consulta vira: SELECT * FROM clientes WHERE email = '' OR '1'='1' → devolve todosA correção é usar consultas parametrizadas: o valor viaja separado do comando e nunca é interpretado como SQL.
// Node.js com o driver pg
const { rows } = await pool.query(
"SELECT * FROM clientes WHERE email = $1",
[email]
);# Python com psycopg (DB-API)
cursor.execute("SELECT * FROM clientes WHERE email = %s", (email,))ORMs e query builders parametrizam por padrão; o risco volta quando se usa SQL cru com interpolação de strings.
Guarde credenciais em variáveis de ambiente ou num cofre de segredos, nunca no código.
Backup e restore
# PostgreSQL
pg_dump -U postgres -d loja -F c -f loja.dump # formato custom (comprimido)
pg_restore -U postgres -d loja_nova loja.dump
pg_dump -U postgres -d loja > loja.sql # SQL puro
psql -U postgres -d loja_nova -f loja.sql
# MySQL
mysqldump -u root -p --single-transaction loja > loja.sql
mysql -u root -p loja_nova < loja.sql-- SQL Server
BACKUP DATABASE Loja TO DISK = 'C:\backup\loja_full.bak';
BACKUP DATABASE Loja TO DISK = 'C:\backup\loja_diff.bak' WITH DIFFERENTIAL;
BACKUP LOG Loja TO DISK = 'C:\backup\loja_log.trn';
RESTORE DATABASE Loja FROM DISK = 'C:\backup\loja_full.bak' WITH NORECOVERY;
RESTORE DATABASE Loja FROM DISK = 'C:\backup\loja_diff.bak' WITH NORECOVERY;
RESTORE LOG Loja FROM DISK = 'C:\backup\loja_log.trn' WITH RECOVERY;NORECOVERY deixa o banco esperando mais backups; o último restore usa RECOVERY para colocá-lo online. Combinando backup completo, diferencial e de log, é possível restaurar até um momento exato (point-in-time recovery).
Siga a regra 3-2-1: 3 cópias, em 2 mídias diferentes, 1 fora do local. E, principalmente, teste a restauração periodicamente: backup que nunca foi restaurado é só esperança.
NoSQL: por que existe
No fim dos anos 2000, empresas como Google, Amazon e Facebook precisavam de volumes e taxas de escrita que um único servidor relacional não suportava. Surgiram bancos que abrem mão de parte das garantias relacionais em troca de:
- Escala horizontal: distribuir dados entre muitas máquinas baratas (sharding).
- Esquema flexível: documentos com formatos diferentes na mesma coleção.
- Modelos sob medida: chave-valor, documento, grafo, colunas, cada um ótimo para um padrão de acesso.
O teorema CAP
Em um sistema distribuído, quando ocorre uma partição de rede (nós que não conseguem se comunicar), é preciso escolher entre:
- Consistência (C): toda leitura vê a escrita mais recente, ou recebe erro.
- Disponibilidade (A): toda requisição recebe resposta, mesmo que com dado desatualizado.
A tolerância a partição (P) não é opcional em redes reais; a escolha só existe durante a partição. O PACELC completa a ideia: se há partição (P), escolha entre A e C; senão (E), escolha entre latência (L) e consistência (C).
BASE e consistência eventual
Muitos bancos NoSQL seguem o modelo BASE (Basically Available, Soft state, Eventually consistent): as réplicas podem divergir por um tempo, mas convergem se as escritas pararem. Para um contador de curtidas, tudo bem; para um saldo bancário, não.
As famílias de NoSQL
Busca vetorial, usada em aplicações de IA, também existe como extensão de bancos tradicionais (o pgvector no PostgreSQL) e em bancos dedicados.
MongoDB na prática
O MongoDB guarda documentos BSON (um JSON binário com tipos extras, como datas e ObjectId) em coleções, dentro de bancos. Cada documento tem um _id único, gerado automaticamente como ObjectId se você não informar.
use loja // seleciona (e cria na primeira escrita) o bancoCRUD
db.produtos.insertOne({ nome: "Teclado", preco: 200, tags: ["periférico"], estoque: 15 })
db.produtos.insertMany([
{ nome: "Mouse", preco: 80, tags: ["periférico"], estoque: 40 },
{ nome: "Monitor", preco: 900, tags: ["vídeo"], estoque: 5 }
])
db.produtos.find() // todos
db.produtos.find({ preco: { $gt: 100 } }) // preço > 100
db.produtos.find({ tags: "periférico" }, { nome: 1, _id: 0 }) // projeção: só o nome
db.produtos.find({ $or: [{ estoque: 0 }, { preco: { $lt: 50 } }] })
db.produtos.find({ nome: { $regex: "^M" } }).sort({ preco: -1 }).limit(10)
db.produtos.countDocuments({ estoque: { $gt: 0 } })
db.produtos.updateOne({ nome: "Mouse" }, { $set: { preco: 85 } })
db.produtos.updateMany({}, { $inc: { estoque: 10 } })
db.produtos.updateOne({ nome: "Mouse" }, { $push: { tags: "promoção" } })
db.produtos.updateOne({ nome: "Webcam" }, { $set: { preco: 150 } }, { upsert: true })
db.produtos.deleteOne({ nome: "Webcam" })
db.produtos.deleteMany({ estoque: 0 })Operadores de consulta: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $and, $or, $not, $exists, $regex. De atualização: $set, $unset, $inc, $push, $pull, $addToSet.
Índices
db.produtos.createIndex({ nome: 1 }) // simples
db.pedidos.createIndex({ clienteId: 1, criadoEm: -1 }) // composto
db.clientes.createIndex({ email: 1 }, { unique: true }) // único
db.sessoes.createIndex({ criadaEm: 1 }, { expireAfterSeconds: 3600 }) // TTL: expira em 1 h
db.produtos.getIndexes()
db.produtos.find({ nome: "Mouse" }).explain("executionStats")Aggregation pipeline
O equivalente ao GROUP BY, JOIN e companhia é uma sequência de estágios:
db.pedidos.aggregate([
{ $match: { status: "pago" } }, // WHERE
{ $unwind: "$itens" }, // uma linha por item
{ $group: { // GROUP BY
_id: "$clienteId",
totalGasto: { $sum: { $multiply: ["$itens.preco", "$itens.quantidade"] } },
pedidos: { $addToSet: "$_id" }
} },
{ $lookup: { // JOIN
from: "clientes", localField: "_id", foreignField: "_id", as: "cliente"
} },
{ $unwind: "$cliente" },
{ $project: { _id: 0, nome: "$cliente.nome", totalGasto: 1 } }, // SELECT
{ $sort: { totalGasto: -1 } }, // ORDER BY
{ $limit: 10 }
])Modelagem: embutir ou referenciar?
- Embutir (documentos dentro do documento): quando os dados são lidos juntos e pertencem ao pai, como os itens de um pedido. Uma leitura traz tudo.
- Referenciar (guardar o
_id): quando os dados são compartilhados, crescem sem limite ou são acessados sozinhos, como o cliente de milhares de pedidos.
Documentos têm limite de 16 MB. A regra de ouro: modele pelo padrão de acesso da aplicação, não pela estrutura "correta" dos dados.
Desde a versão 4.0, o MongoDB suporta transações com múltiplos documentos em replica sets (e em clusters shardeados a partir da 4.2).
SQL e MongoDB lado a lado
Redis na prática
O Redis guarda tudo na memória, o que o torna extremamente rápido (operações em microssegundos). Não é só chave-valor de texto: cada chave guarda uma estrutura de dados.
redis-cliStrings e expiração
SET usuario:42:nome "Ana"
GET usuario:42:nome
SET sessao:abc123 "dados" EX 3600 # expira em 1 hora
SET trava:pedido:55 1 NX EX 30 # só cria se não existir (uma trava simples)
INCR visitas:home # contador atômico
EXPIRE usuario:42:nome 600
TTL usuario:42:nome # segundos restantes (-1 sem expiração, -2 não existe)
DEL usuario:42:nome
EXISTS usuario:42:nomeAs outras estruturas
HSET usuario:42 nome "Ana" cidade "SP" # hash: um objeto
HGET usuario:42 nome
HGETALL usuario:42
LPUSH fila:emails "msg1" "msg2" # lista: fila ou pilha
RPOP fila:emails
LRANGE fila:emails 0 -1
SADD tags:post:1 redis nosql cache # conjunto sem repetição
SMEMBERS tags:post:1
SISMEMBER tags:post:1 redis
ZADD ranking 1500 "ana" 1200 "bruno" # conjunto ordenado por pontuação
ZINCRBY ranking 100 "bruno"
ZRANGE ranking 0 9 REV WITHSCORES # top 10 (REV desde o Redis 6.2; antes, ZREVRANGE)Para que o Redis é usado
- Cache (padrão cache-aside): a aplicação procura no Redis; se não achar, busca no banco principal e grava no Redis com expiração.
- Sessões de usuários, com TTL.
- Rate limiting:
INCRnuma chave por usuário e minuto, comEXPIRE. - Rankings com sorted sets.
- Filas e mensagens: listas, streams e pub/sub (
PUBLISH/SUBSCRIBE). - Travas distribuídas com
SET ... NX EX.
Por padrão os dados vivem na memória, mas o Redis pode persistir em disco com snapshots (RDB) e/ou com um log de operações (AOF). Mesmo assim, trate-o como complemento do banco principal, não como o lugar da única cópia de dados importantes.
SQL ou NoSQL: como decidir
Perguntas para decidir:
- Os dados têm muitas relações e regras de integridade? Relacional.
- Preciso de transações envolvendo várias entidades (dinheiro, estoque)? Relacional.
- O formato dos dados varia muito e é lido quase sempre inteiro? Documento pode ser natural.
- O acesso é sempre por uma chave e precisa de latência mínima? Chave-valor.
- A pergunta principal é sobre conexões ("amigos de amigos")? Grafo.
- Não sei ainda? Comece com PostgreSQL: ele cobre a imensa maioria dos casos, inclusive JSON (
JSONB) e busca vetorial.
Sistemas maduros costumam usar vários bancos ao mesmo tempo, cada um no que faz melhor: PostgreSQL como fonte da verdade, Redis como cache e fila, Elasticsearch para busca. Isso se chama persistência poliglota.
Mitos comuns: "NoSQL é sempre mais rápido" (depende totalmente do padrão de acesso), "SQL não escala" (bancos relacionais atendem sistemas enormes, com réplicas e particionamento) e "MongoDB não tem transações" (tem, desde a versão 4.0).
ORMs e query builders
ORMs (Object-Relational Mappers) mapeiam tabelas para objetos da linguagem: Prisma, Drizzle, TypeORM e Sequelize no ecossistema JavaScript/TypeScript, SQLAlchemy em Python, Hibernate em Java.
- Vantagens: produtividade, tipagem, migrações versionadas e consultas parametrizadas por padrão.
- Desvantagens: escondem o SQL gerado, facilitam o problema N+1 e às vezes produzem consultas ineficientes.
A combinação saudável: use o ORM no dia a dia, leia o SQL que ele gera (todos têm modo de log), e escreva SQL à mão nas consultas críticas. Saber SQL de verdade é o que permite usar um ORM sem medo.
Tabela completa de comandos SQL e NoSQL
As colas definitivas deste guia, no estilo pôster. Para situações práticas do dia a dia explicadas uma a uma, veja também SQL e NoSQL: comandos que salvam sua vida.
SQL essencial
Os comandos que funcionam (quase) igual em qualquer banco relacional.
DDL: estrutura
Criar e alterar tabelas
DML: dados
Inserir, alterar e apagar
SELECT e filtros
Consultas do dia a dia
JOINs
Combinando tabelas
Agregações e janelas
Totais, grupos e rankings
Transações
Tudo ou nada
Índices e desempenho
Deixe as consultas rápidas
Usuários e permissões
Menor privilégio sempre
PostgreSQL e MySQL no terminal
psql e mysql: o dia a dia do administrador.
psql: meta-comandos
Dentro do cliente do PostgreSQL
PostgreSQL: administração
Sessões, tamanhos e manutenção
MySQL
Cliente mysql e administração
SQL Server: comandos essenciais do DBA
Consulta, administração, monitoramento e manutenção.
Consultas e informações
Descubra e explore seu ambiente
Processos e sessões
Veja o que está acontecendo
Administração de bancos
Gerencie seus bancos de dados
Índices e performance
Otimize seu ambiente
Usuários, logins e permissões
Controle o acesso ao ambiente
Tarefas e jobs (SQL Agent)
Automatize e gerencie tarefas
Backup e restore
Proteja seus dados
Segurança e auditoria
Monitore e aumente a segurança
TempDB e recursos
Monitore o uso de recursos
Outros comandos úteis
Diversos que facilitam o dia a dia
NoSQL: MongoDB e Redis
mongosh e redis-cli para consulta rápida.
MongoDB: CRUD
No mongosh
MongoDB: índices e agregação
Desempenho e relatórios
Redis
No redis-cli
Referências e leituras recomendadas
- Documentação do PostgreSQL: referência completa e excelente.
- Manual de referência do MySQL: documentação oficial do MySQL.
- Referência do Transact-SQL (Microsoft): documentação oficial do SQL Server.
- Documentação do SQLite: o banco embutido mais usado do mundo.
- Manual do MongoDB: documentação oficial do MongoDB.
- Documentação do Redis: comandos e estruturas de dados.
- Use The Index, Luke!: o melhor material sobre índices e desempenho em SQL.
- A Relational Model of Data for Large Shared Data Banks: o artigo de Codd que criou o modelo relacional.
- Designing Data-Intensive Applications: o livro de referência sobre sistemas de dados.
- DB-Engines Ranking: popularidade dos bancos de dados.
- SQLBolt: exercícios interativos de SQL.
- PostgreSQL Exercises: exercícios práticos com PostgreSQL.
- SQL Server First Responder Kit: os scripts
sp_Blitz, gratuitos. - OWASP: prevenção de SQL injection: o guia de referência em segurança.
7 perguntas sobre bancos de dados.
O que o A de ACID garante?
Não foi dessa vez. Explicação:
Atomicidade: se qualquer parte falhar, tudo é desfeito.



