Soft deletes em produção: armadilhas e como evitá-las corretamente
1. O que são soft deletes e por que são tão populares?
Soft delete é uma técnica onde registros não são fisicamente removidos do banco de dados, mas marcados como "deletados" através de uma coluna especial. A implementação mais comum utiliza um campo deleted_at (timestamp nulo quando ativo, preenchido quando deletado) ou uma flag booleana is_deleted.
-- Exemplo de tabela com soft delete
CREATE TABLE usuarios (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
nome VARCHAR(100) NOT NULL,
deleted_at TIMESTAMP DEFAULT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Query típica para buscar apenas registros ativos
SELECT * FROM usuarios WHERE deleted_at IS NULL;
A popularidade dos soft deletes vem de promessas atraentes: recuperação de dados após exclusão acidental, auditoria de histórico, e a possibilidade de "desfazer" ações do usuário. Muitos desenvolvedores acreditam que estão implementando uma rede de segurança contra erros operacionais.
2. Armadilha #1: Violação de unicidade e integridade referencial
O problema mais comum surge com restrições de unicidade. Quando um usuário é "deletado" via soft delete, seu email permanece na tabela, impedindo que um novo usuário se cadastre com o mesmo email.
-- Tentativa frustrada de recadastro
INSERT INTO usuarios (email, nome)
VALUES ('joao@email.com', 'João Silva');
-- ERRO: violação de chave única, email já existe (mesmo que deletado)
Solução: Índices únicos parciais (partial unique indexes) resolvem esse problema:
-- PostgreSQL: índice único apenas para registros ativos
CREATE UNIQUE INDEX idx_usuarios_email_ativo
ON usuarios (email)
WHERE deleted_at IS NULL;
Para chaves estrangeiras, o problema é ainda mais grave. Um pedido pode referenciar um produto que foi "deletado", criando inconsistências em relatórios e cálculos.
-- Produto deletado, mas ainda referenciado em pedidos
SELECT p.nome, COUNT(pp.id) as total_pedidos
FROM produtos p
JOIN pedidos_produtos pp ON pp.produto_id = p.id
WHERE p.deleted_at IS NULL -- Produto deletado não aparece
GROUP BY p.nome;
-- Resultado: pedidos com produto "fantasma"
3. Armadilha #2: Degradação de performance e explosão de dados
Com o tempo, tabelas com soft delete acumulam milhões de registros "mortos". Cada query precisa filtrar WHERE deleted_at IS NULL, e índices ficam inchados com dados irrelevantes.
-- Tabela com 10 milhões de registros, 60% deletados
EXPLAIN ANALYZE
SELECT * FROM transacoes
WHERE deleted_at IS NULL
AND data BETWEEN '2024-01-01' AND '2024-12-31';
-- Tempo: 12.3 segundos (scan em 10M registros)
O problema se agrava em JOINs com múltiplas tabelas que também usam soft delete:
-- Query complexa com múltiplos filtros de soft delete
SELECT u.nome, t.valor, p.descricao
FROM usuarios u
JOIN transacoes t ON t.usuario_id = u.id AND t.deleted_at IS NULL
JOIN produtos p ON p.id = t.produto_id AND p.deleted_at IS NULL
WHERE u.deleted_at IS NULL
AND t.data > '2024-06-01';
Estratégias de mitigação:
- Tabelas de arquivo morto: Mover registros deletados para tabelas separadas periodicamente
- TTL (Time-To-Live): Definir prazo máximo para registros deletados antes da exclusão física
- Particionamento: Particionar por data e descartar partições antigas
-- Job de migração para archive
INSERT INTO transacoes_archive (id, usuario_id, valor, data, deleted_at)
SELECT id, usuario_id, valor, data, deleted_at
FROM transacoes
WHERE deleted_at IS NOT NULL
AND deleted_at < NOW() - INTERVAL '90 days';
DELETE FROM transacoes
WHERE deleted_at IS NOT NULL
AND deleted_at < NOW() - INTERVAL '90 days';
4. Armadilha #3: Vazamento de dados e inconsistências em soft deletes
Soft deletes criam uma falsa sensação de segurança. Dados "deletados" podem vazar em APIs, exports e relatórios se o filtro não for aplicado consistentemente.
-- Controller que esquece o filtro de soft delete
GET /api/usuarios
-- Retorna: [{"id": 5, "email": "excluido@email.com", "deleted_at": "2024-01-15"}]
-- Vazamento de dados de usuário deletado!
ORMs como Rails com default_scope podem criar problemas ainda maiores:
# Rails: default_scope perigoso
class Usuario < ApplicationRecord
default_scope { where(deleted_at: nil) }
end
# Problema: queries sem escopo podem retornar dados deletados
Usuario.unscoped.where(email: 'excluido@email.com').first
# Retorna o registro deletado!
Boas práticas:
- Camada de repositório: Centralizar toda lógica de acesso a dados
- Testes de integração: Verificar que dados deletados nunca vazam
- Validação em APIs: Middleware que garante filtros de soft delete
-- Middleware de segurança para APIs
CREATE OR REPLACE FUNCTION check_soft_delete_filter()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'SELECT' AND NEW.deleted_at IS NOT NULL THEN
RAISE EXCEPTION 'Acesso a registro deletado bloqueado';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
5. Quando soft deletes realmente fazem sentido (e quando não)
Casos legítimos:
- Sistemas de auditoria financeira (retenção legal de 5 anos)
- Compliance com LGPD/GDPR (direito ao esquecimento com retenção controlada)
- Funcionalidades de "lixeira" com prazo para recuperação
Quando evitar:
- Sistemas com alto throughput (milhares de exclusões por minuto)
- Tabelas que crescem mais de 100GB/mês
- Requisitos de performance abaixo de 10ms por query
Alternativas recomendadas:
1. Hard delete + log de eventos: Excluir fisicamente e registrar em tabela de auditoria
2. Tabela separada de deletados: Mover para usuarios_deleted com estrutura idêntica
3. Exclusão lógica com TTL: Soft delete com expiração automática
6. Implementação correta: padrões e ferramentas que funcionam
PostgreSQL Row-Level Security (RLS):
-- Política RLS para soft delete automático
CREATE POLICY soft_delete_policy ON usuarios
FOR ALL
USING (deleted_at IS NULL)
WITH CHECK (true);
ALTER TABLE usuarios ENABLE ROW LEVEL SECURITY;
ORM com escopo global (Eloquent/Prisma):
// Prisma: middleware global para soft delete
prisma.$use(async (params, next) => {
if (params.model === 'Usuario' && params.action === 'findMany') {
params.args.where = { ...params.args.where, deleted_at: null };
}
return next(params);
});
Auditoria completa com deleted_by:
-- Tabela com auditoria de soft delete
CREATE TABLE usuarios (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
deleted_at TIMESTAMP DEFAULT NULL,
deleted_by INTEGER REFERENCES usuarios(id),
deleted_reason VARCHAR(500),
restored_at TIMESTAMP DEFAULT NULL,
restored_by INTEGER REFERENCES usuarios(id)
);
7. Monitoramento e manutenção contínua
Dashboard de métricas:
-- Query para monitoramento
SELECT
relname as tabela,
n_live_tup as registros_ativos,
n_dead_tup as registros_deletados,
round(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 2) as percentual_morto
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY percentual_morto DESC;
Jobs de manutenção:
-- Job semanal de vacuum e purge
VACUUM ANALYZE usuarios;
VACUUM ANALYZE transacoes;
-- Hard delete em lote (fora do horário comercial)
DELETE FROM usuarios
WHERE deleted_at IS NOT NULL
AND deleted_at < NOW() - INTERVAL '180 days'
LIMIT 10000;
Política de retenção: Defina claramente:
- 30 dias: soft delete com possibilidade de restore
- 90 dias: soft delete sem restore (apenas auditoria)
- 180 dias: hard delete definitivo
Referências
- PostgreSQL Documentation: Partial Indexes — Documentação oficial sobre índices únicos parciais, essenciais para evitar violações de unicidade em soft deletes
- Rails Guide: Soft Delete with Paranoia Gem — Biblioteca Rails para soft delete com escopos globais e boas práticas de implementação
- Prisma Documentation: Soft Delete Middleware — Tutorial oficial de como implementar soft delete com middleware no Prisma ORM
- PostgreSQL Row-Level Security — Guia completo sobre RLS para aplicar filtros automáticos de soft delete no banco
- Martin Fowler: Soft Delete Pattern — Artigo clássico sobre o padrão de soft delete, vantagens e desvantagens arquiteturais
- Database Performance: Dealing with Soft Deletes — Análise de performance sobre o impacto de soft deletes em bancos de dados grandes
- Clean Architecture: Repository Pattern — Fundamentos de camada de repositório para garantir consistência em operações de soft delete