Datenbankdesign: Wie gehe ich mit dem Problem des Archivs um?

18

Ich bin mir ziemlich sicher, dass viele Anwendungen, kritische Anwendungen, Banken usw. dies täglich tun.

Die Idee dahinter ist:

  • Alle Zeilen müssen einen Verlauf haben
  • Alle Links müssen kohärent bleiben
  • Es sollte einfach sein, Anfragen zu stellen, um "aktuelle" Spalten zu erhalten
  • Kunden, die veraltete Produkte gekauft haben, sollten weiterhin sehen, was sie gekauft haben, obwohl dieses Produkt nicht mehr im Katalog enthalten ist

und so weiter.

Hier ist, was ich tun möchte, und ich erkläre die Probleme, mit denen ich konfrontiert bin.

Alle meine Tabellen haben diese Spalten:

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

Und hier sind die Ideen für CRUD-Operationen:

  • erstelle = füge eine neue Zeile mit id_origin= id, date of creation= now, start date of validity= now, end date of validity= null ein (= bedeutet, es ist der aktuell aktive Datensatz)
  • update =
    • read = liest alle Datensätze mit end date of validity== null
    • Aktualisiere den "aktuellen" Datensatz end date of validity= null mit end date of validity= now
    • erstelle einen neuen mit den neuen Werten und end date of validity= null (= bedeutet, es ist der aktuell aktive Datensatz)
  • delete = Aktualisiere den "aktuellen" Datensatz end date of validity= null mit end date of validity= now

Also hier ist mein Problem: mit vielen zu vielen Assoziationen. Nehmen wir ein Beispiel mit Werten:

  • Tabelle A (id = 1, id_origin = 1, start = now, end = null)
  • Tabelle A_B (start = now, end = null, id_A = 1, id_B = 48)
  • Tabelle B (id = 48, id_origin = 48, start = now, end = null)

Jetzt möchte ich Tabelle A aktualisieren, Datensatz-ID = 1

  • Ich markiere die Datensatz-ID = 1 mit end = now
  • Ich füge einen neuen Wert in Tabelle A ein und ... verdammt, ich habe meine Beziehung A_B verloren, es sei denn, ich dupliziere auch die Beziehung ... dies würde zu einer Tabelle führen:

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

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

Und ... nun, ich habe ein anderes Problem: die Beziehung A_B: soll ich (id_A = 1, id_B = 48) als veraltet markieren oder nicht (A - id = 1 ist veraltet, aber nicht B - 48)?

Wie gehe ich damit um?

Ich muss das in großem Maßstab gestalten: Produkte, Partner und so weiter.

Welche Erfahrungen haben Sie damit gemacht? Wie würden Sie tun (wie haben Sie getan)?

- Bearbeiten

Ich habe diesen sehr interessanten Artikel gefunden , aber er befasst sich nicht richtig mit "Cascasding Obsolescence" (= was ich eigentlich frage)

Olivier Pons
quelle
Wie wäre es, wenn Sie die Daten des Aktualisierungsdatensatzes kopieren, bevor er auf einen neuen Datensatz mit einer neuen ID aktualisiert wird, wobei die verknüpfte Liste des Verlaufs mit dem Feld id_hist_prev beibehalten wird? Die ID des aktuellen Datensatzes wird also nie geändert
Haben Sie überlegt, das Rad neu zu erfinden, beispielsweise Flashback Data Archive unter Oracle zu verwenden?
Jack Douglas

Antworten:

4

Mir ist nicht klar, ob diese Anforderungen zu Prüfungszwecken oder nur als einfache historische Referenz dienen, z. B. für CRM und Einkaufswagen.

In beiden Fällen sollten Sie eine Haupt- und eine Hauptarchivtabelle für jeden Hauptbereich in Betracht ziehen, in dem dies erforderlich ist. "Main" hat nur aktuelle / aktive Einträge, während "main_archive" eine Kopie von allem hat, was jemals in main geht. Einfügen / Aktualisieren in Hauptarchiv kann ein Auslöser für das Einfügen / Aktualisieren in Hauptarchiv sein. Löschvorgänge für main_archive können dann ggf. über einen längeren Zeitraum ausgeführt werden.

Bei referenziellen Problemen wie Cust X, das Produkt Y gekauft hat, können Sie Ihr referenzielles Problem mit cust_archive -> product_archive am einfachsten lösen, indem Sie niemals Einträge aus product_archive löschen. Im Allgemeinen sollte die Abwanderung in dieser Tabelle viel geringer sein, damit die Größe kein Problem darstellt.

HTH.


quelle
2
Gute Antwort, aber ich möchte hinzufügen, dass ein weiterer Vorteil einer Archivtabelle darin besteht, dass sie in der Regel denormalisiert ist, wodurch die Berichterstellung für solche Daten wesentlich effizienter wird. Berücksichtigen Sie bei diesem Ansatz auch die Berichtsanforderungen Ihrer Anwendung.
maple_shaft
1
In den meisten Datenbanken, die ich entwerfe, haben alle 'Haupt'-Tabellen ein Präfix des Produktnamens LP_, und jede einzelne wichtige Tabelle hat ein Äquivalent LH_, wobei Trigger historische Zeilen beim Einfügen, Aktualisieren und Löschen einfügen. Es funktioniert nicht in allen Fällen, aber es war ein solides Modell für die Dinge, die ich tue.
Ich stimme zu - wenn die Mehrheit der Abfragen für die "aktuellen" Zeilen bestimmt ist, erhalten Sie wahrscheinlich einen perfekten Vorteil, wenn Sie den aktuellen Verlauf in zwei Tabellen unterteilen. Eine Ansicht könnte sie als Annehmlichkeit wieder zusammenführen. Auf diese Weise sind die Datenseiten mit den aktuellen Zeilen alle zusammen und bleiben wahrscheinlich besser im Cache, und Sie müssen nicht ständig Abfragen für aktuelle Daten mit Datumslogik qualifizieren.
onupdatecascade
1
@onupdatecascade: Beachten Sie, dass Sie (zumindest in einigen RDBMS) Indizes für diese UNIONAnsicht erstellen können , mit denen Sie beispielsweise eine eindeutige Einschränkung für aktuelle und historische Datensätze erzwingen können.
Jon of All Trades
5 Jahre später habe ich Unmengen von Dingen erledigt und die ganze Zeit habe ich dir deine Idee zurückgegeben. Das einzige, was ich geändert habe, ist, dass ich in Verlaufstabellen die Spalten " id" und " id_ref" habe. id_refist ein Verweis auf die eigentliche Idee der Tabelle. Beispiel: personund person_h. in person_hIch habe " id" und " id_ref" wo id_refsteht mit ' person.id' in Beziehung , so dass ich viele Zeilen mit dem gleichen haben kann person.id(= wenn eine Zeile von persongeändert wird) und idalle meine Tabellen sind autoinc.
Olivier Pons
2

Dies hat einige Überschneidungen mit der funktionalen Programmierung; speziell das Konzept der Unveränderlichkeit.

Sie haben einen Tisch angerufen PRODUCTund einen anderen angerufen PRODUCTVERSIONoder ähnlich. Wenn Sie ein Produkt ändern und kein Update durchführen, fügen Sie einfach eine neue PRODUCTVERSIONZeile ein. Um die neueste Version zu erhalten, können Sie die Tabelle nach Versionsnummer (absteigend), Zeitstempel (absteigend) oder Kennzeichen ( LatestVersion) indizieren .

Wenn Sie etwas haben, das auf ein Produkt verweist, können Sie entscheiden, auf welche Tabelle es verweist. Verweist es auf die PRODUCTEntität (bezieht sich immer auf dieses Produkt) oder auf die PRODUCTVERSIONEntität (bezieht sich nur auf diese Version des Produkts)?

Es wird kompliziert. Was ist, wenn Sie Bilder des Produkts haben? Sie müssen auf die Versionstabelle verweisen, da sie geändert werden können. In vielen Fällen werden sie dies jedoch nicht tun und Sie möchten Daten nicht unnötig duplizieren. Das heißt, Sie brauchen einen PICTURETisch und eine PRODUCTVERSIONPICTUREBeziehung von vielen zu vielen.


quelle
1

Ich habe all das Zeug von hier mit 4 Feldern implementiert , die sich auf allen meinen Tabellen befinden:

  • Ich würde
  • date_creation
  • date_validity_start
  • date_validity_end

Jedes Mal, wenn ein Datensatz geändert werden muss, dupliziere ich ihn. Markiere den duplizierten Datensatz als "alt" = date_validity_end=NOW()und den aktuellen als den guten date_validity_start=NOW()und date_validity_end=NULL.

Der Trick handelt von den vielen zu vielen und eins zu vielen Beziehungen: Es funktioniert, ohne sie zu berühren! Es geht nur um die komplexeren Abfragen: Um einen Datensatz an einem bestimmten Datum (= nicht jetzt) abzufragen , muss ich für jeden Join und für die Haupttabelle die folgenden Einschränkungen hinzufügen:

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

Also mit Produkten und Attributen (viele zu viele Relationen):

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)
)
Olivier Pons
quelle
0

Wie wäre es damit? Es scheint einfach und ziemlich effektiv für das, was ich in der Vergangenheit getan habe. Verwenden Sie in Ihrer "Verlaufstabelle" eine andere PK. Ihr "CustomerID" -Feld ist also die PK in Ihrer Kundentabelle, aber in der "history" -Tabelle ist Ihre PK "NewCustomerID". "CustomerID" wird nur ein weiteres schreibgeschütztes Feld. Dadurch bleibt "CustomerID" im Verlauf unverändert, und alle Ihre Beziehungen bleiben bestehen.

Dimondwoof
quelle
Sehr schöne Idee. Was ich getan habe, ist sehr ähnlich: Ich dupliziere den Datensatz und markiere den neuen als "veraltet", so dass der aktuelle Datensatz immer noch der gleiche ist. Hinweis Ich wollte für jede Tabelle einen Trigger erstellen, aber mysql verbietet Änderungen an einer Tabelle, wenn Sie sich in einem Trigger dieser Tabelle befinden. PostGRESQL macht das. SQL Server tun dies. Oracle macht das. Kurz gesagt, MySQL hat noch einen sehr langen Weg vor sich, und das nächste Mal werde ich bei der Auswahl meines Datenbankservers zweimal überlegen.
Olivier Pons