Progettazione di database per revisioni?


125

Nel progetto è richiesta la memorizzazione di tutte le revisioni (Cronologia delle modifiche) per le entità nel database. Attualmente abbiamo 2 proposte progettate per questo:

ad es. per l'entità "Dipendente"

Disegno 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)"

Disegno 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, .., ..)"

C'è un altro modo di fare questa cosa?

Il problema con "Design 1" è che dobbiamo analizzare XML ogni volta che è necessario accedere ai dati. Ciò rallenterà il processo e aggiungerà anche alcune limitazioni, in quanto non è possibile aggiungere join nei campi di dati delle revisioni.

E il problema con il "Design 2" è che dobbiamo duplicare ogni singolo campo su tutte le entità (abbiamo circa 70-80 entità per le quali vogliamo mantenere le revisioni).



1
Cordiali saluti: Nel caso in cui possa essere d'aiuto. SQL Server 2008 e versioni successive hanno una tecnologia che mostra la storia delle modifiche sulla tabella..visita simple-talk.com/sql/learn-sql-server/… per saperne di più e sono sicuro che i DB come Oracle avrà anche qualcosa del genere.
Durai Amuthan,

Tieni presente che alcune colonne potrebbero archiviare XML o JSON stessi. Se non è così ora, potrebbe succedere in futuro. Assicurati di non aver bisogno di nidificare tali dati l'uno nell'altro.
Jakubiszon,

Risposte:


38
  1. Non non mettere tutto in una tabella con un attributo discriminatore IsCurrent. Questo causa solo problemi, richiede chiavi surrogate e ogni sorta di altri problemi.
  2. Design 2 presenta problemi con le modifiche allo schema. Se si modifica la tabella Employees, è necessario modificare la tabella EmployeeHistories e tutti gli sproc relativi ad essa associati. Raddoppia potenzialmente lo sforzo di modifica dello schema.
  3. Il Design 1 funziona bene e, se fatto correttamente, non costa molto in termini di prestazioni. È possibile utilizzare uno schema xml e persino indici per superare possibili problemi di prestazioni. Il tuo commento sull'analisi dell'xml è valido ma potresti facilmente creare una vista usando xquery, che puoi includere nelle query e unirti. Qualcosa come questo...
CREATE VIEW EmployeeHistory
AS
, FirstName, , DepartmentId

SELECT EmployeeId, RevisionXML.value('(/employee/FirstName)[1]', 'varchar(50)') AS FirstName,

  RevisionXML.value('(/employee/LastName)[1]', 'varchar(100)') AS LastName,

  RevisionXML.value('(/employee/DepartmentId)[1]', 'integer') AS DepartmentId,

FROM EmployeeHistories 

25
Perché dici di non archiviare tutto in una tabella con il trigger IsCurrent. Potresti indicarmi alcuni esempi in cui ciò potrebbe diventare problematico.
Nathan W,

@Simon Munro Che ne dici di una chiave primaria o di una chiave cluster? Quale chiave possiamo aggiungere nella tabella della cronologia di Design 1 per rendere più rapida la ricerca?
gotqn

Presumo SELECT * FROM EmployeeHistory WHERE LastName = 'Doe'risultati semplici in una scansione completa della tabella . Non è la migliore idea per ridimensionare un'applicazione.
Kaii,

54

Penso che la domanda chiave da porre qui sia "Chi / che cosa utilizzerà la cronologia"?

Se si tratterà principalmente di rapporti / cronologia leggibile dall'uomo, abbiamo implementato questo schema in passato ...

Crea una tabella chiamata "AuditTrail" o qualcosa che abbia i seguenti campi ...

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[OldValue] [varchar](5000) NULL,
[NewValue] [varchar](5000) NULL

È quindi possibile aggiungere una colonna "LastUpdatedByUserID" a tutte le tabelle che deve essere impostata ogni volta che si esegue un aggiornamento / inserimento nella tabella.

È quindi possibile aggiungere un trigger a ogni tabella per rilevare eventuali inserimenti / aggiornamenti e creare una voce in questa tabella per ciascun campo modificato. Poiché la tabella viene inoltre fornita con "LastUpdateByUserID" per ogni aggiornamento / inserimento, è possibile accedere a questo valore nel trigger e utilizzarlo quando si aggiunge alla tabella di controllo.

Usiamo il campo RecordID per memorizzare il valore del campo chiave della tabella da aggiornare. Se è una chiave combinata, eseguiamo semplicemente una concatenazione di stringhe con un '~' tra i campi.

Sono sicuro che questo sistema potrebbe avere degli svantaggi: per i database fortemente aggiornati le prestazioni potrebbero essere compromesse, ma per la mia web-app, otteniamo molte più letture che scritture e sembra funzionare abbastanza bene. Abbiamo anche scritto una piccola utility VB.NET per scrivere automaticamente i trigger in base alle definizioni della tabella.

Solo un pensiero!


5
Non è necessario archiviare NewValue, poiché è archiviato nella tabella controllata.
Petrus Theron,

17
A rigor di termini, è vero. Ma - quando ci sono un certo numero di modifiche allo stesso campo per un certo periodo di tempo, la memorizzazione del nuovo valore rende le query come "mostrami tutte le modifiche apportate da Brian" molto più facilmente poiché tutte le informazioni su un aggiornamento sono contenute un record. Solo un pensiero!
Chris Roberts,

1
Penso che sysnamepossa essere un tipo di dati più adatto per i nomi di tabelle e colonne.
Sam,

2
@Sam utilizzando sysname non aggiunge alcun valore; potrebbe anche essere fonte di confusione ... stackoverflow.com/questions/5720212/...
Jowen

19

L' articolo Tabelle della storia nel blog del programmatore di database potrebbe essere utile: tratta alcuni dei punti sollevati qui e discute la memorizzazione dei delta.

modificare

Nel saggio History Tables , l'autore ( Kenneth Downs ), raccomanda di mantenere una tabella cronologica di almeno sette colonne:

  1. Data / ora del cambiamento,
  2. Utente che ha apportato la modifica,
  3. Un token per identificare il record che è stato modificato (in cui la cronologia viene gestita separatamente dallo stato corrente),
  4. Se la modifica è stata un inserimento, aggiornamento o eliminazione,
  5. Il vecchio valore,
  6. Il nuovo valore,
  7. Il delta (per le modifiche ai valori numerici).

Le colonne che non cambiano mai, o la cui cronologia non è richiesta, non devono essere tracciate nella tabella della cronologia per evitare il gonfiamento. La memorizzazione del delta per valori numerici può semplificare le query successive, anche se può essere derivata dai valori vecchi e nuovi.

La tabella della cronologia deve essere protetta e gli utenti non di sistema non possono inserire, aggiornare o eliminare righe. È necessario supportare solo lo spurgo periodico per ridurre le dimensioni complessive (e se consentito dal caso d'uso).


14

Abbiamo implementato una soluzione molto simile alla soluzione suggerita da Chris Roberts e che funziona abbastanza bene per noi.

L'unica differenza è che memorizziamo solo il nuovo valore. Dopo tutto, il vecchio valore viene archiviato nella riga della cronologia precedente

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[NewValue] [varchar](5000) NULL

Supponiamo che tu abbia una tabella con 20 colonne. In questo modo devi solo memorizzare la colonna esatta che è stata modificata invece di dover memorizzare l'intera riga.


14

Avoid Design 1; non è molto utile quando sarà necessario, ad esempio, ripristinare le versioni precedenti dei record, automaticamente o "manualmente" utilizzando la console degli amministratori.

Non vedo davvero gli svantaggi di Design 2. Penso che la seconda tabella History dovrebbe contenere tutte le colonne presenti nella prima tabella Records. Ad esempio in mysql puoi facilmente creare una tabella con la stessa struttura di un'altra tabella ( create table X like Y). E, quando stai per cambiare la struttura della tabella Records nel tuo database live, devi alter tablecomunque usare i comandi - e non c'è grande sforzo nell'eseguire questi comandi anche per la tua tabella History.

Appunti

  • La tabella dei record contiene solo l'ultima revisione;
  • La tabella Cronologia contiene tutte le revisioni precedenti dei record nella tabella Record;
  • La chiave primaria della tabella cronologica è una chiave primaria della tabella Records con RevisionIdcolonna aggiunta ;
  • Pensa a campi ausiliari aggiuntivi come ModifiedBy: l'utente che ha creato una revisione particolare. Potresti anche voler avere un campo DeletedByper tracciare chi ha eliminato una particolare revisione.
  • Pensa a cosa DateModifieddovrebbe significare - o significa dove è stata creata questa particolare revisione, o significherà quando questa particolare revisione è stata sostituita da un'altra. Il primo richiede che il campo sia nella tabella Records e sembra essere più intuitivo a prima vista; la seconda soluzione sembra tuttavia essere più pratica per i record eliminati (data in cui è stata eliminata questa particolare revisione). Se scegli la prima soluzione, probabilmente avresti bisogno di un secondo campo DateDeleted(solo se ovviamente ne hai bisogno). Dipende da te e da cosa vuoi effettivamente registrare.

Le operazioni in Design 2 sono molto banali:

Modificare
  • copia il record dalla tabella Records alla tabella History, dagli un nuovo RevisionId (se non è già presente nella tabella Records), gestisci DateModified (dipende da come lo interpreti, vedi le note sopra)
  • continuare con il normale aggiornamento del record nella tabella Records
Elimina
  • fare esattamente lo stesso del primo passaggio dell'operazione Modifica. Gestisci DateModified / DateDeleted di conseguenza, a seconda dell'interpretazione che hai scelto.
Annulla cancellazione (o rollback)
  • prendere la revisione più alta (o qualche particolare?) dalla tabella Cronologia e copiarla nella tabella Record
Elenca la cronologia delle revisioni per un particolare record
  • selezionare dalla tabella Cronologia e dalla tabella Record
  • pensa esattamente cosa ti aspetti da questa operazione; determinerà probabilmente quali informazioni sono richieste dai campi DateModified / DateDeleted (vedere le note sopra)

Se scegli Design 2, tutti i comandi SQL necessari per farlo saranno molto facili, così come la manutenzione! Forse, sarà molto più facile se usi le colonne ausiliarie ( RevisionId, DateModified) anche nella tabella Records - per mantenere entrambe le tabelle esattamente nella stessa struttura (tranne che per le chiavi univoche)! Ciò consentirà semplici comandi SQL, che saranno tolleranti a qualsiasi modifica della struttura dei dati:

insert into EmployeeHistory select * from Employe where ID = XX

Non dimenticare di usare le transazioni!

Per quanto riguarda il ridimensionamento , questa soluzione è molto efficiente, dal momento che non trasformi alcun dato da XML avanti e indietro, semplicemente copiando intere righe della tabella - query molto semplici, usando indici - molto efficiente!


12

Se devi memorizzare la cronologia, crea una tabella shadow con lo stesso schema della tabella che stai monitorando e una colonna "Data di revisione" e "Tipo di revisione" (ad es. "Elimina", "aggiorna"). Scrivi (o genera - vedi sotto) una serie di trigger per popolare la tabella di controllo.

È abbastanza semplice creare uno strumento che legga il dizionario dei dati di sistema per una tabella e generi uno script che crei la tabella shadow e una serie di trigger per popolarla.

Non tentare di utilizzare XML per questo, l'archiviazione XML è molto meno efficiente dell'archiviazione della tabella del database nativo utilizzata da questo tipo di trigger.


3
+1 per semplicità! Alcuni saranno troppo ingegnerizzati per paura di cambiamenti successivi, mentre la maggior parte delle volte in realtà non si verificano cambiamenti! Inoltre, è molto più semplice gestire le cronologie in una tabella e i record effettivi in ​​un'altra piuttosto che averle tutte in una tabella (incubo) con qualche flag o stato. Si chiama "KISS" e normalmente ti ricompenserà a lungo termine.
Jeach il

+1 sono completamente d'accordo, esattamente quello che dico nella mia risposta ! Semplice e potente!
TMS

8

Ramesh, sono stato coinvolto nello sviluppo del sistema basato sul primo approccio.
Si è scoperto che l'archiviazione delle revisioni come XML sta portando a un'enorme crescita del database e rallentando significativamente le cose.
Il mio approccio sarebbe quello di avere una tabella per entità:

Employee (Id, Name, ... , IsActive)  

dove IsActive è un segno dell'ultima versione

Se si desidera associare alcune informazioni aggiuntive alle revisioni, è possibile creare una tabella separata contenente tali informazioni e collegarla alle tabelle delle entità utilizzando la relazione PK \ FK.

In questo modo è possibile archiviare tutte le versioni dei dipendenti in una tabella. Pro di questo approccio:

  • Semplice base di dati
  • Nessun conflitto poiché la tabella diventa solo append
  • Puoi tornare alla versione precedente semplicemente cambiando il flag IsActive
  • Non è necessario alcun join per ottenere la cronologia degli oggetti

Tieni presente che la chiave primaria deve essere non univoca.


6
Vorrei utilizzare una colonna "RevisionNumber" o "RevisionDate" anziché o in aggiunta a IsActive, in modo da poter vedere tutte le revisioni in ordine.
Sklivvz,

Vorrei usare un "parentRowId" perché ti dà un facile accesso alle versioni precedenti e la possibilità di trovare rapidamente sia la base che la fine.
chacham15,

6

Il modo in cui l'ho visto fare in passato è

Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );

Non si "aggiorna" mai su questa tabella (tranne che per cambiare il valido di isCurrent), basta inserire nuove righe. Per ogni dato EmployeeId, solo 1 riga può avere isCurrent == 1.

La complessità di mantenerlo può essere nascosta dalle viste e dai trigger "anziché" (in Oracle, presumo cose simili altri RDBMS), puoi persino andare a viste materializzate se le tabelle sono troppo grandi e non possono essere gestite dagli indici) .

Questo metodo è ok, ma puoi finire con alcune query complesse.

Personalmente, mi piace molto il tuo modo di farlo con Design 2, ed è così che l'ho fatto anche in passato. È semplice da capire, semplice da implementare e semplice da mantenere.

Inoltre, crea un sovraccarico minimo per il database e l'applicazione, soprattutto quando si eseguono query di lettura, il che è probabilmente ciò che si farà il 99% delle volte.

Sarebbe anche abbastanza facile automatizzare la creazione delle tabelle della cronologia e dei trigger da mantenere (supponendo che sarebbe fatto tramite i trigger).


4

Le revisioni dei dati sono un aspetto del concetto di " tempo valido " di un database temporale. Sono state fatte molte ricerche in questo campo, e sono emersi molti modelli e linee guida. Ho scritto una lunga risposta con un sacco di riferimenti a questa domanda per chi fosse interessato.


4

Condividerò con te il mio design ed è diverso da entrambi i tuoi progetti in quanto richiede una tabella per ogni tipo di entità. Ho trovato il modo migliore per descrivere qualsiasi progetto di database è tramite ERD, ecco il mio:

inserisci qui la descrizione dell'immagine

In questo esempio abbiamo un'entità denominata dipendente . user table contiene i record dei tuoi utenti e entity e entity_revision sono due tabelle che contengono la cronologia delle revisioni per tutti i tipi di entità che avrai nel tuo sistema. Ecco come funziona questo design:

I due campi di entity_id e revision_id

Ogni entità nel tuo sistema avrà un ID entità unico a sé stante. La tua entità potrebbe passare attraverso le revisioni ma il suo entity_id rimarrà lo stesso. È necessario mantenere questo ID entità nella tabella dei dipendenti (come chiave esterna). Dovresti anche memorizzare il tipo di entità nella tabella delle entità (ad es. "Impiegato"). Ora, come per revision_id, come mostra il suo nome, tiene traccia delle revisioni della tua entità. Il modo migliore che ho trovato per questo è usare il dipendente_id come revisione_id. Ciò significa che avrai duplicati ID di revisione per diversi tipi di entità, ma questo non è un piacere per me (non sono sicuro del tuo caso). L'unica nota importante da prendere è che la combinazione di entity_id e revision_id dovrebbe essere unica.

C'è anche un campo di stato nella tabella entity_revision che indica lo stato di revisione. Può avere uno dei tre stati: latest, obsoleteo deleted(non contando sulla data di revisione si aiuta molto per aumentare le vostre domande).

Un'ultima nota su revision_id, non ho creato una chiave esterna che collega employee_id a revision_id perché non vogliamo modificare la tabella entity_revision per ogni tipo di entità che potremmo aggiungere in futuro.

INSERIMENTO

Per ogni dipendente che si desidera inserire nel database, si aggiungerà anche un record a entity e entity_revision . Questi ultimi due record ti aiuteranno a tenere traccia di chi e quando un record è stato inserito nel database.

AGGIORNARE

Ogni aggiornamento per un record di dipendente esistente verrà implementato come due inserti, uno nella tabella dei dipendenti e uno in entity_revision. Il secondo ti aiuterà a sapere da chi e quando il record è stato aggiornato.

CANCELLAZIONE

Per eliminare un dipendente, viene inserito un record in entity_revision che indica la cancellazione e fatto.

Come puoi vedere in questo progetto, nessun dato viene mai modificato o rimosso dal database e, soprattutto, ogni tipo di entità richiede solo una tabella. Personalmente trovo questo design davvero flessibile e facile da lavorare. Ma non sono sicuro di te in quanto le tue esigenze potrebbero essere diverse.

[AGGIORNARE]

Avendo supportato le partizioni nelle nuove versioni di MySQL, credo che il mio design abbia anche una delle migliori prestazioni. Si può partizionare la entitytabella usando il typecampo mentre la partizione entity_revisionusa il suo statecampo. Ciò aumenterà SELECTdi gran lunga le domande mantenendo il design semplice e pulito.


3

Se in effetti è sufficiente una pista di controllo, mi spingerei verso la soluzione della tabella di controllo (completa di copie denormalizzate della colonna importante su altre tabelle, ad es UserName.). Tenete presente, tuttavia, che l'amara esperienza indica che una singola tabella di audit rappresenterà un enorme collo di bottiglia lungo la strada; probabilmente vale la pena di creare singole tabelle di controllo per tutte le vostre tabelle controllate.

Se è necessario tenere traccia delle versioni storiche (e / o future) effettive, la soluzione standard è quella di tracciare la stessa entità con più righe utilizzando una combinazione di valori di inizio, fine e durata. È possibile utilizzare una vista per rendere conveniente l'accesso ai valori correnti. Se questo è l'approccio adottato, è possibile che si verifichino problemi se i dati con versione fanno riferimento a dati mutabili ma non controllati.


3

Se vuoi fare il primo, potresti voler usare anche XML per la tabella Employees. La maggior parte dei database più recenti consente di eseguire query nei campi XML, quindi questo non è sempre un problema. E potrebbe essere più semplice avere un modo per accedere ai dati dei dipendenti, indipendentemente dal fatto che sia la versione più recente o precedente.

Vorrei provare il secondo approccio però. Potresti semplificarlo disponendo di una sola tabella Employees con un campo DateModified. EmployeeId + DateModified sarebbe la chiave primaria e puoi archiviare una nuova revisione semplicemente aggiungendo una riga. In questo modo è anche più facile archiviare le versioni precedenti e ripristinare le versioni dall'archivio.

Un altro modo per farlo potrebbe essere il modello datavault di Dan Linstedt. Ho realizzato un progetto per l'ufficio statistico olandese che utilizzava questo modello e funziona abbastanza bene. Ma non penso che sia direttamente utile per l'uso quotidiano del database. Tuttavia, potresti trarre qualche idea dalla lettura dei suoi articoli.


2

Che ne dite di:

  • Numero Identità dell'impiegato
  • Data modificata
    • e / o numero di revisione, a seconda di come si desidera seguirlo
  • ModifiedByUSerId
    • oltre a qualsiasi altra informazione che desideri monitorare
  • Campi dei dipendenti

Si crea la chiave primaria (EmployeeId, DateModified) e per ottenere i record "attuali" è sufficiente selezionare MAX (DateModified) per ciascun Employid. La memorizzazione di un IsCurrent è una pessima idea, perché prima di tutto può essere calcolata e, in secondo luogo, è troppo facile che i dati non siano sincronizzati.

Puoi anche creare una vista che elenca solo i record più recenti e utilizzarla principalmente mentre lavori nella tua app. La cosa bella di questo approccio è che non hai duplicati di dati e non devi raccogliere dati da due luoghi diversi (attualmente in Employees e archiviati in EmployeesHistory) per ottenere tutta la cronologia o il rollback, ecc.) .


Uno svantaggio di questo approccio è che la tabella crescerà più rapidamente rispetto all'utilizzo di due tabelle.
cdmckay,

2

Se si desidera fare affidamento sui dati della cronologia (per motivi di reportistica) è necessario utilizzare una struttura simile a questa:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds the Employee revisions in rows.
"EmployeeHistories (HistoryId, EmployeeId, DateModified, OldValue, NewValue, FieldName)"

O soluzione globale per l'applicazione:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, OldValue, NewValue, FieldName)"

Puoi salvare le tue revisioni anche in XML, quindi hai un solo record per una revisione. Questo sarà simile a:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, XMLChanges)"

1
Meglio: usa il sourcing degli eventi :)
dariol il

1

Abbiamo avuto requisiti simili e quello che abbiamo scoperto è che spesso l'utente vuole solo vedere cosa è stato modificato, non necessariamente ripristinare eventuali modifiche.

Non sono sicuro di quale sia il tuo caso d'uso, ma ciò che abbiamo fatto è stato creare e controllare la tabella che viene automaticamente aggiornata con le modifiche a un'entità aziendale, incluso il nome descrittivo di eventuali riferimenti ed enumerazioni di chiavi esterne.

Ogni volta che l'utente salva le modifiche, ricarichiamo il vecchio oggetto, eseguiamo un confronto, registriamo le modifiche e salviamo l'entità (tutte vengono eseguite in una singola transazione del database in caso di problemi).

Questo sembra funzionare molto bene per i nostri utenti e ci fa risparmiare il mal di testa di avere una tabella di controllo completamente separata con gli stessi campi della nostra entità aziendale.


0

Sembra che tu voglia monitorare le modifiche a entità specifiche nel tempo, ad esempio ID 3, "bob", "123 main street", quindi un altro ID 3, "bob" "234 elm st" e così via, in sostanza potendo per elaborare una cronologia delle revisioni che mostra tutti gli indirizzi a cui è stato assegnato "bob".

Il modo migliore per farlo è quello di avere un campo "è corrente" su ogni record e (probabilmente) un timestamp o FK a una tabella data / ora.

Gli inserti devono quindi impostare "è corrente" e anche disinserire "è corrente" sul precedente record "è corrente". Le query devono specificare "è corrente", a meno che non si desideri tutta la cronologia.

Ci sono ulteriori modifiche a questo se si tratta di una tabella molto grande, o se ci si aspetta un gran numero di revisioni, ma questo è un approccio abbastanza standard.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.