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

Banco de Dados II

Aula 3 — Material de Leitura Aprofundado

Sobre este encontro

Banco de Dados II · Prof. Afonso

Objetivo de aprendizagem

Ao final do encontro, o estudante deve ser capaz de aplicar ao projeto os conceitos de Banco de Dados II. Escopo do encontro: CRUD: INSERT, SELECT, UPDATE, DELETE.

Estratégia do encontro

Exposição dialogada dos conceitos, alternada com aplicação guiada ao projeto do grupo, e fechamento com verificação do entendimento.

Estrutura do encontro

  1. 1. Revisão: a estrutura está criada — agora vamos popular
  2. 2. INSERT — Inserindo dados
  3. 3. SELECT — Consultando dados
  4. 4. Funções de agregação
  5. 5. UPDATE — Atualizando dados
  6. 6. DELETE — Excluindo dados
  7. 7. Índices — Acelerando consultas
  8. 8. Transações na prática

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