How 2 Soft Delete.

Padrões e melhores práticas para retenção de dados.

Mateus Henrique Bosquetti
por Mateus Henrique Bosquetti
10 minutos de leitura

Em sistemas modernos, a restauração de dados é uma demanda real, seja por erro do usuário, auditoria ou suporte. O objetivo deste guia é apresentar o conceito do Soft Delete, assim como suas principais estratégias de forma interativa, para que você possa testar e escolher a melhor abordagem para o seu próximo projeto.

O Problema

Imagine o seguinte cenário, um usuário passa o dia cadastrando um projeto complexo. Por um erro de clique ou confusão na UI, ele deleta o registro. O suporte é acionado, mas o backup é semanal. O dado sumiu. Isso acontece porque, por padrão, usamos o Hard Delete (DELETE FROM table). O registro é removido fisicamente do disco e não há caminho de volta sem um snapshot do banco.

Aqui esta um exemplo de Hard Delete:

Hard Delete

Exemplo de hard delete em uma tabela de usuários

Step

Selecione alguma linha

id
name
email
11Alice Silvaalice@company.com
22Roberto Carlosroberto@company.com
33Julia Robertsjulia@company.com

SQL Console

readonly
[2026-04-01 00:21:04]db.public>SELECT * FROM users
[2026-04-01 00:21:04]db.public>3 rows retrieved in 1301 ms (execution: 11 ms, fetching: 1290 ms)

O Hard Delete é perigoso para tabelas críticas, mas é a abordagem ideal (e nativa) para dados irrelevantes, registros temporários ou cadastros rápidos que não impactam o negócio.

A Solução

O Soft Delete trata a exclusão como um estado lógico, não físico. Para o usuário, o dado sumiu, mas para o banco ele apenas foi marcado como oculto. Existem duas principais formas de se implementar isso, que são o que chamamos de Soft Delete Patterns.

Logical Delete

É a abordagem mais simples e comum. Consiste em adicionar uma coluna na própria tabela para sinalizar o estado do registro.

Aqui esta uma forma de fazer Logical Delete:

Logical Delete

Exemplo de soft delete em uma tabela de usuários

Step

Selecione alguma linha

id
name
email
archived_at
11Alice Silvaalice@company.comnull
22Roberto Carlosroberto@company.comnull
33Julia Robertsjulia@company.comnull

SQL Console

readonly
[2026-04-01 00:21:04]db.public>SELECT * FROM users
[2026-04-01 00:21:04]db.public>3 rows retrieved in 1301 ms (execution: 11 ms, fetching: 1290 ms)

Para implementar o Logical Delete, basta adicionar uma nova coluna em sua tabela:

How to implement Logical Delete

readonly
ALTER TABLE users
ADD COLUMN archived_at TIMESTAMPTZ DEFAULT NULL;

Esse campo pode ser um booleano ou um timestamp. O importante é que ele indique claramente se o registro está ativo ou deletado.

O problema surge em larga escala. Milhões de registros deletados continuam ocupando espaço e sujando seus índices na tabela principal.

Aqui esta um desafio comum do Logical Delete:

The Zombie Table

Exemplo do problema de tabela poluída com soft delete

Step

Faça um select para exibir todos os usuários ativos

id
name
email
archived_at
11Alice Silvaalice@company.com2026-02-19T10:22:32.123Z
22Roberto Carlosroberto@company.comnull
33Julia Robertsjulia@company.com2026-02-05T17:51:12.332Z
1-3 of 1000000

SQL Console

readonly
[2026-04-01 00:21:04]db.public>SELECT * FROM users
[2026-04-01 00:21:04]db.public>1000000 rows retrieved in 12092 ms (execution: 1092 ms, fetching: 11000 ms)

Isso pode degradar a performance de queries simples, já que o banco precisa filtrar dados mortos constantemente.

Shadow Table

Para resolver o inchaço da tabela principal, subimos o nível de complexidade com o Shadow Table. Aqui, em vez de marcar o registro, nós o movemos para uma tabela secundária de arquivo.

Aqui esta uma forma de fazer Shadow Table:

Shadow Table Pattern

Exemplo do padrão shadow table para recuperação de dados

Step

Selecione alguma linha

id
name
email
11Alice Silvaalice@company.com
22Roberto Carlosroberto@company.com
33Julia Robertsjulia@company.com
id
table_name
record_id
data
archived_at
No rows selected

SQL Console

readonly
[2026-04-01 00:21:04]db.public>SELECT * FROM users
[2026-04-01 00:21:04]db.public>3 rows retrieved in 1301 ms (execution: 11 ms, fetching: 1290 ms)

Para implementar o Shadow Table, a complexidade aumenta, primeiro devemos criar a seguinte função em nosso banco de dados:

How to implement Shadow Table

readonly
CREATE OR REPLACE FUNCTION fn_shadow_archive()
RETURNS TRIGGER AS $$
DECLARE
v_cause_table TEXT;
v_cause_id_text TEXT;
BEGIN
v_cause_table := current_setting('app.current_deleter_table', true);
v_cause_id_text := current_setting('app.current_deleter_id', true);

IF (v_cause_table IS NULL OR v_cause_table = '') THEN
v_cause_table := TG_TABLE_NAME;
v_cause_id_text := OLD.id::text;

PERFORM set_config('app.current_deleter_table', v_cause_table, true);
PERFORM set_config('app.current_deleter_id', v_cause_id_text, true);
END IF;

INSERT INTO archives (
table_name,
record_id,
data,
caused_by_table,
caused_by_id
)
VALUES (
TG_TABLE_NAME,
jsonb_build_object('id', OLD.id),
to_jsonb(OLD),
v_cause_table,
jsonb_build_object('id', v_cause_id_text)
);

RETURN OLD;
END;
$$ LANGUAGE plpgsql;

Essa função é responsável por pegar o registro deletado e inserir ele na tabela archives.

Agora basta criarmos os triggers para as tabelas críticas. Assim, elas ficam protegidas contra Hard Delete. No exemplo abaixo, o padrão de Shadow Table será aplicado em três tabelas: users, establishments e schedulings.

How to create Shadow Table Triggers

readonly
CREATE TRIGGER trg_archive_users BEFORE DELETE ON users FOR EACH ROW EXECUTE FUNCTION fn_shadow_archive();
CREATE TRIGGER trg_archive_establishments BEFORE DELETE ON establishments FOR EACH ROW EXECUTE FUNCTION fn_shadow_archive();
CREATE TRIGGER trg_archive_schedulings BEFORE DELETE ON schedulings FOR EACH ROW EXECUTE FUNCTION fn_shadow_archive();

Nada é de graça. Enquanto o Logical Delete exige apenas um UPDATE, restaurar dados em uma Shadow Table exige queries de INSERT INTO e SELECT mais complexas, especialmente se houver relacionamentos de chaves estrangeiras envolvidos.

Aqui esta um desafio comum da Shadow Table:

Restauring Data

Exemplo de restauração de dados arquivados

id
table_name
record_id
data
archived_at
caused_by_table
caused_by_id
12a8f5d9c-5f74-4d0f-8f61-1f45e6d0a101establishments{"id":"est-500"}{"id":"est-500","name":"Barbearia Central","owner_id":"user-owner-1","created_at":"2026-01-10T10:00:00.000Z"}2026-02-23T10:00:00.000Zestablishments{"id":"est-500"}
29d0f7b6a-2f43-4b23-a6df-3a4db44cf102schedulings{"user_id":"user-200","establishment_id":"est-500","scheduled_at":"2026-03-01T09:00:00.000Z"}{"user_id":"user-200","establishment_id":"est-500","scheduled_at":"2026-03-01T09:00:00.000Z","service":"Corte","status":"CONFIRMED"}2026-02-23T10:00:00.050Zestablishments{"id":"est-500"}
37bb2a10e-61b4-46e9-9f83-58d39f2aa103schedulings{"user_id":"user-201","establishment_id":"est-500","scheduled_at":"2026-03-01T10:00:00.000Z"}{"user_id":"user-201","establishment_id":"est-500","scheduled_at":"2026-03-01T10:00:00.000Z","service":"Barba","status":"PENDING"}2026-02-23T10:00:00.080Zestablishments{"id":"est-500"}
445d98f8a-8cf9-4f35-8f69-c9ec3f5b4104users{"id":"user-300"}{"id":"user-300","name":"Fernanda Lima","email":"fernanda@email.com","role":"CUSTOMER","created_at":"2026-01-15T12:00:00.000Z"}2026-02-23T11:00:00.000Zusers{"id":"user-300"}
5c0f57e2b-2d9f-4d8c-8f2e-98f703a6b105schedulings{"user_id":"user-300","establishment_id":"est-800","scheduled_at":"2026-03-05T14:00:00.000Z"}{"user_id":"user-300","establishment_id":"est-800","scheduled_at":"2026-03-05T14:00:00.000Z","service":"Manicure","status":"CONFIRMED"}2026-02-23T11:00:00.040Zusers{"id":"user-300"}
6ad3bcae9-7c31-4ceb-a20d-e4f952107106schedulings{"user_id":"user-300","establishment_id":"est-801","scheduled_at":"2026-03-06T16:00:00.000Z"}{"user_id":"user-300","establishment_id":"est-801","scheduled_at":"2026-03-06T16:00:00.000Z","service":"Pedicure","status":"PENDING"}2026-02-23T11:00:00.070Zusers{"id":"user-300"}
7f6a1db8e-34a9-4d3d-b7f3-27a8c1de7107schedulings{"user_id":"user-555","establishment_id":"est-999","scheduled_at":"2026-03-10T18:00:00.000Z"}{"user_id":"user-555","establishment_id":"est-999","scheduled_at":"2026-03-10T18:00:00.000Z","service":"Massagem","status":"CANCELLED_BY_USER"}2026-02-23T12:00:00.000Zschedulings{"user_id":"user-555","establishment_id":"est-999","scheduled_at":"2026-03-10T18:00:00.000Z"}
1-7 of 142

Mateus (mateus@gmail.com) acionou o suporte informando que apagou por engano o estabelecimento 'Barbearia X' e precisa recuperar o registro.

A cliente Fernanda Lima solicitou a recuperação da conta removida acidentalmente durante uma limpeza manual de dados.

O suporte recebeu pedido para restaurar um agendamento cancelado por engano, mantendo o histórico para auditoria.

SQL Console

readonly
[2026-04-01 00:21:04]db.public>SELECT * FROM archived LIMIT 7;
[2026-04-01 00:21:04]db.public>7 rows retrieved in 1432 ms (execution: 12 ms, fetching: 1420 ms)

Conclusão: Qual escolher?

Na prática, a melhor estratégia de exclusão depende muito do momento do produto e do nível de maturidade da arquitetura. Não existe resposta única: existe escolha consciente por contexto.

Para POCs e fases iniciais, o Logical Delete costuma fazer mais sentido. Ele entrega segurança para recuperar dados com implementação rápida, baixo atrito e sem elevar cedo demais a complexidade do projeto.

Para MVPs robustos, pilotos e produto final em produção, o Shadow Table pode ser bem mais vantajoso. Apesar do custo maior de implementação, ele ajuda a manter a tabela principal mais limpa, melhora previsibilidade de performance e organiza melhor a estratégia de arquivamento.

Existem outros patterns de soft delete além dos citados aqui. Neste artigo, eu trouxe os mais conhecidos e os que já usei na prática, justamente para compartilhar decisões que testei em cenários reais.