Crud

CRUD (Create, Read, Update, Delete · criar, ler, atualizar, excluir) é o apelido das quatro operações que todo sistema faz no banco: INSERT, SELECT, UPDATE e DELETE. Esta página mostra como escrever cada uma no formato vertical e como decidir o que acontece quando um registro sai de cena.

A decisão de exclusão merece atenção desde o começo. O DELETE apaga a linha do disco e ninguém recupera o dado depois. O soft delete (exclusão lógica) marca a linha como inativa e mantém o histórico disponível para auditoria.

Conceitos fundamentais

ConceitoO que é
CRUD (Create, Read, Update, Delete · criar, ler, atualizar, excluir)As quatro operações básicas sobre uma tabela: INSERT, SELECT, UPDATE, DELETE
vertical formatting (formatação vertical)Cada coluna ou cláusula em sua linha; legível de cima para baixo, sem scroll horizontal
soft delete (exclusão lógica)Marcar a linha como removida (DeletedAt) sem apagar do disco
hard delete (exclusão física)DELETE que remove do disco; irreversível
upsert (inserir ou atualizar)MERGE ou INSERT ... ON CONFLICT; insere se a chave não existe, atualiza se existe
WHERE clause (cláusula WHERE)Filtro de linhas; sem WHERE, UPDATE/DELETE afeta toda a tabela
RETURNING / OUTPUT (cláusula de retorno)Recupera linhas afetadas pelo DML em uma única ida ao banco

O INSERT separa colunas e valores em dois blocos

Escrito na vertical, o INSERT deixa a coluna e o valor dela na mesma posição dos dois blocos: a terceira coluna corresponde ao terceiro valor. Com tudo em uma linha só, conferir se Email está recebendo o e-mail vira contagem de vírgulas.

❌ Ruim: colunas e valores em linha única
INSERT INTO Users(Id, Name, Email) VALUES(1, 'Alice', 'alice@email.com');
✅ Bom: colunas e valores em blocos verticais separados
INSERT INTO Users
(
  Id,
  Name,
  Email
)
VALUES
(
  1,
  'Alice',
  'alice@email.com'
);

INSERT que copia de outra tabela

Quando os valores vêm de um SELECT, a ordem das colunas do INSERT precisa bater com a ordem das colunas do SELECT. Alinhando os dois blocos na vertical, a correspondência fica visível linha a linha: Email embaixo de Email, ContactEmail embaixo de ContactEmail.

❌ Ruim: SELECT inline, sem correspondência visual entre colunas
INSERT INTO Users(Id, Name, Email) SELECT ExternalId, FullName, ContactEmail FROM ExternalUsers WHERE IsVerified = 1;
✅ Bom: colunas do INSERT alinhadas com colunas do SELECT
INSERT INTO Users
(
  Id,
  Name,
  Email
)
SELECT
  ExternalUsers.ExternalId,
  ExternalUsers.FullName,
  ExternalUsers.ContactEmail
FROM
  ExternalUsers
WHERE
  ExternalUsers.IsVerified = 1; -- verified account

UPDATE que busca o valor em outra tabela

O UPDATE ... FROM junta as duas tabelas uma vez e atualiza tudo numa passagem. A alternativa comum é a subquery correlacionada (subconsulta que roda de novo para cada linha da tabela externa) dentro do SET, e ela repete trabalho: com dez mil usuários a atualizar, o banco executa dez mil vezes o SELECT de dentro.

❌ Ruim: subquery correlacionada no SET
UPDATE Users
SET Email = (
  SELECT NewEmail
  FROM EmailUpdates
  WHERE EmailUpdates.UserId = Users.Id
)
WHERE Id IN (SELECT UserId FROM EmailUpdates);
✅ Bom: UPDATE ... FROM ... WHERE
UPDATE
  Users
SET
  Users.Email = EmailUpdates.NewEmail
FROM
  EmailUpdates
WHERE
  Users.Id = EmailUpdates.UserId;

Marcar como inativo em vez de apagar

O DELETE tira a linha do disco e o dado se foi. Quando alguém perguntar por que aquele pedido sumiu, ou quando a auditoria pedir o histórico do cliente, não há o que responder. Marcar a linha com IsActive = 0 e gravar o horário da inativação mantém o registro disponível para consulta e permite desfazer o engano.

❌ Ruim: remoção permanente sem rastro
DELETE FROM
  Users
WHERE
  Id = 1;
✅ Bom: soft delete com timestamp de inativação
UPDATE
  Users
SET
  Users.IsActive = 0, -- inactive
  Users.InactivatedAt = GETUTCDATE()
WHERE
  Users.Id = 1;

Um CASE resolve várias condições em uma passagem

Dois UPDATE seguidos, um para cada condição, varrem a tabela duas vezes. O CASE dentro do SET decide o valor linha a linha, e o banco percorre a tabela uma vez só.

❌ Ruim: um UPDATE por condição, duas passagens na tabela
UPDATE Orders SET StatusId = 2 WHERE IsActive = 1 AND CustomerId IN (SELECT Id FROM PremiumCustomers);
UPDATE Orders SET StatusId = 3 WHERE IsActive = 1 AND CustomerId NOT IN (SELECT Id FROM PremiumCustomers);
✅ Bom: CASE numa única passagem
UPDATE
  Orders
SET
  Orders.StatusId = (
    CASE
      WHEN PremiumCustomers.Id IS NOT NULL THEN 2 -- Priority
      ELSE 3 -- Standard
    END
  )
FROM
  Orders
LEFT JOIN
  PremiumCustomers ON Orders.CustomerId = PremiumCustomers.Id
WHERE
  Orders.IsActive = 1; -- active

Filtrar a tabela principal antes de juntar as outras

Cada linha que entra no JOIN é trabalho que o banco faz. Se a tabela de pedidos tem cinco milhões de linhas e o filtro deixa mil, aplicar o filtro primeiro faz a junção com clientes acontecer sobre mil linhas. Uma CTE (Common Table Expression · expressão de tabela nomeada) recorta a tabela principal e dá um nome a esse recorte, e o JOIN vem depois.

❌ Ruim: filtro aplicado depois do JOIN em tabela grande
SELECT
  Orders.Id,
  Orders.Total,
  Customers.Name
FROM
  Orders
JOIN
  Customers ON Orders.CustomerId = Customers.Id
WHERE
  Orders.Status = 'PENDING' AND
  Orders.CreatedAt >= '2025-01-01';
✅ Bom: CTE filtra a tabela principal antes do JOIN
WITH PendingOrdersCTE AS
(
  SELECT
    Orders.Id,
    Orders.Total,
    Orders.CustomerId
  FROM
    Orders
  WHERE
    Orders.Status = 'PENDING' AND
    Orders.CreatedAt >= '2025-01-01'
)

SELECT
  PendingOrdersCTE.Id,
  PendingOrdersCTE.Total,
  Customers.Name
FROM
  PendingOrdersCTE
JOIN
  Customers ON PendingOrdersCTE.CustomerId = Customers.Id;

Todo SELECT que devolve lista declara ORDER BY

Sem ORDER BY, o banco entrega as linhas na ordem que for mais barata para ele naquele momento, e essa ordem muda quando o plano de execução muda. A query que hoje devolve os times em ordem de cadastro pode devolver em outra ordem amanhã, depois de um índice novo. Se a ordem importa para quem lê o resultado, ela precisa estar escrita na query.

❌ Ruim: sem ORDER BY, ordem indefinida
SELECT
  FootballTeams.Name,
  FootballTeams.ChampionshipsWon
FROM
  FootballTeams
WHERE
  FootballTeams.IsActive = 1;
✅ Bom: ORDER BY explícito
SELECT
  FootballTeams.Name,
  FootballTeams.ChampionshipsWon
FROM
  FootballTeams
WHERE
  FootballTeams.IsActive = 1 -- active
ORDER BY
  FootballTeams.ChampionshipsWon DESC;

O número solto na query ganha um comentário

Orders.StatusId = 2 não diz nada a quem abre a query seis meses depois. Um comentário curto no fim da linha (-- Approved) devolve o significado sem exigir uma tabela de constantes ou uma camada nova de abstração. O mesmo vale para o 30 de uma janela de retenção.

❌ Ruim: números sem contexto
SELECT
  Orders.Id,
  Orders.Total
FROM
  Orders
WHERE
  Orders.StatusId = 2 AND
  DATEDIFF(day, Orders.CreatedAt, GETDATE()) > 30;
✅ Bom: comentário inline expõe a intenção
SELECT
  Orders.Id,
  Orders.Total
FROM
  Orders
WHERE
  Orders.StatusId = 2 AND -- Approved
  DATEDIFF(day, Orders.CreatedAt, GETDATE()) > 30; -- retention window

O valor que vem de fora entra como parâmetro nomeado

O valor que a aplicação envia (o status escolhido, a data inicial do relatório) entra na query como parâmetro: @StatusId no SQL Server, :statusId no PostgreSQL. Com o valor colado direto no texto da query, duas coisas acontecem: o banco trata cada combinação como uma query nova e joga fora o plano de execução que já tinha, e uma string vinda do usuário passa a poder alterar o comando (o ataque conhecido como SQL injection).

❌ Ruim: literais inline, sem contexto
SELECT
  Orders.Id,
  Orders.Total
FROM
  Orders
WHERE
  Orders.Status = 2 AND
  Orders.CreatedAt >= '2025-01-01';
✅ Bom: parâmetros nomeados (SQL Server / PostgreSQL)
-- SQL Server
SELECT
  Orders.Id,
  Orders.Total
FROM
  Orders
WHERE
  Orders.Status = @StatusId AND
  Orders.CreatedAt >= @StartDate;

-- PostgreSQL
SELECT
  Orders.Id,
  Orders.Total
FROM
  Orders
WHERE
  Orders.Status = :statusId AND
  Orders.CreatedAt >= :startDate;

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.