Ist es in Ordnung, einen Wert beizubehalten, der in einer Tabelle aktualisiert wird?


31

Wir entwickeln eine Plattform für Prepaid-Karten, die im Wesentlichen Daten zu Karten und deren Guthaben, Zahlungen usw. enthält.

Bisher hatten wir eine Karteneinheit, die eine Sammlung von Kontoeinheiten enthält, und jedes Konto verfügt über einen Betrag, der bei jeder Ein- / Auszahlung aktualisiert wird.

Es gibt jetzt eine Debatte im Team; Jemand hat uns gesagt, dass dies gegen die 12 Regeln von Codd verstößt und die Aktualisierung des Wertes bei jeder Zahlung problematisch ist.

Ist das wirklich ein Problem?

Wenn ja, wie können wir das beheben?


3
Es gibt eine ausführliche technische Diskussion zu diesem Thema hier auf DBA.SE: Schreiben eines einfachen
Bankschemas

1
Welche Codd-Regeln hat Ihr Team hier angeführt? Die Regeln waren sein Versuch, ein relationales System zu definieren, und erwähnten die Normalisierung nicht ausdrücklich. Codd hat die Normalisierung in seinem Buch Das relationale Modell für die Datenbankverwaltung erörtert .
Iain Samuel McLean Elder

Antworten:


30

Ja, das ist nicht normalisiert, aber gelegentlich gewinnen nicht normalisierte Designs aus Leistungsgründen.

Aus Sicherheitsgründen würde ich es jedoch wahrscheinlich ein wenig anders angehen. (Haftungsausschluss: Ich habe zurzeit noch nie im Finanzsektor gearbeitet. Ich werfe das hier nur raus.)

Haben Sie eine Tabelle für die Guthaben auf den Karten gebucht. In diesem Fall wird für jedes Konto eine Zeile eingefügt, in der der Saldo zum Ende jedes Zeitraums (Tag, Woche, Monat oder was auch immer angemessen ist) angegeben ist. Indizieren Sie diese Tabelle nach Kontonummer und Datum.

Verwenden Sie eine andere Tabelle, um ausstehende Transaktionen zu speichern, die im laufenden Betrieb eingefügt werden. Führen Sie am Ende jedes Zeitraums eine Routine aus, die die nicht gebuchten Transaktionen zum letzten Schlusssaldo des Kontos hinzufügt, um den neuen Saldo zu berechnen. Markieren Sie die ausstehenden Transaktionen entweder als gebucht oder überprüfen Sie anhand der Daten, was noch aussteht.

Auf diese Weise haben Sie die Möglichkeit, bei Bedarf einen Kartensaldo zu berechnen, ohne den gesamten Kontoverlauf aufsummieren zu müssen, und indem Sie die Saldo-Neuberechnung in eine spezielle Buchungsroutine einfügen, können Sie sicherstellen, dass die Transaktionssicherheit dieser Neuberechnung auf beschränkt ist eine einzelne Stelle (und auch die Sicherheit in der Saldotabelle einschränken, sodass nur die Buchungsroutine darauf schreiben kann).

Bewahren Sie dann nur so viele historische Daten auf, wie aufgrund von Audits, Kundendienst und Leistungsanforderungen erforderlich sind.


1
Nur zwei kurze Notizen. Erstens ist das eine sehr gute Beschreibung des Log-Aggregate-Snapshot-Ansatzes, den ich oben vorgeschlagen habe, und vielleicht klarer als ich. (Hat dich aufgewertet). Zweitens vermute ich, dass Sie den Begriff "gebucht" hier etwas seltsam verwenden, um "einen Teil des Abschlusssaldos" zu bedeuten. In finanzieller Hinsicht bedeutet gebucht in der Regel "Auftauchen im aktuellen Sachkontostand", und daher schien es sich zu lohnen, dies zu erläutern, damit es nicht zu Verwirrung kam.
Chris Travers

Ja, es gibt wahrscheinlich viele Feinheiten, die ich verpasse. Ich beziehe mich nur darauf, wie Transaktionen zum Geschäftsschluss auf mein Girokonto gebucht und der Saldo entsprechend aktualisiert werden. Aber ich bin kein Buchhalter; Ich arbeite nur mit mehreren von ihnen.
db2

Dies kann in Zukunft auch eine Anforderung für SOX oder ähnliches sein. Ich weiß nicht genau, welche Anforderungen für Mikrotransaktionen Sie protokollieren müssen, aber ich würde auf jeden Fall jemanden fragen, der weiß, wie die Berichtsanforderungen später aussehen.
Jcolebrand

Ich würde gerne zu Beginn eines jeden Jahres ewige Daten für z. B. den Kontostand aufbewahren, damit der "Summen" -Schnappschuss nie überschrieben wird - die Liste wird einfach angehängt (auch wenn das System lange genug für jedes Konto verwendet wurde) akkumulieren 1.000 jährliche Summen [ SEHR optimistisch], das wäre kaum unüberschaubar). Wenn viele jährliche Gesamtbeträge aufbewahrt werden, kann der Prüfcode bestätigen, dass die Transaktionen der letzten Jahre die richtigen Auswirkungen auf die Gesamtbeträge hatten (einzelne Transaktionen könnten nach 5 Jahren gelöscht werden, wären aber bis dahin gut überprüft).
Superkatze

17

Auf der anderen Seite gibt es ein Problem, auf das wir bei Buchhaltungssoftware häufig stoßen. Umschrieben:

Muss ich wirklich zehn Jahre Daten sammeln, um herauszufinden, wie viel Geld sich auf dem Girokonto befindet?

Die Antwort ist natürlich nein, das tust du nicht. Hier gibt es einige Ansätze. Einer speichert den berechneten Wert. Ich empfehle diesen Ansatz nicht, da Softwarefehler, die zu falschen Werten führen, sehr schwer aufzuspüren sind und ich diesen Ansatz vermeiden würde.

Ein besserer Weg, dies zu tun, ist das, was ich als Log-Snapshot-Aggregat-Ansatz bezeichne. In diesem Ansatz sind unsere Zahlungen und Verwendungen Beilagen und wir aktualisieren diese Werte niemals. In regelmäßigen Abständen aggregieren wir die Daten über einen bestimmten Zeitraum und fügen einen berechneten Schnappschussdatensatz ein, der die Daten zum Zeitpunkt der Gültigkeitsdauer des Schnappschusses darstellt (in der Regel einen Zeitraum, bevor dieser gültig war ).

Dies verstößt nicht gegen die Regeln von Codd, da die Snapshots im Laufe der Zeit möglicherweise weniger als perfekt von den eingegebenen Zahlungs- / Nutzungsdaten abhängen. Wenn wir über funktionierende Snapshots verfügen, können wir entscheiden, 10 Jahre alte Daten zu löschen, ohne unsere Fähigkeit zu beeinträchtigen, aktuelle Salden nach Bedarf zu berechnen.


2
Ich kann berechnete laufende Summen speichern und bin absolut sicher - vertrauenswürdige Einschränkungen stellen sicher, dass meine Zahlen immer korrekt sind: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/…
AK

1
In meiner Lösung gibt es keine Randfälle - eine vertrauenswürdige Einschränkung lässt Sie nichts vergessen. Ich sehe keinen praktischen Bedarf an NULL-Mengen in einem realen System, das laufende Summen kennen muss - diese widersprechen sich. Wenn Sie einen praktischen Bedarf sehen, teilen Sie uns bitte Ihre Erfahrungen mit.
AK

1
Ok, aber dann wird das nicht so funktionieren wie bei DBs, die mehrere NULL-Werte zulassen, ohne die Eindeutigkeit zu verletzen, oder? Auch Ihre Garantie erlischt, wenn Sie frühere Daten löschen, richtig?
Chris Travers

1
Wenn ich beispielsweise in PostgreSQL eine eindeutige Einschränkung für (a, b) habe, kann ich mehrere (1, null) Werte für (a, b) haben, da jede Null als potenziell eindeutig behandelt wird, was meiner Meinung nach für unbekannt semantisch korrekt ist Werte .....
Chris Travers

1
Bezüglich "Ich habe eine eindeutige Einschränkung für (a, b) in PostgreSQL, ich kann mehrere (1, null) Werte haben" - in PostgreSql müssen wir einen eindeutigen Teilindex für (a) verwenden, wobei b null ist.
AK

7

Aus Leistungsgründen müssen wir in den meisten Fällen das aktuelle Guthaben speichern, da es sonst möglicherweise unerschwinglich langsam wird, es im laufenden Betrieb zu berechnen.

Wir speichern vorberechnete laufende Summen in unserem System. Um sicherzustellen, dass die Zahlen immer korrekt sind, verwenden wir Einschränkungen. Die folgende Lösung wurde aus meinem Blog kopiert. Es beschreibt ein Inventar, das im Wesentlichen das gleiche Problem ist:

Das Berechnen von laufenden Summen ist notorisch langsam, egal ob Sie sie mit einem Cursor oder mit einer Dreiecksverknüpfung ausführen. Es ist sehr verlockend zu denormalisieren, laufende Summen in einer Spalte zu speichern, besonders wenn Sie diese häufig auswählen. Beim Denormalisieren müssen Sie jedoch wie gewohnt die Integrität Ihrer denormalisierten Daten gewährleisten. Glücklicherweise können Sie die Integrität von laufenden Summen mit Einschränkungen garantieren. Solange alle Ihre Einschränkungen vertrauenswürdig sind, sind alle laufenden Summen korrekt. Auch auf diese Weise können Sie auf einfache Weise sicherstellen, dass der aktuelle Kontostand (laufende Summen) niemals negativ ist - die Durchsetzung durch andere Methoden kann auch sehr langsam sein. Das folgende Skript demonstriert die Technik.

CREATE TABLE Data.Inventory(InventoryID INT NOT NULL IDENTITY,
  ItemID INT NOT NULL,
  ChangeDate DATETIME NOT NULL,
  ChangeQty INT NOT NULL,
  TotalQty INT NOT NULL,
  PreviousChangeDate DATETIME NULL,
  PreviousTotalQty INT NULL,
  CONSTRAINT PK_Inventory PRIMARY KEY(ItemID, ChangeDate),
  CONSTRAINT UNQ_Inventory UNIQUE(ItemID, ChangeDate, TotalQty),
  CONSTRAINT UNQ_Inventory_Previous_Columns UNIQUE(ItemID, PreviousChangeDate, PreviousTotalQty),
  CONSTRAINT FK_Inventory_Self FOREIGN KEY(ItemID, PreviousChangeDate, PreviousTotalQty)
    REFERENCES Data.Inventory(ItemID, ChangeDate, TotalQty),
  CONSTRAINT CHK_Inventory_Valid_TotalQty CHECK(TotalQty >= 0 AND (TotalQty = COALESCE(PreviousTotalQty, 0) + ChangeQty)),
  CONSTRAINT CHK_Inventory_Valid_Dates_Sequence CHECK(PreviousChangeDate < ChangeDate),
  CONSTRAINT CHK_Inventory_Valid_Previous_Columns CHECK((PreviousChangeDate IS NULL AND PreviousTotalQty IS NULL)
            OR (PreviousChangeDate IS NOT NULL AND PreviousTotalQty IS NOT NULL))
);
GO
-- beginning of inventory for item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090101', 10, 10, NULL, NULL);
-- cannot begin the inventory for the second time for the same item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090102', 10, 10, NULL, NULL);

Msg 2627, Level 14, State 1, Line 10
Violation of UNIQUE KEY constraint 'UNQ_Inventory_Previous_Columns'. Cannot insert duplicate key in object 'Data.Inventory'.
The statement has been terminated.

-- add more
DECLARE @ChangeQty INT;
SET @ChangeQty = 5;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090103', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = 3;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090104', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = -4;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090105', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

-- try to violate chronological order

SET @ChangeQty = 5;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20081231', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

Msg 547, Level 16, State 0, Line 4
The INSERT statement conflicted with the CHECK constraint "CHK_Inventory_Valid_Dates_Sequence". The conflict occurred in database "Test", table "Data.Inventory".
The statement has been terminated.


SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- -----
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 5           15          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           18          2009-01-03 00:00:00.000 15
2009-01-05 00:00:00.000 -4          14          2009-01-04 00:00:00.000 18


-- try to change a single row, all updates must fail
UPDATE Data.Inventory SET ChangeQty = ChangeQty + 2 WHERE InventoryID = 3;
UPDATE Data.Inventory SET TotalQty = TotalQty + 2 WHERE InventoryID = 3;
-- try to delete not the last row, all deletes must fail
DELETE FROM Data.Inventory WHERE InventoryID = 1;
DELETE FROM Data.Inventory WHERE InventoryID = 3;

-- the right way to update

DECLARE @IncreaseQty INT;
SET @IncreaseQty = 2;
UPDATE Data.Inventory SET ChangeQty = ChangeQty + CASE WHEN ItemID = 1 AND ChangeDate = '20090103' THEN @IncreaseQty ELSE 0 END,
  TotalQty = TotalQty + @IncreaseQty,
  PreviousTotalQty = PreviousTotalQty + CASE WHEN ItemID = 1 AND ChangeDate = '20090103' THEN 0 ELSE @IncreaseQty END
WHERE ItemID = 1 AND ChangeDate >= '20090103';

SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- ----------------
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 7           17          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           20          2009-01-03 00:00:00.000 17
2009-01-05 00:00:00.000 -4          16          2009-01-04 00:00:00.000 20

Mir fällt auf, dass eine der enormen Grenzen Ihres Ansatzes darin besteht, dass die Berechnung des Kontostands an einem bestimmten historischen Datum immer noch eine Aggregation erfordert, es sei denn, Sie gehen auch davon aus, dass alle Transaktionen nach dem Datum der Reihe nach eingegeben werden (was in der Regel nicht der Fall ist Annahme).
Chris Travers

@ChrisTravers Alle laufenden Summen sind für alle historischen Daten immer auf dem neuesten Stand. Einschränkungen garantieren das. Daher ist für historische Daten keine Aggregation erforderlich. Wenn wir eine historische Zeile aktualisieren oder etwas mit einem Back-Date einfügen müssen, aktualisieren wir die laufenden Summen aller späteren Zeilen. Ich denke, das ist in postgreSql viel einfacher, weil es aufgeschobene Einschränkungen hat.
AK

6

Das ist eine sehr gute Frage.

Angenommen, Sie haben eine Transaktionstabelle, in der jede Lastschrift / Gutschrift gespeichert ist, dann ist an Ihrem Entwurf nichts falsch. Tatsächlich habe ich mit Prepaid-Telekommunikationssystemen gearbeitet, die genau so funktioniert haben.

Das wichtigste, was Sie tun müssen, ist sicherzustellen, dass Sie einen SELECT ... FOR UPDATETeil der Balance machen, während SieINSERT die Lastschrift / Gutschrift ausführen. Dies garantiert den korrekten Saldo, wenn etwas schief geht (da die gesamte Transaktion zurückgesetzt wird).

Wie bereits erwähnt, benötigen Sie eine Momentaufnahme der Salden zu bestimmten Zeitpunkten, um zu überprüfen, ob alle Transaktionen in einem bestimmten Zeitraum die korrekte Summe der Start- / Endsalden für den Zeitraum ergeben. Schreiben Sie dazu einen Stapeljob, der am Ende des Zeitraums (Monat / Woche / Tag) um Mitternacht ausgeführt wird.


4

Der Saldo ist ein berechneter Betrag, der auf bestimmten Geschäftsregeln basiert. Sie möchten den Saldo also nicht behalten, sondern ihn aus den Transaktionen auf der Karte und daher auf dem Konto berechnen.

Sie möchten alle Transaktionen auf der Karte für die Prüfung und Berichterstellung nachverfolgen und später sogar Daten aus verschiedenen Systemen.

Fazit: Berechnen Sie alle Werte, die nach Bedarf berechnet werden müssen


Auch wenn es vielleicht Tausende von Transaktionen gibt? Also muss ich es jedes Mal neu berechnen? Kann es nicht ein bisschen schwierig für die Leistung sein? Kannst du etwas dazu sagen, warum dies so ein Problem ist?
Mithir

2
@Mithir Weil es gegen die meisten Rechnungslegungsregeln verstößt und es Probleme unmöglich macht, sie nachzuvollziehen. Wenn Sie nur eine laufende Summe aktualisieren, woher wissen Sie, welche Anpassungen angewendet wurden? Wurde diese Rechnung ein- oder zweimal gutgeschrieben? Haben wir den Zahlungsbetrag bereits abgezogen? Wenn Sie Transaktionen verfolgen, kennen Sie die Antworten, wenn Sie eine Gesamtsumme verfolgen, wissen Sie nicht.
JNK

4
Der Verweis auf Codds Regeln ist, dass es gegen die normale Form verstößt. Angenommen, Sie verfolgen die Transaktionen ÜBERALL (was ich denke, müssen Sie tun), und Sie haben eine separate laufende Summe, was richtig ist, wenn sie nicht übereinstimmen? Sie brauchen eine einzige Version der Wahrheit. Beheben Sie das Leistungsproblem erst, wenn es tatsächlich vorhanden ist.
JNK

@JNK so wie es jetzt ist - wir behalten Transaktionen und eine Gesamtsumme bei, sodass alles, was Sie erwähnt haben, bei Bedarf perfekt nachverfolgt werden kann. Die Gesamtsumme dient nur dazu, uns daran zu hindern, den Betrag bei jeder Aktion neu zu berechnen.
Mithir

2
Jetzt wird es nicht gegen Codds Regeln verstoßen, wenn alte Daten nur etwa fünf Jahre lang aufbewahrt werden, oder? Der Saldo an diesem Punkt ist nicht nur die Summe der vorhandenen Datensätze, sondern auch die zuvor vorhandenen Datensätze, die gelöscht wurden, oder fehle ich etwas? Mir scheint, es würde nur gegen die Regeln von Codd verstoßen, wenn wir von einer unendlichen Datenaufbewahrung ausgehen, was unwahrscheinlich ist. Dies wird aus Gründen gesagt, die ich unten sage. Ich denke, dass das Speichern eines kontinuierlich aktualisierten Werts nach Problemen verlangt.
Chris Travers
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.