Temos um requisito no projeto para armazenar todas as revisões (histórico de alterações) das entidades no banco de dados. Atualmente, temos 2 propostas projetadas para isso:
por exemplo, para entidade "Empregado"
Projeto 1:
-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
-- Holds the Employee Revisions in Xml. The RevisionXML will contain
-- all data of that particular EmployeeId
"EmployeeHistories (EmployeeId, DateModified, RevisionXML)"
Projeto 2:
-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
-- In this approach we have basically duplicated all the fields on Employees
-- in the EmployeeHistories and storing the revision data.
"EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName,
LastName, DepartmentId, .., ..)"
Existe alguma outra maneira de fazer isso?
O problema com o "Design 1" é que precisamos analisar o XML sempre que precisar acessar dados. Isso retardará o processo e também adicionará algumas limitações, pois não podemos adicionar junções nos campos de dados das revisões.
E o problema com o "Design 2" é que precisamos duplicar todos os campos de todas as entidades (temos entre 70 e 80 entidades para as quais queremos manter as revisões).
sql
database
database-design
versioning
Ramesh Soni
fonte
fonte
Respostas:
fonte
SELECT * FROM EmployeeHistory WHERE LastName = 'Doe'
resultado simples em uma verificação de tabela completa . Não é a melhor idéia para dimensionar um aplicativo.Acho que a principal pergunta a fazer aqui é 'Quem / O que usará a história'?
Se for principalmente para relatórios / histórico legível por humanos, implementamos esse esquema no passado ...
Crie uma tabela chamada 'AuditTrail' ou algo que possua os seguintes campos ...
Você pode adicionar uma coluna 'LastUpdatedByUserID' a todas as suas tabelas, que devem ser definidas sempre que você fizer uma atualização / inserção na tabela.
Em seguida, você pode adicionar um gatilho a todas as tabelas para capturar qualquer inserção / atualização que ocorra e criar uma entrada nessa tabela para cada campo alterado. Como a tabela também está sendo fornecida com o 'LastUpdateByUserID' para cada atualização / inserção, você pode acessar esse valor no gatilho e usá-lo ao adicionar à tabela de auditoria.
Usamos o campo RecordID para armazenar o valor do campo-chave da tabela que está sendo atualizada. Se for uma chave combinada, fazemos apenas uma concatenação de string com um '~' entre os campos.
Tenho certeza de que esse sistema pode ter desvantagens - para bancos de dados altamente atualizados, o desempenho pode ser atingido, mas para meu aplicativo da web, temos muito mais leituras do que gravações e parece estar funcionando muito bem. Até escrevemos um pequeno utilitário VB.NET para gravar automaticamente os gatilhos com base nas definições da tabela.
Apenas um pensamento!
fonte
sysname
pode ser um tipo de dados mais adequado para os nomes de tabela e coluna.O artigo Tabelas do histórico no blog do programador de banco de dados pode ser útil - aborda alguns dos pontos levantados aqui e discute o armazenamento de deltas.
Editar
No ensaio Tabelas de História , o autor ( Kenneth Downs ) recomenda manter uma tabela de história de pelo menos sete colunas:
As colunas que nunca mudam, ou cujo histórico não é necessário, não devem ser rastreadas na tabela de histórico para evitar inchaço. Armazenar o delta para valores numéricos pode facilitar as consultas subseqüentes, mesmo que possam ser derivadas dos valores antigos e novos.
A tabela de histórico deve ser segura, com usuários que não são do sistema impedidos de inserir, atualizar ou excluir linhas. Somente a limpeza periódica deve ser suportada para reduzir o tamanho geral (e se permitido pelo caso de uso).
fonte
Implementamos uma solução muito parecida com a sugerida por Chris Roberts, e que funciona muito bem para nós.
A única diferença é que apenas armazenamos o novo valor. Afinal, o valor antigo é armazenado na linha do histórico anterior
Digamos que você tenha uma tabela com 20 colunas. Dessa forma, você só precisa armazenar a coluna exata que foi alterada, em vez de ter que armazenar a linha inteira.
fonte
Evite o design 1; não é muito útil, pois você precisará, por exemplo, reverter para versões antigas dos registros - automática ou "manualmente" usando o console de administradores.
Realmente não vejo desvantagens do Design 2. Acho que a segunda tabela Histórico deve conter todas as colunas presentes na primeira tabela Registros. Por exemplo, no mysql, você pode criar facilmente tabela com a mesma estrutura que outra tabela (
create table X like Y
). E, quando você estiver prestes a alterar a estrutura da tabela Registros em seu banco de dadosalter table
ativo , precisará usar os comandos de qualquer maneira - e não há grande esforço em executar esses comandos também na tabela Histórico.Notas
RevisionId
coluna adicionada ;ModifiedBy
- o usuário que criou uma revisão específica. Você também pode querer ter um campoDeletedBy
para rastrear quem excluiu uma revisão específica.DateModified
deveria significar - ou significa onde essa revisão específica foi criada ou quando esta revisão específica foi substituída por outra. O primeiro requer que o campo esteja na tabela Registros e parece ser mais intuitivo à primeira vista; a segunda solução, no entanto, parece ser mais prática para registros excluídos (data em que essa revisão específica foi excluída). Se você optar pela primeira solução, provavelmente precisará de um segundo campoDateDeleted
(somente se precisar, é claro). Depende de você e do que você realmente deseja gravar.As operações no Design 2 são muito triviais:
ModificarSe você optar pelo Design 2, todos os comandos SQL necessários para isso serão muito, muito fáceis, além de manutenção! Talvez seja muito mais fácil se você usar as colunas auxiliares (
RevisionId
,DateModified
) também na tabela Registros - para manter as duas tabelas exatamente na mesma estrutura (exceto chaves exclusivas)! Isso permitirá comandos SQL simples, que serão tolerantes a qualquer alteração na estrutura de dados:Não se esqueça de usar transações!
Quanto ao dimensionamento , esta solução é muito eficiente, já que você não transforma nenhum dado de XML, apenas copia linhas inteiras da tabela - consultas muito simples, usando índices - muito eficientes!
fonte
Se você precisar armazenar o histórico, crie uma tabela de sombra com o mesmo esquema da tabela que você está rastreando e as colunas 'Data da revisão' e 'Tipo de revisão' (por exemplo, 'excluir', 'atualizar'). Escreva (ou gere - veja abaixo) um conjunto de gatilhos para preencher a tabela de auditoria.
É bastante simples criar uma ferramenta que leia o dicionário de dados do sistema para uma tabela e gere um script que crie a tabela de sombra e um conjunto de gatilhos para preenchê-la.
Não tente usar XML para isso, o armazenamento XML é muito menos eficiente que o armazenamento da tabela de banco de dados nativo que esse tipo de gatilho usa.
fonte
Ramesh, eu estava envolvido no desenvolvimento de sistema baseado na primeira abordagem.
Aconteceu que o armazenamento de revisões como XML está levando a um enorme crescimento do banco de dados e a desacelerar significativamente as coisas.
Minha abordagem seria ter uma tabela por entidade:
onde IsActive é um sinal da versão mais recente
Se você deseja associar algumas informações adicionais a revisões, é possível criar uma tabela separada contendo essas informações e vinculá-las às tabelas de entidades usando a relação PK \ FK.
Dessa forma, você pode armazenar todas as versões dos funcionários em uma tabela. Prós desta abordagem:
Observe que você deve permitir que a chave primária não seja exclusiva.
fonte
A maneira que eu vi isso feito no passado é
Você nunca "atualiza" nesta tabela (exceto para alterar a validade de isCurrent), basta inserir novas linhas. Para qualquer EmployeeId, apenas 1 linha pode ter isCurrent == 1.
A complexidade de manter isso pode ser oculta por visualizações e "em vez de" gatilhos (no oracle, presumo coisas semelhantes a outros RDBMS), você pode até acessar visualizações materializadas se as tabelas forem muito grandes e não puderem ser manipuladas por índices) .
Esse método está ok, mas você pode acabar com algumas consultas complexas.
Pessoalmente, gosto bastante da sua maneira de fazê-lo do Design 2, que também foi como fiz no passado. É simples de entender, simples de implementar e simples de manter.
Ele também cria muito pouca sobrecarga para o banco de dados e o aplicativo, especialmente ao executar consultas de leitura, o que provavelmente é o que você fará 99% do tempo.
Também seria muito fácil automatizar a criação das tabelas de histórico e gatilhos para manter (supondo que isso fosse feito via gatilhos).
fonte
Revisões de dados são um aspecto do conceito de ' tempo válido ' de um banco de dados temporal. Muita pesquisa foi feita sobre isso, e muitos padrões e diretrizes surgiram. Escrevi uma longa resposta com várias referências a essa pergunta para os interessados.
fonte
Vou compartilhar com você meu design e é diferente dos dois designs, pois exige uma tabela por cada tipo de entidade. Encontrei a melhor maneira de descrever qualquer design de banco de dados através do ERD, aqui está o meu:
Neste exemplo, temos uma entidade chamada employee . A tabela user guarda os registros de seus usuários, e entity e entity_revision são duas tabelas que mantêm o histórico de revisões de todos os tipos de entidade que você terá em seu sistema. Veja como esse design funciona:
Os dois campos de entity_id e revision_id
Cada entidade em seu sistema terá um ID de entidade exclusivo. Sua entidade pode passar por revisões, mas seu entity_id permanecerá o mesmo. Você precisa manter esse ID de entidade na tabela de funcionários (como uma chave estrangeira). Você também deve armazenar o tipo de sua entidade na tabela de entidades (por exemplo, 'funcionário'). Agora, quanto ao revision_id, como o nome mostra, ele acompanha as revisões da sua entidade. A melhor maneira que encontrei para isso é usar o employee_id como sua revision_id. Isso significa que você terá IDs de revisão duplicados para diferentes tipos de entidades, mas isso não é um prazer para mim (não tenho certeza sobre o seu caso). A única observação importante a ser feita é que a combinação de entity_id e revision_id deve ser única.
Há também um campo de estado na tabela entity_revision que indica o estado da revisão. Ele pode ter um dos três estados:
latest
,obsolete
oudeleted
(não contando com a data de revisões ajuda muito a aumentar suas consultas).Uma última observação em revision_id, não criei uma chave estrangeira conectando employee_id a revision_id porque não queremos alterar a tabela entity_revision para cada tipo de entidade que poderemos adicionar no futuro.
INSERÇÃO
Para cada funcionário que você deseja inserir no banco de dados, você também vai adicionar um registro de entidade e entity_revision . Esses dois últimos registros ajudarão você a controlar quem e quando um registro foi inserido no banco de dados.
ATUALIZAR
Cada atualização para um registro de funcionário existente será implementada como duas inserções, uma na tabela de funcionários e outra na entidade_revisão. O segundo ajudará você a saber por quem e quando o registro foi atualizado.
ELIMINAÇÃO
Para excluir um funcionário, um registro é inserido em entity_revision informando a exclusão e concluída.
Como você pode ver nesse design, nenhum dado é alterado ou removido do banco de dados e, mais importante, cada tipo de entidade requer apenas uma tabela. Pessoalmente, acho esse design realmente flexível e fácil de trabalhar. Mas não tenho certeza sobre você, pois suas necessidades podem ser diferentes.
[ATUALIZAR]
Tendo suportado partições nas novas versões do MySQL, acredito que meu design também vem com uma das melhores performances. Pode-se particionar
entity
tabela usandotype
campo enquanto particionarentity_revision
usando seustate
campo. Isso aumentaráSELECT
de longe as consultas, mantendo o design simples e limpo.fonte
Se de fato uma trilha de auditoria é tudo o que você precisa, eu me inclinaria para a solução da tabela de auditoria (com cópias desnormalizadas da coluna importante em outras tabelas, por exemplo
UserName
). Lembre-se, porém, que a experiência amarga indica que uma única tabela de auditoria será um enorme gargalo no caminho; provavelmente vale a pena o esforço para criar tabelas de auditoria individuais para todas as suas tabelas auditadas.Se você precisar rastrear as versões históricas (e / ou futuras) reais, a solução padrão é rastrear a mesma entidade com várias linhas usando alguma combinação de valores de início, fim e duração. Você pode usar uma visualização para tornar conveniente o acesso aos valores atuais. Se essa é a abordagem adotada, você pode encontrar problemas se os dados com versão referenciarem dados mutáveis, mas não versionados.
fonte
Se você quiser fazer o primeiro, também poderá usar XML para a tabela Funcionários. A maioria dos bancos de dados mais novos permite consultar campos XML, portanto, isso nem sempre é um problema. E pode ser mais simples ter uma maneira de acessar os dados dos funcionários, independentemente da versão mais recente ou anterior.
Eu tentaria a segunda abordagem embora. Você pode simplificar isso tendo apenas uma tabela Funcionários com um campo DateModified. O EmployeeId + DateModified seria a chave primária e você pode armazenar uma nova revisão apenas adicionando uma linha. Dessa forma, o arquivamento de versões mais antigas e a restauração de versões do archive também são mais fáceis.
Outra maneira de fazer isso poderia ser o modelo de datavault de Dan Linstedt. Eu fiz um projeto para o departamento de estatística holandês que usou esse modelo e funciona muito bem. Mas não acho que seja diretamente útil para o uso diário do banco de dados. Você pode ter algumas idéias ao ler os artigos dele.
fonte
E se:
Você cria a chave primária (EmployeeId, DateModified) e, para obter os registros "atuais", basta selecionar MAX (DateModified) para cada ID de funcionário. Armazenar um IsCurrent é uma péssima idéia, porque, em primeiro lugar, pode ser calculado e, em segundo lugar, é muito fácil para os dados ficarem fora de sincronia.
Você também pode fazer uma exibição que lista apenas os registros mais recentes e usá-la principalmente enquanto trabalha no aplicativo. O bom dessa abordagem é que você não possui duplicatas de dados e não precisa coletar dados de dois lugares diferentes (atual em Employees e arquivados em EmployeesHistory) para obter todo o histórico ou reversão, etc.) .
fonte
Se você deseja confiar nos dados do histórico (por motivos de relatório), deve usar a estrutura mais ou menos assim:
Ou solução global para aplicação:
Você pode salvar suas revisões também em XML, e só possui um registro para uma revisão. Será assim:
fonte
Tivemos requisitos semelhantes e o que descobrimos foi que, muitas vezes, o usuário quer apenas ver o que foi alterado, não necessariamente reverter as alterações.
Não tenho certeza de qual é o seu caso de uso, mas o que fizemos foi criar e tabela de auditoria que é atualizada automaticamente com alterações em uma entidade comercial, incluindo o nome amigável de qualquer referência de chave estrangeira e enumerações.
Sempre que o usuário salva suas alterações, recarregamos o objeto antigo, executamos uma comparação, registramos as alterações e salvamos a entidade (tudo é feito em uma única transação do banco de dados, caso haja algum problema).
Isso parece funcionar muito bem para nossos usuários e economiza a dor de cabeça de ter uma tabela de auditoria completamente separada com os mesmos campos da nossa entidade comercial.
fonte
Parece que você deseja rastrear alterações em entidades específicas ao longo do tempo, por exemplo, ID 3, "bob", "123 main street" e, em seguida, outro ID 3, "bob" "234 elm st" e assim por diante, essencialmente, podendo vomitar um histórico de revisões mostrando todos os endereços em que "bob" esteve.
A melhor maneira de fazer isso é ter um campo "é atual" em cada registro e (provavelmente) um carimbo de data / hora ou FK para uma tabela de data / hora.
As inserções precisam definir o "é atual" e também desabilitar o "é atual" no registro "é atual" anterior. As consultas precisam especificar o "é atual", a menos que você queira todo o histórico.
Existem outros ajustes para isso se for uma tabela muito grande ou se for esperado um grande número de revisões, mas essa é uma abordagem bastante padrão.
fonte