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:

Texto
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 procedures

Os 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.

ProblemaCom arquivos (CSV, JSON, planilha)Com um SGBD
Duas pessoas salvando ao mesmo tempoA última gravação sobrescreve a outraControle de concorrência (locks, MVCC)
Dado inválido (preço "abc", data 31/02)Nada impedeTipos de dados e constraints rejeitam
Venda de um produto que não existeNada impedeChave estrangeira rejeita
Achar um cliente entre milhõesLer o arquivo inteiroUm índice encontra em poucas leituras
Queda de energia no meio da gravaçãoArquivo corrompido ou pela metadeO log de transações garante "tudo ou nada"
Quem pode ver o quêPermissão no arquivo inteiroPermissões por tabela, coluna e até por linha
Pergunta nova ("quanto vendemos por estado?")Escrever um programa novoUma consulta SQL

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 SELECT continua 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

Texto
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ções

Nos 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.

Texto
┌──────────────────────────────────────────────────────────────┐
│ 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:

SGBDTamanho padrão da página
PostgreSQL8 KB
SQL Server8 KB (agrupadas em extents de 8 páginas = 64 KB)
MySQL (InnoDB)16 KB

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:

Texto
┌──────────────────────────────────────────────┐
│ 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.

Texto
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 dados

Se 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 .ldf de 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:

Texto
  "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 → cliente

O 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á:

AlgoritmoComo funcionaCostuma ser escolhido quando
Nested loopPara cada linha de A, procura as correspondentes em B (de preferência via índice)A é pequena e B tem índice na coluna da junção
Hash joinMonta uma tabela hash com a entrada menor e varre a maior procurando paresEntradas grandes, sem ordem útil, junção por igualdade
Merge joinPercorre as duas entradas já ordenadas pela chave, em paraleloAs entradas já chegam ordenadas (por exemplo, via índice)

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.
Texto
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ê ligar READ_COMMITTED_SNAPSHOT ou usar o nível SNAPSHOT (as versões ficam no tempdb, 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

Termo formalTermo do dia a diaExemplo
RelaçãoTabelaclientes
TuplaLinha, registroO cliente Ana
AtributoColuna, campoemail
DomínioTipo de dadoVARCHAR(255)

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

Texto
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 IDENTITY no PostgreSQL e no padrão SQL, AUTO_INCREMENT no 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:

pedidodataclienteemail_clienteprodutos
12026-09-01Anaana@ex.comTeclado (R$ 200), Mouse (R$ 80)
22026-09-02Brunobruno@ex.comMouse (R$ 80)
32026-09-02Anaana@ex.comMonitor (R$ 900)

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:

pedidodataclienteemail_clienteprodutopreco
12026-09-01Anaana@ex.comTeclado200
12026-09-01Anaana@ex.comMouse80
22026-09-02Brunobruno@ex.comMouse80
32026-09-02Anaana@ex.comMonitor900

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:

SQL
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, ROLLBACK desfaz 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

SQL
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 resto

Sem 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

NívelLeitura sujaLeitura não repetívelLeitura fantasma
Read Uncommittedpossívelpossívelpossível
Read Committednãopossívelpossível
Repeatable Readnãonãopossível pelo padrão SQL
Serializablenãonãonão
  • 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.

SQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;   -- dentro da transação, antes da primeira consulta (PostgreSQL)

Evitando a atualização perdida

SQL
-- 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

CategoriaSignificadoComandos
DDLDefinição de dados (estrutura)CREATE, ALTER, DROP, TRUNCATE
DMLManipulação de dadosINSERT, UPDATE, DELETE, MERGE
DQLConsulta de dadosSELECT
DCLControle de acessoGRANT, REVOKE (e DENY no SQL Server)
TCLControle de transaçõesBEGIN, COMMIT, ROLLBACK, SAVEPOINT

Tipos de dados nos três principais bancos

UsoPostgreSQLMySQLSQL Server
InteiroINTEGER, BIGINTINT, BIGINTINT, BIGINT
Decimal exato (dinheiro)NUMERIC(12,2)DECIMAL(12,2)DECIMAL(12,2)
Ponto flutuanteDOUBLE PRECISIONDOUBLEFLOAT
Texto curtoVARCHAR(255)VARCHAR(255)NVARCHAR(255)
Texto longoTEXTTEXTNVARCHAR(MAX)
BooleanoBOOLEANBOOLEAN (é TINYINT(1))BIT
DataDATEDATEDATE
Data e hora com fusoTIMESTAMPTZTIMESTAMP (convertido para UTC)DATETIMEOFFSET
JSONJSONBJSONNVARCHAR(MAX) com funções JSON
UUIDUUIDBINARY(16) ou CHAR(36)UNIQUEIDENTIFIER
BinárioBYTEABLOBVARBINARY(MAX)

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:

Bash
sqlite3 loja.db

PostgreSQL e MySQL com Docker

Bash
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 -p

As 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:

SQL
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

SQL
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

ComandoO que fazPode ter WHEREVolta com ROLLBACK
DELETE FROM tApaga linhas, uma a umaSimSim
TRUNCATE TABLE tEsvazia a tabela inteira, rápidoNãoPostgreSQL e SQL Server sim, dentro de transação; MySQL não
DROP TABLE tApaga a tabela e a estruturaNãoPostgreSQL e SQL Server sim, dentro de transação; MySQL não

Índices e views

SQL
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

SQL
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

SQL
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

SQL
-- 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

SQL
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 SQLite

Paginaçã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.

SQL
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

SQL
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

SQL
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:

Texto
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   → recorta

Isso 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:

Texto
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

SQL
SELECT c.nome, p.id, p.total
FROM clientes c
INNER JOIN pedidos p ON p.cliente_id = c.id;
Texto
Ana   | 10 | 280.00
Ana   | 11 | 900.00
Bruno | 12 |  80.00

LEFT JOIN: tudo da esquerda, e o que casar da direita

SQL
SELECT c.nome, p.id, p.total
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id;
Texto
Ana   | 10   | 280.00
Ana   | 11   | 900.00
Bruno | 12   |  80.00
Carla | NULL | NULL      ← Carla não tem pedidos

RIGHT 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

SQL
-- 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

SQL
-- 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:

SQL
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

SQL
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:

SQL
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 (o RANK pula posições depois de um empate; o DENSE_RANK nã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

SQL
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:

Texto
                         [ G | P ]
                /            |            \
        [ B | D ]        [ J | M ]        [ S | V ]
        /   |   \        /   |   \        /   |   \
      folhas ordenadas com o valor e o endereço da linha na tabela

Cada 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

SQL
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

SQL
EXPLAIN ANALYZE                      -- PostgreSQL: executa e mostra tempos reais
SELECT * FROM pedidos WHERE cliente_id = 5;
Texto
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 ms

Seq 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)

SQL
-- 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:

SQL
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) ou EXEC (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, UPDATE ou DELETE.
SQL
-- 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

SQL
-- 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:

JavaScript
// 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 todos

A correção é usar consultas parametrizadas: o valor viaja separado do comando e nunca é interpretado como SQL.

JavaScript
// Node.js com o driver pg
const { rows } = await pool.query(
  "SELECT * FROM clientes WHERE email = $1",
  [email]
);
Python
# 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

Bash
# 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
-- 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

TipoComo guardaExemplosBom para
DocumentoDocumentos JSON/BSONMongoDB, Firestore, CouchDBCatálogos, conteúdo, perfis
Chave-valorChave → valorRedis, DynamoDBCache, sessões, contadores
Colunar largoLinhas com colunas dinâmicas, particionadasCassandra, ScyllaDBEscrita massiva, séries temporais, IoT
GrafoNós e arestasNeo4jRedes sociais, recomendações, fraude
BuscaÍndice invertidoElasticsearch, OpenSearchBusca textual, logs
Séries temporaisPontos com timestampInfluxDB, TimescaleDBMétricas, monitoramento

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.

JavaScript
use loja            // seleciona (e cria na primeira escrita) o banco

CRUD

JavaScript
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

JavaScript
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:

JavaScript
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

SQLMongoDB
BancoBanco
TabelaColeção
LinhaDocumento
ColunaCampo
JOIN$lookup ou documento embutido
SELECT * FROM p WHERE preco > 100db.p.find({ preco: { $gt: 100 } })
SELECT nome FROM pdb.p.find({}, { nome: 1 })
INSERT INTO p ...db.p.insertOne({...})
UPDATE p SET preco = 85 WHERE ...db.p.updateOne({...}, { $set: { preco: 85 } })
DELETE FROM p WHERE ...db.p.deleteMany({...})
GROUP BY$group
ORDER BY preco DESC LIMIT 10.sort({ preco: -1 }).limit(10)
CREATE INDEXcreateIndex

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.

Bash
redis-cli

Strings e expiração

Bash
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:nome

As outras estruturas

Bash
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: INCR numa chave por usuário e minuto, com EXPIRE.
  • 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

CritérioRelacional (SQL)NoSQL
EsquemaRígido, definido antesFlexível
Relações entre dadosExcelentes (JOINs)Limitadas; modela-se pelo acesso
ConsistênciaForte, ACIDVaria; muitas vezes eventual
Consultas ad hocMuito poderosasDependem do modelo
EscalaVertical, e horizontal com mais esforçoHorizontal é o ponto forte
TransaçõesCompletasVariam de banco para banco
Casos típicosFinanceiro, ERP, e-commerce, a maioria dos sistemasCache, catálogos, eventos, IoT, grafos

Perguntas para decidir:

  1. Os dados têm muitas relações e regras de integridade? Relacional.
  2. Preciso de transações envolvendo várias entidades (dinheiro, estoque)? Relacional.
  3. O formato dos dados varia muito e é lido quase sempre inteiro? Documento pode ser natural.
  4. O acesso é sempre por uma chave e precisa de latência mínima? Chave-valor.
  5. A pergunta principal é sobre conexões ("amigos de amigos")? Grafo.
  6. 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.

Cheat sheet

SQL essencial

Os comandos que funcionam (quase) igual em qualquer banco relacional.

DDL: estrutura

Criar e alterar tabelas

ComandoDescrição
CREATE DATABASE loja;Cria um banco
CREATE TABLE t (id INT PRIMARY KEY, ...);Cria uma tabela
ALTER TABLE t ADD COLUMN c VARCHAR(50);Adiciona coluna
ALTER TABLE t DROP COLUMN c;Remove coluna
ALTER TABLE t ADD CONSTRAINT u UNIQUE (c);Adiciona restrição
CREATE INDEX idx ON t (c);Cria índice
CREATE VIEW v AS SELECT ...;Cria uma view
TRUNCATE TABLE t;Esvazia a tabela
DROP TABLE t;Apaga a tabela
DROP DATABASE loja;Apaga o banco (cuidado)

DML: dados

Inserir, alterar e apagar

ComandoDescrição
INSERT INTO t (a, b) VALUES (1, 'x');Insere uma linha
INSERT INTO t (a) VALUES (1), (2), (3);Insere várias
INSERT INTO t2 SELECT ... FROM t;Insere a partir de consulta
UPDATE t SET a = 2 WHERE id = 1;Altera (sempre com WHERE)
DELETE FROM t WHERE id = 1;Apaga (sempre com WHERE)
... ON CONFLICT (id) DO UPDATE ...Upsert no PostgreSQL
... ON DUPLICATE KEY UPDATE ...Upsert no MySQL
MERGE INTO t USING ...Upsert no SQL Server

SELECT e filtros

Consultas do dia a dia

ComandoDescrição
SELECT * FROM t;Todas as colunas
SELECT DISTINCT cidade FROM t;Valores sem repetição
WHERE a = 1 AND b <> 2Condições
WHERE a BETWEEN 10 AND 20Intervalo inclusivo
WHERE a IN (1, 2, 3)Lista de valores
WHERE nome LIKE 'A%'Começa com A
WHERE c IS NULLValores nulos
ORDER BY a DESC, bOrdenação
LIMIT 10 OFFSET 20Paginação (PG, MySQL)
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLYPaginação (padrão, SQL Server)
COALESCE(a, 'padrão')Primeiro não nulo
CASE WHEN ... THEN ... ENDCondicional

JOINs

Combinando tabelas

ComandoDescrição
INNER JOIN b ON b.a_id = a.idSó o que casa dos dois lados
LEFT JOIN b ON ...Tudo da esquerda
RIGHT JOIN b ON ...Tudo da direita
FULL OUTER JOIN b ON ...Tudo dos dois (não existe no MySQL)
CROSS JOIN bTodas as combinações
LEFT JOIN b ... WHERE b.id IS NULLQuem não tem correspondência
WHERE NOT EXISTS (SELECT 1 ...)Anti-join legível
FROM func f JOIN func g ON f.gestor_id = g.idSelf join

Agregações e janelas

Totais, grupos e rankings

ComandoDescrição
COUNT(*) / COUNT(col)Linhas / valores não nulos
SUM(col), AVG(col)Soma, média
MIN(col), MAX(col)Menor, maior
GROUP BY colAgrupa
HAVING COUNT(*) > 5Filtra grupos
ROW_NUMBER() OVER (PARTITION BY a ORDER BY b)Numeração por grupo
RANK() OVER (ORDER BY total DESC)Ranking com empates
LAG(col) OVER (ORDER BY data)Valor da linha anterior
SUM(col) OVER (ORDER BY data)Total acumulado
WITH cte AS (SELECT ...) SELECT ...Consulta nomeada

Transações

Tudo ou nada

ComandoDescrição
BEGIN;Inicia transação (PG, MySQL)
BEGIN TRANSACTION;Inicia transação (SQL Server)
COMMIT;Confirma
ROLLBACK;Desfaz tudo
SAVEPOINT sp1;Ponto de retorno
ROLLBACK TO SAVEPOINT sp1;Volta ao ponto (PG, MySQL)
SELECT ... FOR UPDATE;Trava as linhas lidas
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;Nível de isolamento

Índices e desempenho

Deixe as consultas rápidas

ComandoDescrição
CREATE INDEX idx ON t (a);Índice simples
CREATE INDEX idx ON t (a, b);Composto (ordem importa)
CREATE UNIQUE INDEX idx ON t (email);Índice único
DROP INDEX idx;Remove (PG)
EXPLAIN SELECT ...;Plano estimado
EXPLAIN ANALYZE SELECT ...;Plano com tempos reais (PG)
ANALYZE t;Atualiza estatísticas (PG, MySQL)
VACUUM ANALYZE t;Limpa e atualiza estatísticas (PG)

Usuários e permissões

Menor privilégio sempre

ComandoDescrição
CREATE ROLE app LOGIN PASSWORD 'x';Cria usuário (PG)
CREATE USER 'app'@'%' IDENTIFIED BY 'x';Cria usuário (MySQL)
GRANT SELECT, INSERT ON t TO app;Concede
REVOKE INSERT ON t FROM app;Revoga
DENY DELETE ON t TO app;Nega explicitamente (SQL Server)
ALTER ROLE app PASSWORD 'nova';Troca senha (PG)
DROP ROLE app;Remove usuário (PG)
Cheat sheet

PostgreSQL e MySQL no terminal

psql e mysql: o dia a dia do administrador.

psql: meta-comandos

Dentro do cliente do PostgreSQL

ComandoDescrição
psql -h host -U usuario -d bancoConecta
\lLista bancos
\c bancoConecta a outro banco
\dtLista tabelas
\d tabelaEstrutura de uma tabela
\diLista índices
\duLista usuários e papéis
\dnLista schemas
\dfLista funções
\xAlterna exibição vertical
\timingMostra tempo das consultas
\i arquivo.sqlExecuta um arquivo
\copy t TO 'dados.csv' CSV HEADERExporta para CSV
\qSai

PostgreSQL: administração

Sessões, tamanhos e manutenção

ComandoDescrição
SELECT version();Versão do servidor
SELECT * FROM pg_stat_activity;Sessões e consultas ativas
SELECT pg_cancel_backend(pid);Cancela a consulta de uma sessão
SELECT pg_terminate_backend(pid);Encerra a sessão
SELECT pg_size_pretty(pg_database_size('loja'));Tamanho do banco
SELECT pg_size_pretty(pg_total_relation_size('t'));Tamanho da tabela com índices
SELECT * FROM pg_indexes WHERE tablename = 't';Índices de uma tabela
SELECT * FROM pg_locks;Locks atuais
SHOW max_connections;Lê uma configuração
pg_dump -F c -d loja -f loja.dumpBackup
pg_restore -d loja loja.dumpRestore

MySQL

Cliente mysql e administração

ComandoDescrição
mysql -h host -u usuario -p bancoConecta
SHOW DATABASES;Lista bancos
USE loja;Seleciona o banco
SHOW TABLES;Lista tabelas
DESCRIBE t;Estrutura da tabela
SHOW CREATE TABLE t;DDL completo da tabela
SHOW INDEX FROM t;Índices
SHOW PROCESSLIST;Conexões e consultas ativas
KILL 123;Encerra a conexão 123
SHOW GRANTS FOR 'app'@'%';Permissões de um usuário
SHOW VARIABLES LIKE 'max_connections';Lê configuração
mysqldump -u root -p --single-transaction loja > loja.sqlBackup
Cheat sheet

SQL Server: comandos essenciais do DBA

Consulta, administração, monitoramento e manutenção.

Consultas e informações

Descubra e explore seu ambiente

ComandoDescrição
SELECT @@VERSIONVersão do SQL Server
SELECT DB_NAME()Nome do banco atual
SELECT GETDATE()Data e hora do servidor
SELECT SUSER_SNAME()Login da sessão
SELECT HOST_NAME()Máquina de origem da conexão
SELECT @@SPIDID da sessão atual
SELECT name FROM sys.databasesLista os bancos
SELECT name FROM sys.tablesLista as tabelas
SELECT name FROM sys.proceduresLista as procedures
SELECT name FROM sys.indexesLista os índices
SELECT * FROM sys.server_principalsLogins da instância
SELECT * FROM sys.database_principalsUsuários do banco

Processos e sessões

Veja o que está acontecendo

ComandoDescrição
EXEC sp_whoLista processos (básico)
EXEC sp_who2Lista processos, detalhado (não documentado)
SELECT * FROM sys.dm_exec_sessionsSessões ativas
SELECT * FROM sys.dm_exec_requestsRequisições em execução
SELECT * FROM sys.dm_exec_connectionsConexões ativas
KILL 57Encerra a sessão 57
DBCC INPUTBUFFER(57)Último comando da sessão 57
SELECT * FROM sys.dm_os_waiting_tasksTarefas em espera
SELECT * FROM sys.dm_exec_query_statsEstatísticas de consultas
SELECT * FROM sys.dm_exec_sql_text(sql_handle)Texto da consulta pelo handle

Administração de bancos

Gerencie seus bancos de dados

ComandoDescrição
CREATE DATABASE LojaCria um banco
ALTER DATABASE Loja MODIFY NAME = Loja2Renomeia o banco
DROP DATABASE LojaRemove o banco (cuidado)
USE LojaSeleciona o banco
ALTER DATABASE Loja SET OFFLINEColoca offline
ALTER DATABASE Loja SET ONLINEColoca online
ALTER DATABASE Loja SET READ_ONLYSomente leitura
ALTER DATABASE Loja SET READ_WRITELeitura e escrita
DBCC SHRINKDATABASE(Loja)Reduz o tamanho (evite em rotina)
DBCC CHECKDB(Loja)Verifica a consistência
DBCC CHECKDB(Loja) WITH NO_INFOMSGSVerificação sem mensagens informativas

Índices e performance

Otimize seu ambiente

ComandoDescrição
CREATE INDEX idx_nome ON tabela(coluna)Cria um índice
DROP INDEX idx_nome ON tabelaRemove o índice
ALTER INDEX idx_nome ON tabela REBUILDReconstrói o índice
ALTER INDEX idx_nome ON tabela REORGANIZEReorganiza o índice
EXEC sp_helpindex 'tabela'Lista índices da tabela
EXEC sp_BlitzIndex @TableName = 'tabela'Análise detalhada (First Responder Kit, gratuito, de terceiros)
SELECT * FROM sys.dm_db_index_usage_statsUso dos índices
SELECT * FROM sys.dm_db_missing_index_detailsÍndices faltantes (sugestões)
SET STATISTICS IO, TIME ONExibe estatísticas de execução
SET STATISTICS IO, TIME OFFDesativa as estatísticas
UPDATE STATISTICS tabelaAtualiza estatísticas da tabela

Usuários, logins e permissões

Controle o acesso ao ambiente

ComandoDescrição
CREATE LOGIN usuario WITH PASSWORD = 'senha'Cria um login
ALTER LOGIN usuario WITH PASSWORD = 'nova'Altera a senha do login
DROP LOGIN usuarioRemove o login
CREATE USER usuario FOR LOGIN usuarioCria usuário no banco
ALTER USER usuario WITH LOGIN = novo_loginReliga usuário a um login (corrige órfãos)
DROP USER usuarioRemove o usuário
EXEC sp_helpuserLista usuários do banco
EXEC sp_helploginsLista logins da instância
GRANT SELECT ON tabela TO usuarioConcede permissão
REVOKE SELECT ON tabela FROM usuarioRemove permissão
DENY DELETE ON tabela TO usuarioNega permissão
ALTER ROLE db_datareader ADD MEMBER usuarioAdiciona a um papel

Tarefas e jobs (SQL Agent)

Automatize e gerencie tarefas

ComandoDescrição
EXEC msdb.dbo.sp_help_jobLista jobs do Agent
EXEC msdb.dbo.sp_help_jobhistoryHistórico de execuções
EXEC msdb.dbo.sp_start_job @job_name = 'Nome'Inicia um job
EXEC msdb.dbo.sp_stop_job @job_name = 'Nome'Para um job
EXEC msdb.dbo.sp_add_job @job_name = 'Nome'Cria um novo job
EXEC msdb.dbo.sp_delete_job @job_name = 'Nome'Remove um job
SELECT * FROM msdb.dbo.sysjobsJobs cadastrados
SELECT * FROM msdb.dbo.sysjobstepsPassos dos jobs
SELECT * FROM msdb.dbo.sysjobhistoryHistórico bruto

Backup e restore

Proteja seus dados

ComandoDescrição
BACKUP DATABASE Loja TO DISK = 'C:\bk\loja.bak'Backup completo
BACKUP DATABASE Loja TO DISK = '...' WITH DIFFERENTIALBackup diferencial
BACKUP LOG Loja TO DISK = 'C:\bk\loja.trn'Backup de log
RESTORE DATABASE Loja FROM DISK = '...'Restaura completo
RESTORE DATABASE Loja FROM DISK = '...' WITH NORECOVERYRestaura aguardando mais backups
RESTORE LOG Loja FROM DISK = '...' WITH RECOVERYRestaura log e coloca online
RESTORE DATABASE Loja WITH RECOVERYFinaliza a restauração
RESTORE FILELISTONLY FROM DISK = '...'Arquivos contidos no backup
RESTORE VERIFYONLY FROM DISK = '...'Verifica se o backup é legível

Segurança e auditoria

Monitore e aumente a segurança

ComandoDescrição
SELECT * FROM sys.server_auditsLista auditorias
SELECT * FROM sys.server_audit_specificationsEspecificações de auditoria
SELECT * FROM sys.fn_get_audit_file('C:\Audit\*.sqlaudit', DEFAULT, DEFAULT)Lê arquivos de auditoria
SELECT name, is_encrypted FROM sys.databasesVerifica se o TDE está ativo
SELECT * FROM sys.certificatesLista certificados
SELECT * FROM sys.symmetric_keysLista chaves de criptografia
ALTER SERVER AUDIT NomeAuditoria WITH (STATE = ON)Ativa uma auditoria

TempDB e recursos

Monitore o uso de recursos

ComandoDescrição
SELECT * FROM sys.dm_db_file_space_usageUso de espaço do TempDB
SELECT * FROM sys.dm_os_memory_clerksUso de memória
SELECT COUNT(*) FROM tempdb.sys.tablesObjetos no TempDB
DBCC SQLPERF(LOGSPACE)Uso do log de todos os bancos
SELECT * FROM sys.dm_os_performance_countersContadores de desempenho

Outros comandos úteis

Diversos que facilitam o dia a dia

ComandoDescrição
EXEC sp_rename 'nome_antigo', 'novo_nome'Renomeia objeto
TRUNCATE TABLE tabelaRemove todos os dados (mais rápido)
UPDATE STATISTICS tabelaAtualiza estatísticas da tabela
CHECKPOINTForça gravação em disco
SELECT name, recovery_model_desc FROM sys.databasesModelo de recuperação dos bancos
SELECT SERVERPROPERTY('MachineName')Nome da máquina
SELECT SERVERPROPERTY('InstanceName')Nome da instância
EXEC sp_help 'tabela'Estrutura de um objeto
EXEC sp_spaceused 'tabela'Espaço usado por uma tabela
Cheat sheet

NoSQL: MongoDB e Redis

mongosh e redis-cli para consulta rápida.

MongoDB: CRUD

No mongosh

ComandoDescrição
show dbsLista bancos
use lojaSeleciona banco
show collectionsLista coleções
db.c.insertOne({...})Insere um documento
db.c.insertMany([...])Insere vários
db.c.find({ preco: { $gt: 100 } })Busca com filtro
db.c.find({}, { nome: 1, _id: 0 })Projeção
db.c.find().sort({ preco: -1 }).limit(10)Ordena e limita
db.c.countDocuments({...})Conta
db.c.updateOne({...}, { $set: {...} })Atualiza um
db.c.updateMany({...}, { $inc: { n: 1 } })Atualiza vários
db.c.deleteMany({...})Apaga vários

MongoDB: índices e agregação

Desempenho e relatórios

ComandoDescrição
db.c.createIndex({ campo: 1 })Índice crescente
db.c.createIndex({ email: 1 }, { unique: true })Índice único
db.c.createIndex({ d: 1 }, { expireAfterSeconds: 3600 })Índice TTL
db.c.getIndexes()Lista índices
db.c.find({...}).explain("executionStats")Plano de execução
db.c.aggregate([{ $match: {...} }, ...])Pipeline de agregação
{ $group: { _id: "$campo", total: { $sum: 1 } } }Agrupa e conta
{ $lookup: { from, localField, foreignField, as } }Junta coleções
db.c.drop()Apaga a coleção
mongodump --db loja / mongorestoreBackup e restore

Redis

No redis-cli

ComandoDescrição
SET chave valor EX 60Grava com expiração
GET chaveLê
SET chave 1 NX EX 30Só grava se não existir
INCR contadorIncrementa atômico
EXPIRE chave 600 / TTL chaveDefine / consulta expiração
DEL chaveApaga
HSET h campo valor / HGETALL hHash
LPUSH l v / RPOP l / LRANGE l 0 -1Listas
SADD s v / SMEMBERS sConjuntos
ZADD z 100 m / ZRANGE z 0 9 REV WITHSCORESRanking
SCAN 0 MATCH user:* COUNT 100Varre chaves sem travar
INFO memoryUso de memória
DBSIZENúmero de chaves

Referências e leituras recomendadas

Teste seus conhecimentos
1 / 7

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.