Progettazione del database: come gestire il problema "archivio"?


18

Sono abbastanza sicuro che molte applicazioni, applicazioni critiche, banche e così via lo facciano ogni giorno.

L'idea alla base di tutto ciò è:

  • tutte le righe devono avere una cronologia
  • tutti i collegamenti devono rimanere coerenti
  • dovrebbe essere facile fare richieste per ottenere colonne "attuali"
  • i clienti che hanno acquistato oggetti obsoleti dovrebbero comunque vedere ciò che hanno acquistato anche se questo prodotto non fa più parte del catalogo

e così via.

Ecco cosa voglio fare e spiegherò i problemi che sto affrontando.

Tutti i miei tavoli avranno quelle colonne:

  • id
  • id_origin
  • date of creation
  • start date of validity
  • start end of validity

Ed ecco le idee per le operazioni CRUD:

  • create = inserisci una nuova riga con id_origin= id, date of creation= now, start date of validity= now, end date of validity= null (= significa che è il record attivo corrente)
  • aggiornamento =
    • read = leggi tutti i record con end date of validity== null
    • aggiorna il record "corrente" end date of validity= null con end date of validity= now
    • creane uno nuovo con i nuovi valori e end date of validity= null (= significa che è il record attivo corrente)
  • delete = aggiorna il record "corrente" end date of validity= null con end date of validity= now

Quindi, ecco il mio problema: con molte associazioni. Facciamo un esempio con i valori:

  • Tabella A (id = 1, id_origin = 1, inizio = ora, fine = null)
  • Tabella A_B (inizio = ora, fine = null, id_A = 1, id_B = 48)
  • Tabella B (id = 48, id_origin = 48, inizio = ora, fine = null)

Ora voglio aggiornare la tabella A, record id = 1

  • Contrassegno id record = 1 con end = ora
  • Inserisco un nuovo valore nella tabella A e ... accidenti ho perso la mia relazione A_B a meno che non duplicassi anche la relazione ... questo finirebbe con una tabella:

  • Tabella A (id = 1, id_origin = 1, start = now, end = now + 8mn)

  • Tabella A (id = 2, id_origin = 1, start = now + 8mn, end = null)
  • Tabella A_B (inizio = ora, fine = null, id_A = 1, id_B = 48)
  • Tabella A_B (inizio = ora, fine = null, id_A = 2, id_B = 48)
  • Tabella B (id = 48, id_origin = 48, inizio = ora, fine = null)

E ... beh, ho un altro problema: la relazione A_B: devo contrassegnare (id_A = 1, id_B = 48) come obsoleto o meno (A - id = 1 è obsoleto, ma non B - 48)?

Come gestirlo?

Devo progettarlo su larga scala: prodotti, partner e così via.

Qual è la tua esperienza al riguardo? Come faresti (come hai fatto)?

-- Modificare

Ho trovato questo articolo molto interessante , ma non tratta correttamente "obsolescenza a cascata" (= cosa sto chiedendo in realtà)


Che ne dite di copiare i dati del record di aggiornamento prima che venga aggiornato in uno nuovo con un nuovo ID mantenendo l'elenco collegato della cronologia con il campo id_hist_prev. Quindi l'id del record attuale non viene mai cambiato

Piuttosto che reinventare la ruota, hai considerato di utilizzare, ad esempio, Flashback Data Archive su Oracle?
Jack Douglas,

Risposte:


4

Non mi è chiaro se questi requisiti siano a fini di controllo o solo semplici riferimenti storici come CRM e carrelli della spesa.

In entrambi i casi, considera di avere una tabella main e main_archive per ogni area principale in cui è richiesta. "Main" avrà solo voci correnti / attive mentre "main_archive" avrà una copia di tutto ciò che sarà mai andato in main. Inserire / aggiornare in main_archive può essere un trigger da inserire / aggiornare in main. Le eliminazioni contro main_archive possono quindi essere eseguite per un periodo di tempo più lungo, se mai.

Per i problemi referenziali come Cust X ha acquistato il Prodotto Y, il modo più semplice per risolvere il problema referenziale di cust_archive -> product_archive è di non eliminare mai le voci da product_archive. In genere, lo zangoletto dovrebbe essere molto più basso in quella tabella, quindi le dimensioni non dovrebbero essere un problema.

HTH.


2
Ottima risposta, ma vorrei aggiungere che un altro vantaggio derivante dall'avere una tabella di archivio è che tendono a essere denormalizzati, rendendo molto più efficiente la segnalazione di tali dati. Considera anche le esigenze di reporting della tua applicazione con questo approccio.
maple_shaft

1
Nella maggior parte dei database che progetto tutte le tabelle 'principali' hanno un prefisso come il nome del prodotto LP_, e ogni singola tabella importante ha un equivalente LH_, con i trigger che inseriscono righe storiche su insert, update, delete. Non funziona in tutti i casi, ma è stato un modello solido per le cose che faccio.

Sono d'accordo - se la maggior parte delle query riguarda le righe "correnti", probabilmente otterrai un vantaggio perfetto partizionando la corrente dalla cronologia in due tabelle. Una visione potrebbe unirli di nuovo insieme, per comodità. In questo modo le pagine di dati con le righe correnti sono tutte insieme e probabilmente rimangono meglio nella cache e non è necessario qualificare costantemente le query per i dati correnti con la logica della data.
onupdatecascade

1
@onupdatecascade: Nota che (almeno in alcuni RDBMS) puoi mettere gli indici in quella UNIONvista, il che ti consente di fare cose interessanti come far rispettare un vincolo unico tra i record attuali e storici.
Jon of All Trades,

5 anni dopo, ho fatto un sacco di cose e per tutto il tempo ti ho restituito la tua idea. L'unica cosa che ho cambiato è che nelle tabelle della cronologia ho una colonna " id" e " id_ref". id_refè un riferimento all'idea reale della tabella. Esempio: persone person_h. in person_hho " id" e " id_ref" dove id_refè correlato a ' person.id', quindi posso avere molte righe con lo stesso person.id(= quando una riga di personviene modificata) e tutte idle mie tabelle sono autoinc.
Olivier Pons

2

Ciò si sovrappone alla programmazione funzionale; in particolare il concetto di immutabilità.

Hai una tabella chiamata PRODUCTe un'altra chiamata PRODUCTVERSIONo simile. Quando si modifica un prodotto non si esegue un aggiornamento, è sufficiente inserire una nuova PRODUCTVERSIONriga. Per ottenere le ultime novità, è possibile indicizzare la tabella in base al numero di versione (desc), timestamp (desc) oppure è possibile avere un flag ( LatestVersion).

Ora se hai qualcosa che fa riferimento a un prodotto, puoi decidere a quale tabella indica. Indica l' PRODUCTentità (si riferisce sempre a questo prodotto) o PRODUCTVERSIONall'entità (si riferisce solo a questa versione del prodotto)?

Diventa complicato. Cosa succede se si dispone di immagini del prodotto? Devono puntare alla tabella delle versioni, perché potrebbero essere cambiate, ma in molti casi non lo faranno e non vorrai duplicare inutilmente i dati. Ciò significa che hai bisogno di un PICTUREtavolo e una relazione PRODUCTVERSIONPICTUREmolti-a-molti.


1

Ho implementato tutte le cose da qui con 4 campi che sono su tutti i miei tavoli:

  • id
  • DATE_CREATION
  • date_validity_start
  • date_validity_end

Ogni volta che un record deve essere modificato, lo duplico, contrassegno il record duplicato come "vecchio" = date_validity_end=NOW()e quello corrente come buono date_validity_start=NOW()e date_validity_end=NULL.

Il trucco riguarda le relazioni da molti a molti e da uno a molti: funziona senza toccarli! Riguarda le query che sono più complesse: per interrogare un record in una data precisa (= non ora), ho per ogni join, e per la tabella principale, aggiungere quei vincoli:

WHERE (
  (date_validity_start<=:dateparam AND date_validity_end IS NULL)
  OR
  (date_validity_start<=:dateparam AND date_validity_start>=:dateparam)
)

Quindi, con prodotti e attributi (relazione molti a molti):

SELECT p.*,a.*

FROM products p

JOIN products_attributes pa
ON pa.id_product = p.id
AND (
  (pa.date_validity_start<=:dateparam AND pa.date_validity_end IS NULL)
  OR
  (pa.date_validity_start<=:dateparam AND pa.date_validity_start>=:dateparam)
)

JOIN attributes a
ON a.id = pa.id_attribute
AND (
  (a.date_validity_start<=:dateparam AND a.date_validity_end IS NULL)
  OR
  (a.date_validity_start<=:dateparam AND a.date_validity_start>=:dateparam)
)

WHERE (
  (p.date_validity_start<=:dateparam AND p.date_validity_end IS NULL)
  OR
  (p.date_validity_start<=:dateparam AND p.date_validity_start>=:dateparam)
)

0

Cosa ne pensi di questo? Sembra semplice e piuttosto efficace per quello che ho fatto in passato. Nella tabella "cronologia", usa un PK diverso. Pertanto, il campo "CustomerID" è il PK nella tabella del cliente, ma nella tabella "cronologia", il PK è "NewCustomerID". "CustomerID" diventa solo un altro campo di sola lettura. Ciò lascia "CustomerID" invariato nella storia e tutte le tue relazioni rimangono intatte.


Ottima idea Quello che ho fatto è molto simile: duplico il record e contrassegno quello nuovo come "obsoleto" in modo che il record corrente sia sempre lo stesso. Nota Volevo creare un trigger su ogni tabella ma mysql proibisce le modifiche di una tabella quando si è in un trigger di questa tabella. PostGRESQL fa questo. SQL Server lo fa. Oracle fa questo. In breve, MySQL ha ancora molta strada da fare e la prossima volta ci penserò due volte quando sceglierò il mio server di database.
Olivier Pons
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.