Prazo de retenção de dados: tabela de prazos e um job que apaga de verdade
Monte uma tabela com prazo, motivo e dono para cada tipo de dado e implemente a limpeza automática em lotes no MySQL, com registro de cada execução.
A política de privacidade de uma plataforma de cursos diz "guardamos seus dados pelo tempo necessário". A tabela logs_acesso tem 410 milhões de linhas desde 2019, ocupa 180 GB e deixa o backup noturno com quatro horas de duração. Há contas que não entram há seis anos e anexos de alunos que cancelaram em 2020. Ninguém decidiu guardar tudo isso; ninguém decidiu apagar. A resposta curta: cada tipo de dado precisa de um prazo escrito, um motivo e um responsável, e de um job que apaga quando o prazo vence. "Enquanto for necessário" sem número vira "para sempre".
Prazo de retenção é, ao mesmo tempo, obrigação de privacidade e economia de infraestrutura. O dado que não existe não vaza, não ocupa disco e não deixa o backup lento.
Por que o dado fica para sempre
Apagar dá trabalho e guardar não. Criar uma tabela leva um minuto; definir quando as linhas dela deixam de ser úteis exige uma conversa com produto, jurídico e financeiro. Sem essa conversa, o padrão é guardar, e o padrão vence.
A LGPD prevê que os dados pessoais sejam eliminados quando termina o tratamento, com exceções, como o cumprimento de obrigação legal. Outras normas, como as fiscais e o Marco Civil da Internet, obrigam a guardar certos registros por prazos mínimos. O trabalho é separar o que a lei manda guardar, e por quanto tempo, do que seria "bom ter por via das dúvidas". Isto não substitui a orientação de um profissional habilitado: os prazos legais do seu caso devem vir do jurídico e do contador.
A tabela de prazos do exemplo
Para a plataforma de cursos, a tabela de prazos ficou assim. Os números são exemplos de uma decisão interna, não referência legal:
- Registros de acesso (
logs_acesso): 180 dias, prazo confirmado pelo jurídico para obrigação legal e investigação de incidentes. Dono: infraestrutura. - Sessões expiradas e links de redefinição usados: 7 dias depois de expirar. Dono: time de autenticação.
- Contas sem acesso há 3 anos e sem curso ativo: aviso por e-mail com 30 dias de antecedência e, sem resposta, exclusão. Dono: produto.
- Anexos de trabalhos: 1 ano depois do fim do curso. Dono: produto.
- Notas fiscais e dados de pagamento necessários a elas: pelo prazo que o contador indicar, desvinculados do perfil quando a conta for excluída. Dono: financeiro.
- Backups: 35 dias. Dono: infraestrutura.
Cada linha tem quatro coisas: o tipo de dado, o prazo, o gatilho (desde quando conta) e o dono. O gatilho é o que mais se esquece. "Um ano" a partir da criação, do último acesso ou do fim do contrato são regras completamente diferentes.
Como colocar a retenção para rodar
- Inventarie as tabelas e os depósitos de arquivos que guardam dado pessoal. Sem inventário, o prazo não tem onde ser aplicado; o post adequação à LGPD num sistema pequeno mostra por onde começar.
- Escreva a tabela de prazos e publique onde o time encontra, junto do código.
- Garanta um índice na coluna do gatilho, como
criado_em. Sem índice, cada limpeza varre a tabela inteira. - Implemente a limpeza em lotes pequenos, para não travar a tabela nem inchar o log de transações.
- Registre cada execução: tabela, quantidade apagada, duração e erro. Nunca o conteúdo apagado.
- Agende e alerte. Job que falha em silêncio por três meses é igual a não ter job.
- Revise a tabela de prazos uma vez por ano e sempre que surgir um tipo de dado novo.
O job de limpeza em Node.js com MySQL
const pool = require('./db'); // mysql2/promise
const REGRAS = [
{ tabela: 'logs_acesso', sql: 'DELETE FROM logs_acesso WHERE criado_em < NOW() - INTERVAL 180 DAY ORDER BY id LIMIT 5000' },
{ tabela: 'sessoes', sql: 'DELETE FROM sessoes WHERE expira_em < NOW() - INTERVAL 7 DAY ORDER BY id LIMIT 5000' },
{ tabela: 'redefinicoes_senha', sql: 'DELETE FROM redefinicoes_senha WHERE expira_em < NOW() - INTERVAL 7 DAY ORDER BY id LIMIT 5000' }
];
const pausa = (ms) => new Promise((r) => setTimeout(r, ms));
async function limpar(regra) {
const inicio = Date.now();
let total = 0;
let apagadas;
do {
const [r] = await pool.query(regra.sql);
apagadas = r.affectedRows;
total += apagadas;
await pausa(200);
} while (apagadas === 5000);
await pool.execute(
'INSERT INTO execucoes_retencao (tabela, apagadas, duracao_ms, executado_em) VALUES (?, ?, ?, NOW())',
[regra.tabela, total, Date.now() - inicio]
);
return total;
}
async function main() {
let falhou = false;
for (const regra of REGRAS) {
try {
const n = await limpar(regra);
console.log(regra.tabela, n);
} catch (err) {
falhou = true;
console.error('retencao falhou em', regra.tabela, err);
}
}
await pool.end();
process.exit(falhou ? 1 : 0);
}
main();
O DELETE ... LIMIT apaga no máximo 5 mil linhas por comando; o laço repete até sobrar menos que isso. A pausa curta entre lotes dá espaço para o tráfego normal e para a replicação acompanhar. Na primeira execução, numa tabela como a de 410 milhões de linhas, o job pode levar horas; rode num horário calmo e acompanhe. Depois disso, cada execução diária apaga só um dia de dados.
O código de saída diferente de zero quando alguma regra falha permite que o agendador, seja cron, um timer do systemd ou o agendador do seu orquestrador, dispare o alerta. A tabela execucoes_retencao é a prova de que a política roda, sem guardar nada do que foi apagado.
Contas inativas e arquivos
Conta inativa não se resolve com um DELETE simples. O job marca as candidatas, envia o aviso e, depois do prazo, chama a mesma função de exclusão usada quando a pessoa pede para sair, que também cuida das cópias derivadas. Anexos em armazenamento de objetos podem usar as regras de ciclo de vida do próprio serviço, quando ele oferece; se não, o job apaga o arquivo e a linha que aponta para ele na mesma rotina.
Lembre que os backups guardam o que o job apagou até expirarem. A retenção do backup entra na conta do prazo real, como explica exclusão que chega no backup.
Erros comuns
- "Enquanto for necessário" sem número. Ninguém consegue implementar, e o dado fica para sempre.
- Um
DELETEgigante semLIMIT. A tabela trava, o log de transações explode e a réplica fica horas atrasada. - Coluna de data sem índice. Cada execução varre a tabela inteira e o job passa a demorar mais que o intervalo entre execuções.
- Job sem alerta de falha. A limpeza para numa migração e ninguém percebe até o disco encher.
- Apagar a linha e esquecer o arquivo. O anexo continua no armazenamento, sem nada que aponte para ele.
- Misturar obrigação legal com conveniência. Guardar tudo "porque a lei manda" sem saber qual lei, e qual prazo.
Checklist
- Cada tabela e depósito de arquivos com dado pessoal tem prazo, gatilho, motivo e dono.
- Os prazos com base legal foram confirmados pelo jurídico ou pelo contador.
- As colunas usadas como gatilho têm índice.
- A limpeza roda em lotes, todos os dias, com alerta em caso de falha.
- Cada execução registra tabela, quantidade e duração, sem conteúdo.
- Contas inativas recebem aviso antes da exclusão.
- A retenção dos backups está escrita e compatível com o que a política promete.
Abra o seu banco agora e olhe a linha mais antiga de cada tabela: qual delas não tem ninguém responsável por apagar o que passou do prazo?
Referência: https://www.gov.br/anpd/pt-br