▶ Slides ← MĂłdulo
Módulo 2 · Ciclo Comum · IN02

Banco de Dados II

Aula 3 — Material de Leitura Aprofundado

1. Revisão: a estrutura está criada — agora vamos popular

Na aula anterior, vocĂȘ aprendeu a modelar o domĂ­nio usando o Diagrama de Entidade-Relacionamento (DER) e a traduzir esse modelo em tabelas SQL com CREATE TABLE, chaves primĂĄrias, chaves estrangeiras e constraints. VocĂȘ criou a estrutura do banco — o esqueleto. Agora chegou a hora de dar vida a esse esqueleto: inserir dados, consultĂĄ-los, atualizĂĄ-los e eventualmente removĂȘ-los. Esse conjunto de quatro operaçÔes Ă© o que chamamos de CRUD — Create, Read, Update, Delete.

CRUD nĂŁo Ă© apenas uma sigla de mnemĂŽnico: Ă© a fundação de qualquer sistema que persiste dados. NĂŁo importa se vocĂȘ estĂĄ construindo um e-commerce, um sistema hospitalar ou um aplicativo de tarefas — em algum nĂ­vel, toda funcionalidade se reduz a uma combinação dessas quatro operaçÔes. Uma "matrĂ­cula de aluno" Ă© um INSERT em matriculas. Um "relatĂłrio de vendas" Ă© um SELECT com GROUP BY. Uma "atualização de endereço" Ă© um UPDATE com WHERE. Uma "exclusĂŁo de conta" Ă©, muitas vezes, nĂŁo um DELETE, mas um UPDATE que marca o registro como inativo — e entender essa nuance Ă© parte do que separa desenvolvedores experientes de iniciantes.

O ciclo de vida de um dado em um sistema tĂ­pico começa com a inserção (geralmente disparada por uma ação do usuĂĄrio: preencher um formulĂĄrio, finalizar uma compra, registrar uma presença). Em seguida, o dado Ă© lido repetidamente — em listagens, relatĂłrios, telas de detalhe. Com o tempo, pode ser atualizado (um pedido muda de status, um produto tem o preço corrigido). E eventualmente pode ser arquivado ou removido. Cada etapa desse ciclo exige que vocĂȘ escreva SQL correto, seguro e eficiente — e esse Ă© o tema central desta aula.

Contexto: o modelo da Loja UniversitĂĄria

Ao longo desta aula, usaremos as tabelas clientes, produtos, pedidos e itens_pedido criadas na Aula 2. Se precisar, revise o DER e os CREATE TABLE antes de prosseguir — o SQL desta aula pressupĂ”e que essas tabelas existem no banco.

INSERT
Adiciona novos registros a uma tabela
CREATE em CRUD
SELECT
Consulta e retorna registros existentes
READ em CRUD
UPDATE
Modifica registros existentes
UPDATE em CRUD
DELETE
Remove registros de uma tabela
DELETE em CRUD

2. INSERT — Inserindo dados

O comando INSERT INTO Ă© a forma de adicionar novos registros a uma tabela. A sintaxe bĂĄsica exige que vocĂȘ especifique o nome da tabela, a lista de colunas que estĂĄ preenchendo e os valores correspondentes. Listar explicitamente as colunas Ă© uma boa prĂĄtica obrigatĂłria: se um dia vocĂȘ alterar a ordem das colunas na tabela ou adicionar uma nova coluna, um INSERT sem lista de colunas quebrarĂĄ silenciosamente — ou, pior, inserirĂĄ dados nas colunas erradas sem nenhuma mensagem de erro.

O PostgreSQL nĂŁo exige que vocĂȘ forneça valores para colunas com DEFAULT ou SERIAL (geradas automaticamente). Mas para colunas definidas com NOT NULL sem valor padrĂŁo, omiti-las causarĂĄ um erro imediato. Esse erro Ă© desejĂĄvel: o banco estĂĄ protegendo a integridade dos seus dados. A lição prĂĄtica Ă© simples — antes de escrever um INSERT, leia o CREATE TABLE e identifique quais colunas sĂŁo obrigatĂłrias.

Para inserçÔes que envolvem mĂșltiplas tabelas relacionadas — por exemplo, criar um pedido e seus itens ao mesmo tempo — a ordem importa. VocĂȘ deve inserir primeiro o registro pai (o pedido) antes dos registros filhos (os itens), porque a chave estrangeira pedido_id em itens_pedido deve referenciar um pedido que jĂĄ existe. Se tentar inserir os itens primeiro, o banco rejeitarĂĄ a operação com uma violação de chave estrangeira. É exatamente por isso que transaçÔes — que veremos adiante — sĂŁo essenciais em inserçÔes em cascata.

INSERT básico — uma linha
-- Inserindo um produto
INSERT INTO produtos (nome, preco, estoque)
VALUES ('Notebook', 2500.00, 10);

2.1 INSERT mĂșltiplo e RETURNING

O PostgreSQL permite inserir mĂșltiplas linhas em um Ășnico comando INSERT, separando cada conjunto de valores por vĂ­rgula. Isso Ă© significativamente mais eficiente do que enviar N comandos INSERT separados, pois reduz o nĂșmero de round-trips entre a aplicação e o banco — cada ida e volta pela rede tem um custo de latĂȘncia que se acumula. Em testes com 1000 registros, um INSERT em lote pode ser 10 a 50 vezes mais rĂĄpido que 1000 INSERTs individuais.

A clĂĄusula RETURNING Ă© uma extensĂŁo do PostgreSQL (nĂŁo existe no SQL padrĂŁo) que permite recuperar valores de colunas dos registros recĂ©m-inseridos sem precisar fazer um SELECT adicional. Isso Ă© especialmente Ăștil para obter o id gerado automaticamente por um SERIAL ou BIGSERIAL. Em vez de inserir e depois fazer SELECT MAX(id) (que Ă© inseguro em ambientes concorrentes), vocĂȘ usa RETURNING id e obtĂ©m o valor correto da sua inserção especĂ­fica, mesmo que outros clientes estejam inserindo registros simultaneamente.

Outra forma poderosa de inserção Ă© o INSERT INTO ... SELECT, que permite copiar ou transformar dados de uma tabela para outra em uma Ășnica operação. Essa tĂ©cnica Ă© Ăștil para migraçÔes de dados, criação de tabelas de histĂłrico, população de tabelas de cache e operaçÔes de ETL (Extract, Transform, Load). O SELECT pode incluir qualquer transformação que o banco suporte — funçÔes de string, cĂĄlculos, subqueries, JOINs — e todos os resultados serĂŁo inseridos de uma vez.

INSERT mĂșltiplo e RETURNING
-- INSERT mĂșltiplo: mais eficiente que INSERTs separados
INSERT INTO produtos (nome, preco, estoque) VALUES
  ('Mouse',    45.90,  150),
  ('Teclado',  120.00, 80),
  ('Monitor',  890.00, 25);

-- RETURNING: obtém o id gerado sem um SELECT extra
INSERT INTO pedidos (cliente_id, total)
VALUES (1, 350.00)
RETURNING id, criado_em;

-- INSERT com SELECT: copia produtos descontinuados para histĂłrico
INSERT INTO produtos_historico (produto_id, nome, preco, arquivado_em)
SELECT id, nome, preco, NOW()
FROM produtos
WHERE descontinuado = TRUE;
Boas prĂĄticas de INSERT

Sempre liste as colunas explicitamente — nunca confie na ordem implícita da tabela.
Use INSERT em lote quando precisar inserir muitos registros de uma vez.
Use RETURNING para obter IDs gerados em vez de fazer SELECT MAX(id).
Envolva INSERTs em cascata em transaçÔes — se a inserção do item falhar, o pedido deve ser desfeito.

3. SELECT — Consultando dados

O SELECT Ă© o comando mais usado em SQL — e tambĂ©m o mais poderoso. Toda consulta começa com uma pergunta em linguagem natural ("quais produtos custam mais de R$100 e tĂȘm estoque disponĂ­vel?") e termina em SQL que o banco executa eficientemente. O processo de transformar perguntas de negĂłcio em SQL Ă© uma habilidade fundamental para desenvolvedores back-end.

O SELECT * (asterisco) retorna todas as colunas de uma tabela e Ă© conveniente para exploração durante o desenvolvimento. No entanto, em cĂłdigo de produção, prefira sempre listar as colunas que vocĂȘ realmente precisa. Isso reduz a quantidade de dados transferidos pelo banco, documenta o contrato de dados da sua query e evita surpresas quando alguĂ©m adiciona novas colunas Ă  tabela. Um SELECT * em produção tambĂ©m dificulta o uso de Ă­ndices cobrindo (covering indexes), que sĂł sĂŁo usados quando o Ă­ndice contĂ©m todas as colunas que a query precisa.

Os aliases (apelidos) com AS permitem renomear colunas no resultado da query. Isso Ă© especialmente Ăștil quando o nome da coluna no banco Ă© tĂ©cnico (como dt_nasc) e vocĂȘ quer retornar algo mais legĂ­vel para a camada de aplicação (como data_nascimento). Aliases tambĂ©m sĂŁo usados em expressĂ”es calculadas — vocĂȘ nĂŁo pode fazer SELECT preco * 0.9 FROM produtos ORDER BY preco * 0.9; Ă© muito mais claro usar SELECT preco * 0.9 AS preco_com_desconto FROM produtos ORDER BY preco_com_desconto.

SELECT com colunas especĂ­ficas e aliases
-- Listando colunas especĂ­ficas com alias
SELECT
  nome                          AS produto,
  preco                         AS preco_original,
  preco * 0.9                   AS preco_com_desconto,
  estoque                       AS quantidade_disponivel
FROM produtos
WHERE estoque > 0
ORDER BY preco_com_desconto DESC;

3.1 WHERE, BETWEEN, IN, LIKE, IS NULL

A clĂĄusula WHERE Ă© onde a maioria da lĂłgica de filtro acontece. VocĂȘ pode combinar condiçÔes com AND e OR, agrupando com parĂȘnteses quando necessĂĄrio para controlar a precedĂȘncia — AND tem precedĂȘncia sobre OR, assim como multiplicação tem sobre adição na matemĂĄtica. Uma query sem WHERE retorna todos os registros da tabela, o que pode ser intencional (em uma listagem completa) ou acidental (em um UPDATE, como veremos, isso Ă© catastrĂłfico).

O operador BETWEEN Ă© um atalho conveniente para coluna >= valor1 AND coluna <= valor2 — observe que Ă© inclusivo em ambos os lados. O operador IN permite comparar uma coluna com uma lista de valores, substituindo mĂșltiplos OR. O LIKE permite busca por padrĂŁo em strings: % representa zero ou mais caracteres e _ representa exatamente um caractere. Para buscas case-insensitive no PostgreSQL, use ILIKE em vez de LIKE.

O tratamento de valores nulos exige atenção especial. Em SQL, NULL representa a ausĂȘncia de valor — e qualquer comparação com NULL usando = ou != retorna NULL (nĂŁo TRUE, nĂŁo FALSE — NULL). Por isso, para verificar se um campo Ă© nulo, vocĂȘ deve usar IS NULL ou IS NOT NULL. Esquecer isso Ă© um bug clĂĄssico: WHERE descricao != NULL nunca retorna nada, pois a comparação Ă© NULL, nĂŁo TRUE.

SELECT com filtros avançados
-- BETWEEN: faixa de preço (inclusivo em ambos os lados)
SELECT nome, preco FROM produtos
WHERE preco BETWEEN 50 AND 500
  AND estoque > 0
ORDER BY preco DESC
LIMIT 10;

-- IN: filtrar por lista de valores
SELECT nome, categoria FROM produtos
WHERE categoria IN ('eletrÎnicos', 'acessórios', 'periféricos');

-- ILIKE: busca case-insensitive por padrĂŁo
SELECT nome, email FROM clientes
WHERE nome ILIKE '%silva%';

-- IS NULL / IS NOT NULL: valores ausentes
SELECT nome, telefone FROM clientes
WHERE telefone IS NULL;        -- clientes sem telefone cadastrado

SELECT nome, descricao FROM produtos
WHERE descricao IS NOT NULL;  -- produtos que tĂȘm descrição
Cuidado: comparaçÔes com NULL

Em SQL, NULL = NULL retorna NULL, nĂŁo TRUE. Para verificar valores nulos, sempre use IS NULL ou IS NOT NULL. Usar = NULL Ă© um erro silencioso que nĂŁo filtra nada e nĂŁo gera nenhuma mensagem de erro — apenas retorna um conjunto vazio inesperado.

4. FunçÔes de agregação

As funçÔes de agregação calculam um valor Ășnico a partir de um conjunto de linhas. Elas sĂŁo fundamentais para relatĂłrios, dashboards e qualquer situação em que vocĂȘ precisa responder perguntas como "quantos?", "qual a soma?", "qual a mĂ©dia?", "qual o maior?" ou "qual o menor?". Sem funçÔes de agregação, vocĂȘ teria que buscar todos os dados para a aplicação e calcular tudo no cĂłdigo — o que Ă© ineficiente, escala mal e aumenta o trĂĄfego de rede desnecessariamente.

A distinção entre COUNT(*) e COUNT(coluna) Ă© importante e frequentemente confunde iniciantes. COUNT(*) conta o nĂșmero de linhas no conjunto de resultados, independente de qualquer valor ser NULL. COUNT(coluna) conta apenas as linhas onde o valor dessa coluna especĂ­fica nĂŁo Ă© NULL. Se vocĂȘ quer saber quantos clientes existem, use COUNT(*). Se quer saber quantos clientes tĂȘm telefone cadastrado (campo opcional), use COUNT(telefone). Essa diferença pode produzir resultados significativamente diferentes em tabelas com muitos valores nulos.

As funçÔes SUM, AVG, MIN e MAX tambĂ©m ignoram valores NULL automaticamente — o que geralmente Ă© o comportamento desejado. Uma exceção importante: se vocĂȘ usar AVG em uma coluna onde alguns valores sĂŁo NULL e outros sĂŁo zero, os zeros entram no cĂĄlculo mas os NULLs nĂŁo. Isso pode fazer a mĂ©dia parecer maior do que seria se NULL fosse tratado como zero. Se NULL foi usado erroneamente para representar "zero", vocĂȘ tem um problema de modelagem de dados que distorcerĂĄ qualquer agregação.

COUNT
Conta linhas ou valores nĂŁo nulos
COUNT(*) vs COUNT(col)
SUM
Soma de valores numéricos
Total de vendas do dia
AVG
Média aritmética dos valores
Preço médio por categoria
MAX
Maior valor no conjunto
Produto mais caro
MIN
Menor valor no conjunto
Pedido mais antigo

4.1 GROUP BY e HAVING

O GROUP BY divide os resultados em grupos com base nos valores de uma ou mais colunas, e as funçÔes de agregação sĂŁo calculadas para cada grupo separadamente. Por exemplo, para calcular o total de vendas por categoria de produto, vocĂȘ agrupa por categoria e soma os valores. Uma regra obrigatĂłria do SQL: toda coluna que aparece no SELECT e nĂŁo estĂĄ dentro de uma função de agregação deve aparecer no GROUP BY. Violar isso Ă© um erro em bancos como PostgreSQL (alguns outros bancos, como MySQL em modo permissivo, aceitam — mas produzem resultados imprevisĂ­veis).

O HAVING filtra grupos apĂłs a agregação, da mesma forma que o WHERE filtra linhas antes. A distinção Ă© crucial: WHERE age sobre as linhas individuais antes do agrupamento; HAVING age sobre os grupos resultantes depois do agrupamento. VocĂȘ nĂŁo pode usar funçÔes de agregação no WHERE — isso gerarĂĄ um erro. Se quiser filtrar por uma condição que envolve uma agregação (como "mostrar apenas categorias com mais de 5 produtos"), use HAVING.

GROUP BY e HAVING
-- Total e média de preço por categoria
SELECT
  categoria,
  COUNT(*) AS total_produtos,
  AVG(preco) AS preco_medio,
  SUM(preco * estoque) AS valor_em_estoque
FROM produtos
GROUP BY categoria
HAVING COUNT(*) > 5         -- sĂł categorias com mais de 5 produtos
ORDER BY valor_em_estoque DESC;

-- Clientes com mais de 3 pedidos
SELECT
  c.nome,
  COUNT(p.id) AS total_pedidos,
  SUM(p.total) AS valor_total_compras
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nome
HAVING COUNT(p.id) > 3
ORDER BY valor_total_compras DESC;
ClåusulaQuando agePode usar agregação?Exemplo
WHEREAntes do GROUP BY — filtra linhasNãoWHERE preco > 100
GROUP BYDepois do WHERE — agrupa os grupos—GROUP BY categoria
HAVINGDepois do GROUP BY — filtra gruposSimHAVING COUNT(*) > 5
ORDER BYPor Ășltimo — ordena o resultadoSimORDER BY AVG(preco) DESC

5. UPDATE — Atualizando dados

O comando UPDATE modifica o valor de uma ou mais colunas em registros existentes. Sua sintaxe Ă© aparentemente simples — UPDATE tabela SET coluna = valor WHERE condição — mas esconde um perigo que pode destruir dados de produção em segundos. O perigo Ă© a ausĂȘncia da clĂĄusula WHERE: um UPDATE sem WHERE atualiza todos os registros da tabela. Um UPDATE usuarios SET senha = 'abc123' sem WHERE nĂŁo atualiza um usuĂĄrio — ele atualiza todos os usuĂĄrios, substituindo todas as senhas. Esse tipo de acidente Ă© responsĂĄvel por incidentes de segurança e perdas de dados que custam horas ou dias de trabalho para recuperar.

A regra de ouro — praticada por desenvolvedores experientes — Ă© sempre verificar o WHERE com um SELECT antes de executar o UPDATE. Escreva o SELECT equivalente Ă  sua atualização, execute-o, e confirme que os registros retornados sĂŁo exatamente os que vocĂȘ quer atualizar. SĂł entĂŁo substitua o SELECT pelo UPDATE. Essa prĂĄtica de dois passos Ă© especialmente importante quando vocĂȘ estĂĄ trabalhando diretamente em um banco de produção, onde nĂŁo hĂĄ como fazer Ctrl+Z.

Assim como o INSERT, o UPDATE suporta a clĂĄusula RETURNING no PostgreSQL, permitindo que vocĂȘ veja os valores atualizados sem precisar de um SELECT adicional. Isso Ă© particularmente Ăștil quando a atualização depende do valor anterior — por exemplo, incrementar um contador — e vocĂȘ precisa saber qual foi o valor resultante. O UPDATE tambĂ©m suporta subqueries no SET e no WHERE, permitindo atualizaçÔes complexas baseadas em dados de outras tabelas.

UPDATE seguro: verificar antes, atualizar depois
-- PASSO 1: verificar quais registros serĂŁo afetados
SELECT * FROM produtos WHERE categoria = 'eletrĂŽnicos';

-- PASSO 2: sĂł apĂłs confirmar, executar o UPDATE
UPDATE produtos
SET desconto = 0.10
WHERE categoria = 'eletrĂŽnicos';

-- UPDATE com RETURNING: ver o resultado imediatamente
UPDATE pedidos
SET status = 'confirmado', atualizado_em = NOW()
WHERE id = 42
RETURNING id, status, atualizado_em;

-- UPDATE com subquery: atualizar estoque com base em pedidos
UPDATE produtos p
SET estoque = p.estoque - (
  SELECT SUM(i.quantidade)
  FROM itens_pedido i
  WHERE i.produto_id = p.id
    AND i.pedido_id = 42
)
WHERE p.id IN (
  SELECT produto_id FROM itens_pedido WHERE pedido_id = 42
);
Perigo: UPDATE sem WHERE

Nunca execute um UPDATE sem WHERE em produção. Sempre valide os registros afetados com um SELECT antes. Em ambientes crĂ­ticos, considere começar o UPDATE dentro de uma transação (BEGIN) e sĂł fazer COMMIT apĂłs confirmar que os dados estĂŁo corretos — assim vocĂȘ pode usar ROLLBACK se necessĂĄrio.

6. DELETE — Excluindo dados

O comando DELETE FROM remove registros de uma tabela de forma permanente. Como o UPDATE, o DELETE sem WHERE remove todos os registros da tabela — algo potencialmente catastrĂłfico. A mesma prĂĄtica de segurança se aplica: valide com um SELECT primeiro, execute o DELETE depois. E como o UPDATE, o DELETE tambĂ©m suporta RETURNING no PostgreSQL, o que pode ser Ăștil quando vocĂȘ precisa saber exatamente o que foi removido para fins de auditoria ou para desfazer a operação em outro sistema.

O DELETE com subquery permite remover registros baseados em condiçÔes de outras tabelas. Por exemplo, para remover logs antigos sem um Ă­ndice eficiente na coluna de data, vocĂȘ pode usar um subquery com LIMIT para deletar em lotes menores, evitando bloquear a tabela por tempo excessivo. Essa tĂ©cnica de "delete em lotes" Ă© uma boa prĂĄtica em tabelas de log e histĂłrico de grandes sistemas, onde deletar milhĂ”es de registros de uma vez pode degradar o desempenho do banco por minutos.

Em muitos sistemas de negĂłcio, a exclusĂŁo permanente de dados nĂŁo Ă© desejĂĄvel — nem do ponto de vista tĂ©cnico (um cliente que cancela pode voltar, um pedido cancelado pode precisar de auditoria) nem do ponto de vista legal (legislaçÔes como LGPD e GDPR tĂȘm regras sobre retenção e exclusĂŁo de dados). Por isso, o padrĂŁo de soft delete (exclusĂŁo suave) Ă© amplamente usado: em vez de deletar o registro, vocĂȘ adiciona uma coluna como deleted_at TIMESTAMP ou ativo BOOLEAN e marca o registro como inativo. O registro continua no banco, mas as queries normais o excluem do resultado com WHERE deleted_at IS NULL.

6.1 DELETE vs TRUNCATE vs DROP

Esses trĂȘs comandos tĂȘm efeitos muito diferentes e confundi-los Ă© um erro perigoso. O DELETE remove registros selecionados (ou todos, sem WHERE), registra cada remoção no log de transaçÔes e pode ser revertido com ROLLBACK dentro de uma transação. O TRUNCATE remove todos os registros de uma tabela de forma muito mais rĂĄpida que um DELETE sem WHERE, pois nĂŁo registra cada linha no log — ele simplesmente descarta as pĂĄginas de dados e reseta os contadores de SERIAL. E o DROP TABLE nĂŁo apenas remove os dados — ele remove a tabela inteira, incluindo sua definição, Ă­ndices e constraints.

DELETE, TRUNCATE e soft delete
-- DELETE com WHERE: remove registros especĂ­ficos
DELETE FROM logs
WHERE criado_em < NOW() - INTERVAL '30 days';

-- DELETE com RETURNING: saber o que foi removido
DELETE FROM sessoes
WHERE expirado_em < NOW()
RETURNING usuario_id, criado_em;

-- TRUNCATE: remove TUDO rapidamente (sem WHERE!)
TRUNCATE logs;
TRUNCATE logs RESTART IDENTITY; -- tambĂ©m reseta sequĂȘncia SERIAL

-- Soft delete: desativar em vez de remover
UPDATE clientes
SET deleted_at = NOW()
WHERE id = 7;

-- Query que exclui registros "deletados"
SELECT * FROM clientes
WHERE deleted_at IS NULL;   -- apenas ativos
ComandoO que removeTransacionĂĄvel?VelocidadeRemove a estrutura?
DELETELinhas selecionadas (ou todas)Sim — pode usar ROLLBACKLento em grandes volumesNão
TRUNCATETodas as linhasSim no PostgreSQLMuito rĂĄpidoNĂŁo
DROP TABLETudo — dados e estruturaNĂŁo Ă© recomendadoImediatoSim — tabela some
Soft Delete na prĂĄtica

Para implementar soft delete, adicione deleted_at TIMESTAMP DEFAULT NULL Ă  tabela. Registros ativos tĂȘm deleted_at IS NULL. Registros excluĂ­dos tĂȘm um timestamp. Crie um Ă­ndice em deleted_at e inclua sempre WHERE deleted_at IS NULL nas suas queries de listagem. Isso preserva o histĂłrico, facilita auditoria e permite restaurar dados acidentalmente "excluĂ­dos".

7. Índices — Acelerando consultas

Um Ă­ndice Ă© uma estrutura de dados auxiliar que o banco mantĂ©m separadamente dos dados da tabela, com o objetivo de acelerar consultas. A analogia clĂĄssica Ă© o Ă­ndice remissivo de um livro: sem ele, para encontrar todas as pĂĄginas que mencionam "transação", vocĂȘ teria que ler o livro inteiro. Com o Ă­ndice, vocĂȘ vai diretamente Ă s pĂĄginas listadas. Da mesma forma, sem um Ă­ndice em cliente_id na tabela de pedidos, o banco precisa varrer todos os pedidos para encontrar os de um cliente especĂ­fico — uma operação chamada de sequential scan. Com o Ă­ndice, ele vai diretamente Ă s linhas relevantes.

O tipo de Ă­ndice padrĂŁo do PostgreSQL Ă© o B-tree (ĂĄrvore B balanceada). Internamente, o Ă­ndice organiza os valores indexados em uma estrutura de ĂĄrvore onde cada nĂł aponta para um subconjunto de valores. Buscar um valor começa na raiz e percorre a ĂĄrvore — tipicamente 3 a 4 nĂ­veis — atĂ© encontrar as linhas correspondentes. Isso reduz uma busca em uma tabela de 1 milhĂŁo de registros de 1.000.000 comparaçÔes para cerca de 20. B-trees funcionam para comparaçÔes de igualdade (=), ranges (BETWEEN, <, >) e ordenação (ORDER BY). Para buscas por texto com LIKE '%palavra%' (prefixo desconhecido), um B-tree nĂŁo ajuda — seria necessĂĄrio um Ă­ndice de texto completo (GIN/GiST).

O custo de um índice não é apenas de espaço em disco (tipicamente 10-30% do tamanho da tabela). Cada INSERT, UPDATE e DELETE precisa também atualizar todos os índices da tabela. Em tabelas com muitos índices e alto volume de escrita, os índices podem se tornar um gargalo. A regra pråtica: crie índices em colunas usadas frequentemente em WHERE, JOIN ON e ORDER BY. Evite indexar colunas com poucos valores distintos (como um campo booleano ativo), pois o banco muitas vezes preferirå um sequential scan nesses casos de qualquer jeito.

CREATE INDEX e boas prĂĄticas
-- Índice simples em coluna de JOIN/WHERE frequente
CREATE INDEX idx_pedidos_cliente_id
  ON pedidos (cliente_id);

-- Índice composto: Ăștil quando WHERE usa ambas as colunas
CREATE INDEX idx_pedidos_status_criado
  ON pedidos (status, criado_em DESC);

-- Índice Ășnico: garante unicidade e acelera buscas
CREATE UNIQUE INDEX idx_clientes_email
  ON clientes (email);

-- Índice parcial: só indexa registros ativos (menor, mais rápido)
CREATE INDEX idx_produtos_ativos
  ON produtos (categoria, preco)
  WHERE deleted_at IS NULL;

7.1 EXPLAIN ANALYZE — Entendendo o que o banco faz

O comando EXPLAIN ANALYZE Ă© a ferramenta mais poderosa para diagnosticar problemas de desempenho em queries SQL. Enquanto EXPLAIN mostra o plano que o banco pretende executar (sem executar a query de fato), EXPLAIN ANALYZE executa a query e mostra o plano real com os tempos medidos em cada etapa. Isso permite comparar estimativas com a realidade, identificar onde o banco estĂĄ gastando mais tempo e confirmar se seus Ă­ndices estĂŁo sendo usados.

No plano de execução, vocĂȘ procura dois indicadores principais: Seq Scan (sequential scan — varrendo a tabela toda) versus Index Scan (usando um Ă­ndice). Um Seq Scan em uma tabela pequena (poucos milhares de registros) Ă© normal e esperado. Um Seq Scan em uma tabela com milhĂ”es de registros em uma coluna de filtro usada frequentemente Ă© um sinal claro de que um Ă­ndice estĂĄ faltando. ApĂłs criar o Ă­ndice, execute o EXPLAIN ANALYZE novamente e verifique se o plano mudou.

EXPLAIN ANALYZE na prĂĄtica
-- Analisa o plano de execução E executa a query
EXPLAIN ANALYZE
SELECT * FROM pedidos WHERE cliente_id = 42;

-- SaĂ­da tĂ­pica SEM Ă­ndice (ruim):
-- Seq Scan on pedidos  (cost=0.00..245.00 rows=3 width=48)
--   Filter: (cliente_id = 42)
--   Rows Removed by Filter: 9997
-- Execution Time: 12.345 ms

-- SaĂ­da tĂ­pica COM Ă­ndice (bom):
-- Index Scan using idx_pedidos_cliente_id
--   Index Cond: (cliente_id = 42)
-- Execution Time: 0.112 ms
Regra de ouro dos Ă­ndices

Crie Ă­ndices nas colunas que aparecem em WHERE, JOIN ON e ORDER BY em queries frequentes e de alto volume. Mas nĂŁo crie Ă­ndices "por precaução" — cada Ă­ndice tem um custo em escrita. Medir com EXPLAIN ANALYZE antes e depois Ă© a Ășnica forma de saber se um Ă­ndice estĂĄ ajudando.

8. TransaçÔes na pråtica

Uma transação Ă© um grupo de operaçÔes SQL que sĂŁo executadas como uma unidade atĂŽmica — ou todas tĂȘm sucesso, ou nenhuma tem efeito. As propriedades que garantem isso sĂŁo conhecidas pelo acrĂŽnimo ACID: Atomicidade (tudo ou nada), ConsistĂȘncia (o banco passa de um estado vĂĄlido para outro), Isolamento (transaçÔes concorrentes nĂŁo interferem entre si) e Durabilidade (uma vez confirmada, a transação persiste mesmo em caso de falha de hardware). Sem transaçÔes, um banco de dados relacional nĂŁo seria confiĂĄvel para aplicaçÔes que precisam de garantias de integridade.

O exemplo clĂĄssico de por que transaçÔes sĂŁo necessĂĄrias Ă© a transferĂȘncia bancĂĄria. Para transferir R$100 da conta A para a conta B, vocĂȘ precisa de dois UPDATEs: debitar R$100 da conta A e creditar R$100 na conta B. Se o sistema falhar apĂłs o primeiro UPDATE e antes do segundo — seja por queda de energia, bug na aplicação ou sobrecarga do servidor — o dinheiro desaparecerĂĄ da conta A sem aparecer na conta B. Com uma transação, esses dois UPDATEs sĂŁo atĂŽmicos: se qualquer um falhar, ambos sĂŁo desfeitos automaticamente. O banco nunca ficarĂĄ em um estado inconsistente.

O SAVEPOINT permite criar pontos de retorno dentro de uma transação, de forma que vocĂȘ possa fazer rollback parcial sem desfazer toda a transação. Isso Ă© Ăștil em operaçÔes complexas onde algumas etapas podem falhar de forma aceitĂĄvel. Por exemplo, em um processo de importação de dados, vocĂȘ pode usar um SAVEPOINT por registro importado — se um registro falhar (dados invĂĄlidos), vocĂȘ faz rollback ao SAVEPOINT daquele registro e continua com o prĂłximo, sem perder os registros jĂĄ importados com sucesso.

1
BEGIN — inicia a transação. O banco cria um contexto isolado para estas operaçÔes
2
UPDATE contas SET saldo = saldo - 100 WHERE id = 1 — debita da conta origem
3
UPDATE contas SET saldo = saldo + 100 WHERE id = 2 — credita na conta destino
4
Verificar se ambas as contas tĂȘm saldo coerente — validação opcional antes de confirmar
5
COMMIT — confirma. AlteraçÔes se tornam permanentes e visĂ­veis para outras transaçÔes
!
ROLLBACK — em caso de erro em qualquer passo, desfaz TUDO desde o BEGIN
Transação completa com SAVEPOINT
-- TransferĂȘncia bancĂĄria segura com transação
BEGIN;

SAVEPOINT antes_da_transferencia;

UPDATE contas SET saldo = saldo - 100.00 WHERE id = 1;
UPDATE contas SET saldo = saldo + 100.00 WHERE id = 2;

-- Verificar se nenhuma conta ficou negativa
SELECT id, saldo FROM contas WHERE id IN (1, 2);

-- Se tudo ok:
COMMIT;

-- Se algo deu errado:
-- ROLLBACK TO SAVEPOINT antes_da_transferencia;
-- ROLLBACK;  (desfaz tudo desde o BEGIN)

Aprofundamento: o que acontece se o servidor cair no meio de uma transação?

O PostgreSQL usa um mecanismo chamado Write-Ahead Log (WAL) para garantir durabilidade. Antes de modificar os dados no disco, o banco escreve a intenção no log WAL. Se o servidor cair durante uma transação que ainda nĂŁo fez COMMIT, ao reiniciar, o banco verifica o WAL, descobre que havia uma transação incompleta e automaticamente faz o rollback. Apenas as transaçÔes que chegaram a fazer COMMIT — ou seja, que gravaram o registro de commit no WAL — sĂŁo recuperadas.

Isso significa que vocĂȘ nunca precisa se preocupar com "transação parcialmente gravada no disco". O banco garante que ou a transação toda estĂĄ lĂĄ, ou nĂŁo estĂĄ. Essa garantia Ă© implementada pelo WAL em conjunto com o sistema de checkpoints do PostgreSQL, que periodicamente sincroniza o estado do banco com o disco.

9. Diagramas de SequĂȘncia — visualizando o fluxo CRUD

AtĂ© aqui vocĂȘ viu o SQL — a linguagem que descreve o que queremos que o banco faça. Mas entre o seu comando e a linha modificada no disco hĂĄ uma cadeia de componentes em interação: a aplicação valida, o driver serializa, o banco abre uma transação, o lock manager protege a linha, o query planner escolhe um Ă­ndice, o WAL grava a intenção e sĂł entĂŁo o COMMIT torna a mudança visĂ­vel para outros. Quando algo dĂĄ errado — uma duplicidade de e-mail, um deadlock, um pedido com itens parciais — vocĂȘ precisa entender onde na cadeia o problema apareceu para diagnosticĂĄ-lo.

O Diagrama de SequĂȘncia UML Ă© a ferramenta canĂŽnica para visualizar essa cadeia. Diferentemente do diagrama de classes (estrutura) ou do diagrama de Casos de Uso (objetivos do ator), o diagrama de sequĂȘncia mostra linhas verticais de vida (uma por componente) e setas horizontais cronolĂłgicas (mensagens trocadas), com o tempo fluindo de cima para baixo. Blocos especiais — alt (alternativa), opt (opcional), loop (repetição), par (paralelo) — modelam decisĂ”es e iteração. Notas anexadas a participantes esclarecem invariantes e custos.

Nesta seção vocĂȘ vai ler seis diagramas que cobrem os fluxos CRUD que aprendemos nas seçÔes anteriores: o INSERT que pode violar UNIQUE, o SELECT que se beneficia (ou nĂŁo) de Ă­ndice, o UPDATE protegido por lock e versionamento MVCC, o DELETE que esbarra em FK, a transação multi-tabela com estoque e a transferĂȘncia bancĂĄria com SAVEPOINT. Cada diagrama Ă© seguido de uma leitura linha a linha, dos atores envolvidos, de uma armadilha comum e do takeaway prĂĄtico. Se um Ășnico conceito vocĂȘ levar desta seção, que seja: SQL Ă© declarativo, mas sua execução Ă© orquestrada — diagramas de sequĂȘncia sĂŁo o roteiro dessa orquestração.

📖 Como ler um diagrama de sequĂȘncia

Linhas verticais = participantes (atores, sistemas, componentes). Setas → = mensagens sĂ­ncronas (chama e espera resposta). Setas ⇱ tracejadas = retorno. Tempo = de cima para baixo. Tudo que estĂĄ no mesmo "bloco horizontal" acontece em sequĂȘncia cronolĂłgica.

đŸ§© Blocos estruturais

alt = um e somente um dos caminhos serĂĄ percorrido (if/else). opt = caminho que pode ou nĂŁo ocorrer. loop = bloco repetido N vezes. Note over X,Y = anotação que cobre mĂșltiplos participantes. Combinados, esses blocos descrevem qualquer fluxo CRUD do mais simples ao mais complexo.

INSERT

9.1 INSERT com RETURNING e violação de UNIQUE

O fluxo mais comum de criação: a API recebe dados do usuĂĄrio, valida no nĂ­vel de aplicação, abre uma transação implĂ­cita, executa o INSERT com RETURNING id para devolver o identificador gerado e confirma com COMMIT. O ponto delicado Ă© a violação de UNIQUE — quando o e-mail jĂĄ existe, o PostgreSQL retorna o SQLSTATE 23505 e a transação precisa de ROLLBACK explĂ­cito antes que a aplicação retorne 409 ao cliente. Note que o WAL grava a intenção antes de o COMMIT retornar OK: essa Ă© a base da Durabilidade do ACID.

sequenceDiagram autonumber actor U as đŸ‘€ UsuĂĄrio participant API as API (Node.js) participant DB as PostgreSQL participant WAL as Write-Ahead Log U->>API: POST /clientes {nome, email} API->>API: Valida formato do e-mail e tamanho do nome alt Dados invĂĄlidos API-->>U: 400 Bad Request {erros: [...]} else Dados vĂĄlidos API->>DB: BEGIN API->>DB: INSERT INTO clientes (nome, email) VALUES ($1, $2) RETURNING id DB->>DB: Verifica constraint UNIQUE(email) alt Email duplicado (SQLSTATE 23505) DB-->>API: ERROR unique_violation API->>DB: ROLLBACK API-->>U: 409 Conflict {erro: "email jĂĄ cadastrado"} else Insert vĂĄlido DB->>WAL: Grava registro de mudança (xmin) DB-->>API: id = 42 API->>DB: COMMIT DB->>WAL: Grava registro de COMMIT Note over DB,WAL: Durabilidade garantida — fsync no WAL DB-->>API: OK API-->>U: 201 Created {id: 42, nome, email} end end
đŸ‘€ UsuĂĄrio — origina a requisição via formulĂĄrio API — camada Node.js/Express; valida e orquestra PostgreSQL — executa SQL e checa constraints WAL — log persistente que garante durabilidade
💡 Takeaway: sempre trate 23505 (unique) e 23503 (FK) explicitamente — devolver 500 para conflitos previsíveis confunde o consumidor da API e esconde bugs.
⚠ Armadilha: esquecer o ROLLBACK apĂłs erro mantĂ©m a transação aberta no pool, "envenenando" a prĂłxima requisição que reusar a mesma conexĂŁo.
SELECT

9.2 SELECT — query planner, índice B-tree e buffer cache

O SELECT esconde uma decisĂŁo crĂ­tica: ler a tabela inteira (Sequential Scan) ou usar um Ă­ndice (Index Scan). O query planner faz essa escolha consultando estatĂ­sticas (pg_stats) sobre cardinalidade, seletividade e distribuição. Para uma coluna com Ă­ndice e seletividade alta, o planner caminha pela ĂĄrvore B-tree em O(log n) atĂ© encontrar os ponteiros para as linhas, depois lĂȘ apenas essas linhas no heap. AlĂ©m disso, antes de tocar o disco, ele verifica se a pĂĄgina jĂĄ estĂĄ no buffer cache (RAM): cache hit custa microssegundos, cache miss exige I/O e pode custar milissegundos.

sequenceDiagram autonumber participant API as API participant PL as Query Planner participant ST as pg_stats participant CACHE as Buffer Cache (RAM) participant IDX as Índice B-tree (cliente_id) participant HEAP as Heap (tabela pedidos) API->>PL: SELECT * FROM pedidos WHERE cliente_id = 42 PL->>ST: Consulta estatĂ­sticas (n_distinct, selectivity) ST-->>PL: cliente_id tem 5000 valores distintos PL->>PL: Decide plano de execução pelo menor custo alt Coluna SEM Ă­ndice PL->>HEAP: Sequential Scan — lĂȘ tabela inteira HEAP-->>PL: 100 000 linhas verificadas Note over PL,HEAP: Custo: O(n) — proibitivo em produção else Coluna COM Ă­ndice (cliente_id) PL->>IDX: Index Scan — busca chave 42 alt PĂĄgina do Ă­ndice no cache (hit) CACHE-->>IDX: Resposta em ~10 ”s else PĂĄgina fria (cache miss) IDX->>HEAP: Solicita leitura de pĂĄgina do disco HEAP-->>CACHE: Carrega pĂĄgina (~5 ms) CACHE-->>IDX: PĂĄgina agora disponĂ­vel em RAM end IDX-->>PL: Ponteiros TID para 3 tuplas PL->>HEAP: Fetch das 3 linhas apontadas HEAP-->>PL: 3 tuplas materializadas Note over PL,IDX: Custo: O(log n) + 3 lookups end PL-->>API: ResultSet com 3 pedidos
Query Planner — escolhe Sequential vs Index Scan pg_stats — view de estatísticas atualizada por ANALYZE Buffer Cache — pool de páginas em RAM (shared_buffers) Índice B-tree — estrutura ordenada para busca em O(log n) Heap — armazenamento físico das linhas
💡 Takeaway: EXPLAIN ANALYZE mostra exatamente esse fluxo — qual nó foi usado, quantas linhas voltaram, quanto cada etapa custou. Use sempre antes de criar um índice "por intuição".
⚠ Armadilha: Ă­ndice em coluna de baixa cardinalidade (ex.: status com 3 valores) frequentemente Ă© ignorado pelo planner — cria custo de escrita sem ganho de leitura.
UPDATE

9.3 UPDATE — lock de linha, MVCC e visibilidade

O UPDATE no PostgreSQL nĂŁo sobrescreve a linha original — ele cria uma nova versĂŁo da tupla (MVCC: Multi-Version Concurrency Control) e marca a antiga como expirada. Enquanto sua transação estĂĄ aberta, outras transaçÔes ainda enxergam a versĂŁo antiga; sĂł apĂłs o COMMIT a nova versĂŁo se torna visĂ­vel. Para evitar que duas transaçÔes concorrentes alterem a mesma linha simultaneamente, o lock manager solicita um ROW EXCLUSIVE. Se a linha jĂĄ estiver travada, sua transação espera — e se houver dependĂȘncia circular entre dois locks, o detector de deadlock aborta uma das transaçÔes.

sequenceDiagram autonumber participant API as API participant DB as PostgreSQL participant LM as Lock Manager participant MVCC as MVCC Engine participant ROW as Linha id=42 API->>DB: BEGIN API->>DB: UPDATE clientes SET status='ativo' WHERE id = 42 DB->>LM: Solicita ROW EXCLUSIVE em id=42 alt Linha jå travada por outra transação LM-->>DB: Aguarda liberação do lock... Note over LM,ROW: Detector de deadlock observa
a fila de locks e aborta uma transação
se detectar dependĂȘncia circular else Lock concedido LM-->>DB: ROW EXCLUSIVE adquirido DB->>MVCC: Cria nova versĂŁo da tupla (xmin = txid atual) MVCC->>ROW: Marca versĂŁo antiga como expirada (xmax) Note over MVCC,ROW: Outras transaçÔes ainda
leem a versão antiga (snapshot isolation) DB-->>API: 1 linha afetada alt Aplicação confirma API->>DB: COMMIT DB->>LM: Libera ROW EXCLUSIVE Note over MVCC,ROW: Nova versão visível para todos
a partir deste ponto else Erro detectado pela aplicação API->>DB: ROLLBACK DB->>MVCC: Descarta a nova versão DB->>LM: Libera ROW EXCLUSIVE end end
Lock Manager — coordena acesso concorrente MVCC Engine — versiona tuplas por xmin/xmax Linha — armazenada como mĂșltiplas versĂ”es enquanto hĂĄ transaçÔes abertas
💡 Takeaway: linhas atualizadas geram "tuplas mortas" no heap; o VACUUM (auto ou manual) Ă© quem recupera esse espaço — atualizaçÔes em massa sem VACUUM levam Ă  degradação chamada table bloat.
⚠ Armadilha: UPDATE clientes SET status='ativo' sem WHERE atualiza todas as linhas — sempre rode primeiro como SELECT com o mesmo WHERE para validar o conjunto afetado.
DELETE

9.4 DELETE — FOREIGN KEY constraints (RESTRICT, CASCADE, SET NULL)

Deletar uma linha referenciada por outras tabelas Ă© o ponto onde a integridade referencial entra em ação. A clĂĄusula ON DELETE da chave estrangeira define o comportamento: RESTRICT (default) bloqueia a deleção se houver dependentes, CASCADE propaga a deleção em ĂĄrvore e SET NULL mantĂ©m os dependentes mas zera a referĂȘncia. Em sistemas reais, CASCADE em tabelas crĂ­ticas (como pedidos) Ă© raramente desejĂĄvel — preferimos soft delete (marcar deleted_at) para preservar histĂłrico.

sequenceDiagram autonumber participant API as API participant DB as PostgreSQL participant CL as Tabela clientes participant PE as Tabela pedidos participant IT as Tabela itens_pedido API->>DB: DELETE FROM clientes WHERE id = 42 DB->>CL: Localiza linha id=42 DB->>PE: Verifica FK pedidos.cliente_id REFERENCES clientes(id) PE-->>DB: 5 pedidos referenciam o cliente 42 alt FK ON DELETE RESTRICT (default) DB-->>API: ERROR 23503 foreign_key_violation Note over DB,PE: Deleção bloqueada
protege integridade else FK ON DELETE CASCADE DB->>IT: Verifica FK itens_pedido.pedido_id REFERENCES pedidos(id) IT-->>DB: 23 itens referenciam os 5 pedidos DB->>IT: DELETE em cascata (23 linhas) DB->>PE: DELETE em cascata (5 linhas) DB->>CL: DELETE da linha original (1 linha) DB-->>API: OK — 29 linhas afetadas no total Note over DB,IT: ⚠ Dados removidos
permanentemente em 3 tabelas else FK ON DELETE SET NULL DB->>PE: UPDATE pedidos SET cliente_id = NULL WHERE cliente_id = 42 DB->>CL: DELETE da linha original DB-->>API: OK — pedidos preservados como órfãos Note over DB,PE: Útil para auditoria histórica
quando o cliente Ă© excluĂ­do por LGPD end
clientes / pedidos / itens_pedido — trĂȘs nĂ­veis de hierarquia FK constraint — declarada em CREATE TABLE ou ALTER TABLE
💡 Takeaway: a regra de FK Ă© decidida na modelagem, nĂŁo no DELETE — escolher CASCADE no DDL Ă© uma decisĂŁo arquitetural irreversĂ­vel na prĂĄtica (rollback exige migração).
⚠ Armadilha: ON DELETE CASCADE em tabelas com milhĂ”es de linhas pode disparar uma deleção que dura horas e segura locks — sempre prefira deleção em batch controlada pela aplicação.
TRANSAÇÃO

9.5 Transação multi-tabela: criar pedido com itens e baixar estoque

Este Ă© o fluxo "ordem + itens + estoque" — um padrĂŁo clĂĄssico em e-commerce. A operação envolve trĂȘs tabelas (pedidos, itens_pedido, produtos), mĂșltiplos INSERTs e UPDATEs e uma trava pessimista (SELECT ... FOR UPDATE) para evitar a venda concorrente do mesmo produto sem estoque. Se qualquer item falhar — estoque insuficiente, produto inexistente, erro de rede — o ROLLBACK desfaz a transação inteira, garantindo que o pedido nunca seja persistido pela metade. Esta Ă© a Atomicidade do ACID em ação.

sequenceDiagram autonumber actor U as đŸ‘€ Cliente participant API as API participant DB as PostgreSQL participant ORD as pedidos participant ITM as itens_pedido participant STK as produtos (estoque) U->>API: POST /pedidos {cliente_id, itens: [{prod, qtd}, ...]} API->>DB: BEGIN API->>DB: INSERT INTO pedidos (cliente_id, total) VALUES ($1, 0) RETURNING id DB->>ORD: Cria cabeçalho do pedido ORD-->>API: pedido_id = 100 loop Para cada item da requisição API->>DB: SELECT estoque, preco FROM produtos WHERE id = $1 FOR UPDATE DB->>STK: Lock pessimista na linha do produto STK-->>API: {estoque: 10, preco: 49.90} alt Estoque insuficiente API->>DB: ROLLBACK Note over DB,STK: Todos os locks liberados
nenhum dado persiste API-->>U: 422 Unprocessable Entity {erro: "sem estoque"} else Estoque suficiente API->>DB: INSERT INTO itens_pedido (pedido_id, produto_id, qtd, preco) DB->>ITM: Insere linha de detalhe API->>DB: UPDATE produtos SET estoque = estoque - $qtd WHERE id = $1 DB->>STK: Decrementa estoque (lock garante atomicidade) end end API->>DB: UPDATE pedidos SET total = (SELECT SUM(qtd*preco) FROM itens_pedido WHERE pedido_id = 100) WHERE id = 100 DB->>ORD: Atualiza total calculado API->>DB: COMMIT Note over DB,STK: Locks liberados
todas as 3 tabelas mudam atomicamente API-->>U: 201 Created {pedido_id: 100, total: 1234.56}
pedidos — cabeçalho (1 linha por pedido) itens_pedido — detalhe (N linhas por pedido) produtos — catálogo + controle de estoque FOR UPDATE — lock pessimista que evita oversell
💡 Takeaway: a Atomicidade do ACID Ă© o que torna seguro orquestrar 3 tabelas + N iteraçÔes em uma Ășnica "operação de negĂłcio" sem precisar de saga, retry ou compensação manual.
⚠ Armadilha: SELECT FOR UPDATE sem NOWAIT ou SKIP LOCKED pode bloquear requisiçÔes concorrentes indefinidamente — defina statement_timeout no pool de conexĂŁo como rede de segurança.
SAVEPOINT

9.6 SAVEPOINT — rollback parcial em transferĂȘncia bancĂĄria

O SAVEPOINT permite criar pontos de retorno dentro de uma transação aberta. É a ferramenta para implementar lĂłgica condicional nĂŁo-trivial: "tente debitar, se passar do limite faça rollback parcial e tente de outra fonte, mas mantenha o log de auditoria jĂĄ inserido". Diferente de aninhamento (que o PostgreSQL nĂŁo suporta diretamente para transaçÔes), o SAVEPOINT Ă© um marcador leve que pode ser revertido sem encerrar a transação. Este diagrama mostra a transferĂȘncia bancĂĄria clĂĄssica com validação de saldo negativo apĂłs os UPDATEs e trĂȘs caminhos possĂ­veis: COMMIT, ROLLBACK TO SAVEPOINT (parcial) e ROLLBACK total.

sequenceDiagram autonumber participant API as API BancĂĄria participant DB as PostgreSQL participant LOG as Tabela auditoria participant CA as Conta Origem id=1 participant CB as Conta Destino id=2 API->>DB: BEGIN API->>DB: INSERT INTO auditoria (tipo, valor, ts) VALUES ('TRANSF', 100.00, now()) DB->>LOG: Registra tentativa de transferĂȘncia API->>DB: SAVEPOINT antes_da_transferencia Note over DB,LOG: A partir daqui, o ROLLBACK parcial
preserva o log de auditoria API->>DB: UPDATE contas SET saldo = saldo - 100 WHERE id = 1 DB->>CA: Debita R$ 100 (saldo: 350 → 250) API->>DB: UPDATE contas SET saldo = saldo + 100 WHERE id = 2 DB->>CB: Credita R$ 100 (saldo: 700 → 800) API->>DB: SELECT id, saldo FROM contas WHERE id IN (1, 2) DB-->>API: [{id:1, saldo:250}, {id:2, saldo:800}] alt Validação OK (sem saldo negativo) API->>DB: COMMIT Note over DB,CB: TransferĂȘncia efetivada
auditoria + UPDATEs persistidos else Saldo negativo detectado API->>DB: ROLLBACK TO SAVEPOINT antes_da_transferencia Note over DB,CA: Apenas os UPDATEs sĂŁo desfeitos
o INSERT na auditoria permanece pendente API->>DB: UPDATE auditoria SET status='falhou' WHERE id = currval('auditoria_id_seq') API->>DB: COMMIT Note over DB,LOG: Rastreia tentativa falha
para anĂĄlise de fraude else Erro inesperado (ex: rede caiu) API->>DB: ROLLBACK Note over DB,LOG: Desfaz TUDO desde o BEGIN
nem mesmo a auditoria persiste end
SAVEPOINT — marcador nomeado dentro da transação ROLLBACK TO SAVEPOINT — reverte sĂł atĂ© o marcador auditoria — tabela append-only de eventos
💡 Takeaway: SAVEPOINT Ă© a ferramenta certa para recuperação parcial; reserve ROLLBACK total para erros que invalidam o contexto inteiro da transação.
⚠ Armadilha: SAVEPOINTs em loops longos consomem memĂłria do backend — apĂłs milhares deles a transação fica lenta. Use COMMIT frequentes em vez de SAVEPOINT-em-massa.
Conexão com a próxima seção

Na prĂłxima seção, RM-ODP, vamos enquadrar essas operaçÔes nos viewpoints Information (estrutura dos dados) e Computational (interfaces dos componentes). Diagramas de sequĂȘncia sĂŁo, na prĂĄtica, a materialização do viewpoint Computational — eles tornam visĂ­vel o contrato de mensagens entre componentes que vocĂȘ atĂ© agora viu apenas como SQL e cĂłdigo.

10. RM-ODP — Viewpoints Information e Computational

Na Aula 1, apresentamos as cinco visĂ”es do RM-ODP (Reference Model for Open Distributed Processing). Nesta aula, dois viewpoints se tornam especialmente relevantes: o Information e o Computational. Analisar as operaçÔes CRUD atravĂ©s dessas lentes nĂŁo Ă© um exercĂ­cio burocrĂĄtico — Ă© uma forma de pensar sobre o que o sistema faz com a informação e como essa informação Ă© acessada por diferentes componentes.

O viewpoint Information descreve como a informação Ă© estruturada, quais sĂŁo seus invariantes (regras que nunca podem ser violadas) e como ela evolui ao longo do tempo. No contexto do CRUD, cada tabela do banco Ă© um objeto de informação, e cada operação CRUD Ă© uma transformação desse objeto. Um INSERT cria uma nova instĂąncia do objeto. Um UPDATE transiciona o objeto de um estado para outro. Um DELETE (ou soft delete) marca o objeto como inativo. Os constraints do banco — NOT NULL, UNIQUE, CHECK, FOREIGN KEY — sĂŁo a forma de expressar os invariantes do viewpoint Information no SQL.

O viewpoint Computational descreve a decomposição funcional do sistema em componentes com interfaces bem definidas. As operaçÔes CRUD sĂŁo exatamente as interfaces que a camada de banco expĂ”e para a camada de aplicação. Quando um desenvolvedor back-end escreve um Model no padrĂŁo MVC, esse Model encapsula as operaçÔes CRUD de uma entidade — o ClienteModel.buscarPorId(id) nada mais Ă© que um SELECT com WHERE. O PedidoModel.criar(dados) Ă© um BEGIN + INSERT + INSERT (itens) + COMMIT. Pensar em CRUD como interfaces computacionais ajuda a projetar APIs mais coerentes e testĂĄveis.

📋
Information Viewpoint
Define as entidades (tabelas), atributos (colunas), relacionamentos (chaves estrangeiras) e invariantes (constraints). O banco de dados é a materialização deste viewpoint. Cada tabela é um objeto de informação; cada linha é uma instùncia desse objeto; cada constraint é um invariante que o banco garante automaticamente.
DER, CREATE TABLE, constraints, INSERT, SELECT
⚙️
Computational Viewpoint
Descreve os componentes funcionais e suas interfaces. Os Models do padrĂŁo MVC encapsulam as operaçÔes CRUD como interfaces bem definidas. Uma rota GET /produtos/:id Ă© uma interface computacional que internamente executa um SELECT. Uma rota POST /pedidos Ă© uma interface que executa uma transação com mĂșltiplos INSERTs.
Models, Controllers, funçÔes CRUD, endpoints REST
ConexĂŁo entre viewpoints

A camada de informação (banco) e a camada computacional (cĂłdigo) se comunicam por SQL. Um erro de modelagem no viewpoint Information — como armazenar mĂșltiplos valores em uma coluna — força o viewpoint Computational a implementar lĂłgica de parsing que nĂŁo deveria existir. Quando vocĂȘ modela bem o banco, o cĂłdigo fica mais simples. Quando o banco estĂĄ mal modelado, a complexidade migra para o cĂłdigo.

11. RF, RN e RNF — Mapeando CRUD a requisitos

Toda operação CRUD que vocĂȘ implementa deve ser rastreĂĄvel a pelo menos um Requisito Funcional (RF) que justifica sua existĂȘncia. Se nenhum RF exige que o sistema exclua pedidos cancelados definitivamente, entĂŁo o DELETE nĂŁo deveria estar no cĂłdigo — e o soft delete provavelmente Ă© a implementação correta. Esse rastreamento nĂŁo Ă© burocracia: ele Ă© a diferença entre desenvolver funcionalidades que o sistema precisa e desenvolver funcionalidades que ninguĂ©m pediu.

As Regras de NegĂłcio (RN) se traduzem diretamente em validaçÔes e constraints no banco. "Um pedido nĂŁo pode ter total negativo" Ă© uma RN que se materializa como CHECK (total >= 0) na tabela de pedidos — e tambĂ©m como validação na camada de aplicação (lembre: validação no banco Ă© a Ășltima linha de defesa, mas a aplicação deve validar antes para dar mensagens de erro adequadas ao usuĂĄrio). "Um e-mail de cliente deve ser Ășnico" Ă© uma RN que vira um UNIQUE INDEX na coluna email.

Os Requisitos NĂŁo Funcionais (RNF) associados Ă s operaçÔes CRUD se concentram em trĂȘs eixos principais: Desempenho (as consultas respondem em menos de X ms sob Y usuĂĄrios simultĂąneos — endereçado por Ă­ndices, queries otimizadas e EXPLAIN ANALYZE), Confiabilidade (os dados nunca ficam em estado inconsistente — endereçado por transaçÔes, constraints e backups), e Segurança (dados sensĂ­veis como senhas, CPFs e informaçÔes de cartĂŁo nunca sĂŁo armazenados em texto puro nem expostos desnecessariamente nas respostas da API — endereçado por criptografia, hashing e seleção cuidadosa das colunas no SELECT).

TipoExemploImplementação SQL/DB
RFO sistema permite cadastrar produtos com nome, preço e estoqueINSERT INTO produtos (nome, preco, estoque) VALUES (...)
RFO sistema lista os pedidos de um cliente, do mais recente ao mais antigoSELECT ... FROM pedidos WHERE cliente_id = ? ORDER BY criado_em DESC
RNO estoque não pode ficar negativo após uma vendaCHECK (estoque >= 0) + validação no UPDATE de estoque
RNUm pedido deve ter pelo menos um itemValidação na camada de aplicação antes do COMMIT
RNFDesempenho: listagem de pedidos por cliente em menos de 50msCREATE INDEX idx_pedidos_cliente_id ON pedidos(cliente_id)
RNFConfiabilidade: operaçÔes de compra nunca deixam o banco inconsistenteBEGIN/COMMIT/ROLLBACK em todos os fluxos de compra
RNFSegurança: senhas nunca armazenadas em texto puroHashing (bcrypt) antes do INSERT; nunca SELECT senha FROM usuarios
RNF precisa de métrica

"O sistema deve ser rĂĄpido" nĂŁo Ă© um RNF — Ă© uma intenção vaga. Um RNF bem escrito tem eixo, mĂ©trica, carga e evidĂȘncia: "As consultas de listagem de pedidos por cliente devem responder em menos de 50ms para 95% das requisiçÔes com 500 usuĂĄrios simultĂąneos, medido com EXPLAIN ANALYZE e teste de carga com k6." SĂł assim vocĂȘ sabe quando o requisito foi atendido.

12. Checklist de Estudo

Clique nos itens Ă  medida que vocĂȘ dominar cada conceito:

  • Consigo escrever um INSERT bĂĄsico listando explicitamente as colunas, sem omitir nenhuma NOT NULL.
  • Consigo inserir mĂșltiplas linhas em um Ășnico INSERT e usar RETURNING para obter o ID gerado.
  • Consigo escrever um SELECT com WHERE usando AND/OR, BETWEEN, IN, LIKE e IS NULL corretamente.
  • Consigo explicar a diferença entre COUNT(*) e COUNT(coluna) e quando cada um Ă© adequado.
  • Consigo usar GROUP BY com COUNT, SUM e AVG, e filtrar grupos com HAVING.
  • Consigo explicar por que WHERE e HAVING existem separados e quando usar cada um.
  • Consigo escrever um UPDATE seguro: verificar com SELECT antes, usar WHERE correto, e RETURNING.
  • Consigo explicar a diferença entre DELETE, TRUNCATE e DROP TABLE e quando cada um Ă© adequado.
  • Consigo descrever o padrĂŁo de soft delete e implementĂĄ-lo com a coluna deleted_at.
  • Consigo explicar como um Ă­ndice B-tree funciona e o tradeoff entre velocidade de leitura e escrita.
  • Consigo usar EXPLAIN ANALYZE para identificar se uma query estĂĄ fazendo Seq Scan desnecessĂĄrio.
  • Consigo escrever uma transação com BEGIN/COMMIT/ROLLBACK e explicar as propriedades ACID.
  • Consigo mapear as operaçÔes CRUD aos viewpoints Information e Computational do RM-ODP.
  • Consigo associar cada operação CRUD a RFs, RNs e RNFs com mĂ©tricas mensurĂĄveis.

ReferĂȘncias

Inteli Logo