Manipulando usuários excluídos - tabela separada ou mesma?

19

O cenário é que eu tenho um conjunto crescente de usuários e, com o passar do tempo, os usuários cancelam suas contas que atualmente marcamos como 'excluídas' (com um sinalizador) na mesma tabela.

Se usuários com o mesmo endereço de e-mail (é assim que os usuários fazem login) desejam criar uma nova conta, eles podem se inscrever novamente, mas uma NOVA conta é criada. (Como temos identificações únicas para cada conta, os endereços de e-mail podem ser duplicados entre os ativos e os excluídos).

O que eu notei é que em todo o sistema, no curso normal das coisas, consultamos constantemente a tabela de usuários, verificando se o usuário não é excluído, enquanto o que estou pensando é que não precisamos fazer isso ... ! [Esclarecimento1: ao 'consultar constantemente', quis dizer que temos consultas como: '... FROM users WHERE isdeleted = "0" AND ...'. Por exemplo, talvez seja necessário buscar todos os usuários registrados para todas as reuniões em uma data específica; portanto, nessa consulta, também temos FROM usuários WHERE isdeleted = "0" - isso torna meu argumento mais claro?]

(1) continue keeping deleted users in the 'main' users table
(2) keep deleted users in a separate table (mostly required for historical
    book-keeping)

Quais são os prós e os contras de qualquer uma das abordagens?

Alan Beats
fonte
Por que razões você mantém os usuários?
keppla
2
Isso é chamado de exclusão reversa. Veja também Exclusão de registros do banco de dados unpermenantley (soft-delete)
Sjoerd
@keppla - ele menciona que: "contabilidade histórica".
ChrisF
@ ChrisF: Eu estava interessado no escopo: Será que ele quer manter livros de apenas os usuários, ou há ainda alguns dados anexados (comentários exemplo, pagamentos, etc.)
keppla
Pode ajudar a parar de pensar neles como excluídos (o que não é verdade) e começar a pensar na conta deles como cancelada (o que é verdade).
Mike Sherrill 'Cat Recall'

Respostas:

13

(1) continue mantendo os usuários excluídos na tabela de usuários 'principais'

  • Prós: consultas mais simples em todos os casos
  • Contras: podem prejudicar o desempenho ao longo do tempo, se houver um número alto de usuários

(2) mantenha os usuários excluídos em uma tabela separada (principalmente necessária para a contabilidade histórica)

Você pode usar, por exemplo, um gatilho para mover automaticamente os usuários excluídos para a tabela de histórico.

  • Prós: manutenção mais simples para a tabela de usuários ativos, desempenho estável
  • Contras: precisam de consultas diferentes para a tabela de histórico; no entanto, como a maioria do aplicativo não deveria estar interessada nisso, esse efeito negativo provavelmente é limitado
Péter Török
fonte
11
Uma tabela de partição (em IsDeleted) removeria os problemas de desempenho ao usar uma única tabela.
21711 Ian
1
@Ian, a menos que toda consulta seja fornecida com IsDeleted como critério de consulta (o que parece não estar na pergunta original), o particionamento pode até causar degradação no desempenho.
Adrian Shum
1
@Adrian, eu estava assumindo que as consultas mais comum seria na hora do login e que só nenhum excluídos os usuários seriam autorizados a entrar.
Ian
1
Use uma exibição indexada em isdeleted se isso se tornar um problema de desempenho e você desejar o benefício de uma única tabela.
22411 JeffO:
10

Eu recomendo usar a mesma tabela. O principal motivo é a integridade dos dados. Provavelmente, haverá muitas tabelas com relacionamentos, dependendo dos usuários. Quando um usuário é excluído, você não deseja deixar esses registros órfãos.
Ter registros órfãos dificulta a aplicação de restrições e torna mais difícil a busca de informações históricas. O outro comportamento a considerar se, quando um usuário fornecer um email usado, se você desejar recuperar todos os registros antigos. Isso funcionaria automaticamente usando a exclusão suave. Quanto à codificação, por exemplo, no meu aplicativo c # linq atual, a cláusula where delete = 0 é automaticamente anexada ao final de todas as consultas

Andrey
fonte
7

"O que eu notei é que, em todo o sistema, no curso normal das coisas, consultamos constantemente a tabela de usuários, verificando se o usuário não é excluído"

Isso me dá um cheiro ruim de design. Você deve esconder esse tipo de lógica. Por exemplo, você deve UserServicefornecer um método isValidUser(userId)para uso "em todo o sistema", em vez de fazer algo como:

"obtém o registro do usuário, verifique se o usuário está marcado como excluído".

Sua maneira de armazenar usuários excluídos não deve afetar a lógica de negócios.

Com esse tipo de encapsulamento, o argumento acima não deve mais afetar a abordagem de sua persistência. Então você pode se concentrar mais nos prós e contras relacionados à própria persistência.

As coisas a considerar incluem:

  • Por quanto tempo o registro excluído deve ser realmente eliminado?
  • Qual é a proporção de registros excluídos?
  • Haverá um problema de integridade referencial (por exemplo, o usuário é encaminhado de outra tabela) se você realmente a remover da tabela?
  • Você considera reabrir o usuário?

Normalmente, eu adotaria uma maneira combinada:

  1. Sinalize o registro como excluído (para mantê-lo como requisito funcional, como reabrir a CA ou verificar a CA recentemente fechada).
  2. Após um período predefinido, mova o registro excluído para a tabela de arquivamento (para fins de contabilidade).
  3. Limpe-o após um período predefinido de arquivamento.
Adrian Shum
fonte
1
[Esclarecimento1: ao 'consultar constantemente', quis dizer que temos consultas como: '... FROM users WHERE isdeleted = "0" AND ...'. Por exemplo, talvez seja necessário buscar todos os usuários registrados para todas as reuniões em uma data específica; portanto, nessa consulta, também temos FROM usuários WHERE isdeleted = "0" - isso torna meu argumento mais claro?] @Adrian
Alan Beats
Sim, muito mais claro. :) Se eu estiver fazendo isso, prefiro fazê-lo como alteração de status do usuário, em vez de considerá-lo como exclusão física / lógica. Embora a quantidade de código não seja reduzida ("e isDeleted = '0'" vs 'e "state <>' TERMINATED '"), mas tudo parecerá muito mais razoável, e é normal também ter um estado de usuário diferente. Periódica-expurgo de usuários cancelados pode ser realizada também, como sugerido na minha resposta anterior)
Adrian Shum
5

Para responder adequadamente a essa pergunta, primeiro você precisa decidir: O que "excluir" significa no contexto deste sistema / aplicativo?

Para responder a essa pergunta, você precisa responder a outra pergunta: Por que os registros estão sendo excluídos?

Existem várias boas razões pelas quais um usuário pode precisar excluir dados. Normalmente, acho que há exatamente uma razão (por tabela) para a exclusão de uma exclusão. Alguns exemplos são:

  • Recuperar espaço em disco;
  • É necessária a exclusão definitiva de acordo com a política de retenção / privacidade;
  • Dados corrompidos / irremediavelmente incorretos, mais fáceis de excluir e regenerar do que reparar.
  • A maioria das linhas é excluída, por exemplo, uma tabela de log limitada a X registros / dias.

Há também algumas razões muito ruins para exclusão completa (mais sobre as razões para isso posteriormente):

  • Para corrigir um erro menor. Isso geralmente ressalta a preguiça do desenvolvedor e uma interface host.
  • Anular uma transação (por exemplo, fatura que nunca deveria ter sido faturada).
  • Porque você pode .

Por que, você pergunta, é realmente tão importante? O que há de errado com o bom e velho DELETE?

  • Em qualquer sistema que esteja remotamente vinculado ao dinheiro, a exclusão definitiva viola todos os tipos de expectativas contábeis, mesmo se movida para uma tabela de arquivamento / marca para exclusão. A maneira correta de lidar com isso é um evento retroativo .
  • As tabelas de arquivamento tendem a divergir do esquema ativo. Se você esquecer apenas uma coluna ou cascata recém-adicionada, você perderá esses dados permanentemente.
  • A exclusão definitiva pode ser uma operação muito cara, especialmente com cascatas . Muitas pessoas não percebem que a cascata mais de um nível (ou em alguns casos, qualquer cascata, dependendo DBMS) irá resultar em operações de nível recorde em vez de operações de conjunto.
  • A exclusão intensa e repetida acelera o processo de fragmentação do índice.

Então, a exclusão suave é melhor, certo? Não, na verdade não:

  • Configurar cascatas se torna extremamente difícil. Você quase sempre acaba com o que aparece para o cliente como linhas órfãs.
  • Você só consegue rastrear uma exclusão. E se a linha for excluída e desmarcada várias vezes?
  • O desempenho da leitura sofre, embora isso possa ser atenuado de alguma forma com particionamento, visualizações e / ou índices filtrados.
  • Como sugerido anteriormente, pode ser ilegal em alguns cenários / jurisdições.

A verdade é que ambas as abordagens estão erradas. A exclusão está errada. Se você está realmente fazendo essa pergunta, significa que está modelando o estado atual em vez das transações. Essa é uma prática ruim e ruim na área de banco de dados.

Udi Dahan escreveu sobre isso em Não Excluir - Apenas Não . Há sempre algum tipo de tarefa, operação, atividade , ou (o meu preferido prazo) evento que realmente representa o "delete". Tudo bem se você desejar desnormalizar posteriormente em uma tabela de "estado atual" para desempenho, mas faça isso depois de definir o modelo transacional, não antes.

Nesse caso, você tem "usuários". Usuários são essencialmente clientes. Os clientes têm um relacionamento comercial com você. Esse relacionamento não simplesmente desaparece no ar porque eles cancelaram sua conta. O que realmente está acontecendo é:

  • Cliente cria conta
  • O cliente cancela a conta
  • Cliente renova conta
  • O cliente cancela a conta
  • ...

Em todos os casos, é o mesmo cliente e, possivelmente, a mesma conta (ou seja, cada renovação de conta é um novo contrato de serviço). Então, por que você está excluindo linhas? Isso é muito fácil de modelar:

+-----------+       +-------------+       +-----------------+
| Account   | --->* | Agreement   | --->* | AgreementStatus |
+-----------+       +-------------+       +----------------+
| Id        |       | Id          |       | AgreementId     |
| Name      |       | AccountId   |       | EffectiveDate   |
| Email     |       | ...         |       | StatusCode      |
+-----------+       +-------------+       +-----------------+

É isso aí. É tudo o que há para isso. Você nunca precisa excluir nada. O acima é um design bastante comum que acomoda um bom grau de flexibilidade, mas você pode simplificá-lo um pouco; você pode decidir que não precisa do nível "Contrato" e fazer com que "Conta" vá para uma tabela "Status da conta".

Se uma necessidade frequente em seu aplicativo é obter uma lista de contratos / contas ativos , é uma consulta (um pouco) complicada, mas é para isso que servem as visualizações:

CREATE VIEW ActiveAgreements AS
SELECT agg.Id, agg.AccountId, acc.Name, acc.Email, s.EffectiveDate, ...
FROM AgreementStatus s
INNER JOIN Agreement agg
    ON agg.Id = s.AgreementId
INNER JOIN Account acc
    ON acc.Id = agg.AccountId
WHERE s.StatusCode = 'ACTIVE'
AND NOT EXISTS
(
    SELECT 1
    FROM AgreementStatus so
    WHERE so.AgreementId = s.AgreementId
    AND so.EffectiveDate > s.EffectiveDate
)

E você terminou. Agora você tem algo com todos os benefícios das exclusões eletrônicas, mas nenhuma das desvantagens:

  • Os registros órfãos não são um problema porque todos os registros são visíveis o tempo todo; basta selecionar a partir de uma visualização diferente sempre que necessário.
  • "Excluir" geralmente é uma operação incrivelmente barata - basta inserir uma linha em uma tabela de eventos.
  • Nunca há qualquer chance de perder qualquer história, sempre , não importa o quanto você estragar.
  • Você ainda pode excluir uma conta manualmente, se precisar (por exemplo, por motivos de privacidade), e ficar à vontade com o conhecimento de que a exclusão ocorrerá de forma limpa e não interferirá em nenhuma outra parte do aplicativo / banco de dados.

O único problema a ser resolvido é o problema de desempenho. Em muitos casos, na verdade, não é um problema por causa do índice clusterizado AgreementStatus (AgreementId, EffectiveDate)- há muito pouca procura de E / S lá. Mas, se houver algum problema, há maneiras de resolver isso, usando gatilhos, visualizações indexadas / materializadas, eventos no nível do aplicativo, etc.

Porém, não se preocupe com o desempenho muito cedo - é mais importante acertar o design e, "nesse caso", significa usar o banco de dados da maneira que um banco de dados deve ser usado, como um sistema transacional .

Aaronaught
fonte
1

Atualmente, estou trabalhando com um sistema no qual todas as tabelas possuem um sinalizador Excluído para exclusão reversa. É a desgraça de toda a existência. Ele quebra totalmente a integridade relacional quando um usuário pode "excluir" um registro de uma tabela, mas os registros filhos que FK retornam a essa tabela não são excluídos em cascata. Realmente cria dados de lixo após o tempo passar.

Portanto, recomendo tabelas de histórico separadas.

Jesse C. Slicer
fonte
Certamente, sem mudanças históricas em cascata, você tem exatamente o mesmo problema?
glenatron
Não está nas suas tabelas de registros ativos, não.
Jesse C. Slicer
Então, o que acontece com os registros filhos que saem da tabela do usuário após o envio do usuário à tabela de histórico?
glenatron
Seu gatilho (ou lógica de negócios) também consignaria os registros filhos nas respectivas tabelas de histórico. O ponto é que você não pode excluir fisicamente o registro pai (para passar para o histórico) sem o banco de dados informando que você quebrou o RI. Portanto, você é forçado a projetá-lo. O sinalizador excluído não força exclusões em cascata.
Jesse C. Slicer
3
Depende do que sua exclusão virtual realmente significa. Se é apenas uma maneira de desativá-los, não há necessidade de ajustar registros relacionados a uma conta desativada. Parece apenas dados para mim. E sim, eu tenho que lidar com isso também em um sistema que não projetei. Não significa que você precisa gostar.
21411 JeffO
1

Quebrar a mesa ao meio seria a coisa mais lamentável que se possa imaginar.

Aqui estão as duas etapas muito simples que eu recomendaria:

  1. Renomeie a tabela 'users' para 'allusers'.
  2. Crie uma visualização chamada 'usuários' como 'selecione * de todos os usuários onde delete = false'.

PS Desculpe pelo atraso de vários meses em responder!

Mike Nakis
fonte
0

Se você estivesse recuperando contas excluídas quando alguém voltasse com o mesmo endereço de e-mail, eu teria mantido todos os usuários na mesma tabela. Isso tornaria o processo de recuperação da conta trivial.

No entanto, à medida que você cria novas contas, provavelmente seria mais simples mover as contas excluídas para uma tabela separada. O sistema ativo não precisa dessas informações, portanto, não as exponha. Como você diz, as consultas são mais simples e possivelmente mais rápidas em conjuntos de dados maiores. Código mais simples também é mais fácil de manter.

ChrisF
fonte
0

Você não menciona o DBMS em uso. Se você possui o Oracle com licença adequada, considere particionar a tabela de usuários em duas partições: usuários ativos e excluídos.

mczajk
fonte
Em seguida, você deve mover linhas de uma partição para outra ao excluir usuários, o que definitivamente não é como as partições devem ser usadas.
Péter Török
@ Péter: Hein? Você pode particionar com qualquer critério que desejar, incluindo o sinalizador excluído.
Aaronaught
@Aaronaught, OK, eu expressei errado. O DBMS pode fazer o trabalho por você, mas ainda é um trabalho extra (porque a linha deve ser fisicamente movida de um local para outro, possivelmente para um arquivo diferente) e pode deteriorar a distribuição física dos dados.
Péter Török