Privacidade Por Equipe DevBras

Capa do artigo: Prazo de retenção de dados: tabela de prazos e um job que apaga de verdade

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

  1. 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.
  2. Escreva a tabela de prazos e publique onde o time encontra, junto do código.
  3. Garanta um índice na coluna do gatilho, como criado_em. Sem índice, cada limpeza varre a tabela inteira.
  4. Implemente a limpeza em lotes pequenos, para não travar a tabela nem inchar o log de transações.
  5. Registre cada execução: tabela, quantidade apagada, duração e erro. Nunca o conteúdo apagado.
  6. Agende e alerte. Job que falha em silêncio por três meses é igual a não ter job.
  7. 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 DELETE gigante sem LIMIT. 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?

Avalie este artigo

Referência: https://www.gov.br/anpd/pt-br