Peut-on conserver une valeur qui se met à jour dans une table?

31

Nous développons une plate-forme pour les cartes prépayées, qui contient essentiellement des données sur les cartes et leur solde, les paiements, etc.

Jusqu'à présent, nous avions une entité de carte qui a une collection d'entités de compte, et chaque compte a un montant, qui est mis à jour dans chaque dépôt / retrait.

Il y a maintenant un débat dans l'équipe; quelqu'un nous a dit que cela enfreint les 12 règles de Codd et que la mise à jour de sa valeur à chaque paiement est un problème.

Est-ce vraiment un problème?

Si tel est le cas, comment pouvons-nous résoudre ce problème?

Mithir
la source
3
Il y a une discussion technique approfondie sur ce sujet ici sur DBA.SE: Rédaction d'un schéma bancaire simple
Nick Chammas
1
Quelles règles de Codd votre équipe a-t-elle citées ici? Les règles étaient sa tentative de définir un système relationnel et ne mentionnaient pas explicitement la normalisation. Codd a discuté de la normalisation dans son livre Le modèle relationnel pour la gestion des bases de données .
Iain Samuel McLean Elder

Réponses:

30

Oui, ce n'est pas normalisé, mais parfois les conceptions non normalisées l'emportent pour des raisons de performances.

Cependant, je l'aborderais probablement un peu différemment, pour des raisons de sécurité. (Avertissement: je ne travaille pas actuellement, et je n'ai jamais travaillé dans le secteur financier.

Ayez une table pour les soldes affichés sur les cartes. Cela aurait une ligne insérée pour chaque compte, indiquant le solde affiché à la fin de chaque période (jour, semaine, mois ou tout ce qui est approprié). Indexez ce tableau par numéro de compte et date.

Utilisez une autre table pour conserver les transactions en attente, qui sont insérées à la volée. À la fin de chaque période, exécutez une routine qui ajoute les transactions non reportées au dernier solde de clôture du compte pour calculer le nouveau solde. Soit marquer les transactions en attente comme validées, soit regarder les dates pour déterminer ce qui est encore en attente.

De cette façon, vous avez un moyen de calculer un solde de carte à la demande, sans avoir à résumer tout l'historique du compte, et en plaçant le recalcul du solde dans une routine de validation dédiée, vous pouvez vous assurer que la sécurité des transactions de ce recalcul est limitée à un seul endroit (et également limiter la sécurité sur la table du solde afin que seule la routine de comptabilisation puisse y écrire).

Ensuite, conservez autant de données historiques que l'exigent l'audit, le service client et les exigences de performance.

db2
la source
1
Juste deux notes rapides. Tout d'abord, c'est une très bonne description de l'approche log-agrégat-instantané que je proposais ci-dessus, et peut-être plus claire que moi. (Vous a voté). Deuxièmement, je soupçonne que vous utilisez le terme "publié" de façon quelque peu étrange ici, pour signifier "une partie du solde de clôture". En termes financiers, publié signifie généralement «apparaître dans le solde du grand livre actuel» et il semblait donc qu'il valait la peine d'expliquer que cela ne causait pas de confusion.
Chris Travers
Oui, il y a probablement beaucoup de subtilités qui me manquent. Je fais simplement référence à la façon dont les transactions semblent être "validées" sur mon compte courant à la fermeture des bureaux, et le solde est mis à jour en conséquence. Mais je ne suis pas comptable; Je travaille juste avec plusieurs d'entre eux.
db2
Cela peut également être une exigence pour SOX ou similaires à l'avenir, je ne sais pas exactement quel type d'exigences de micro-transaction vous devez enregistrer, mais je demanderais par défaut à quelqu'un qui connaît les exigences de déclaration pour plus tard.
jcolebrand
Je serais enclin à conserver des données perpétuelles pour, par exemple, le solde au début de chaque année, afin que l'instantané "totaux" ne soit jamais écrasé - la liste est simplement ajoutée à (même si le système est resté en service assez longtemps pour que chaque compte accumuler 1 000 totaux annuels [ TRÈS optimiste], ce qui serait difficilement ingérable). La conservation de nombreux totaux annuels permettrait au code d'audit de confirmer que les transactions entre les dernières années ont eu les effets appropriés sur les totaux [les transactions individuelles pourraient être purgées après 5 ans, mais seraient bien vérifiées d'ici là].
supercat
17

De l'autre côté, il y a un problème que nous rencontrons fréquemment dans les logiciels de comptabilité. Paraphrasé:

Ai-je vraiment besoin d'agréger dix ans de données pour savoir combien d'argent est dans le compte courant?

La réponse est bien sûr non. Il y a quelques approches ici. L'un stocke la valeur calculée. Je ne recommande pas cette approche car les bogues logiciels qui provoquent des valeurs incorrectes sont très difficiles à détecter et j'éviterais donc cette approche.

Une meilleure façon de le faire est ce que j'appelle l'approche log-snapshot -greg. Dans cette approche, nos paiements et utilisations sont des insertions et nous ne mettons jamais à jour ces valeurs. Périodiquement, nous agrégons les données sur une période de temps et insérons un enregistrement d'instantané calculé qui représente les données au moment où l'instantané est devenu valide (généralement une période de temps avant la date actuelle).

Maintenant, cela ne viole pas les règles de Codd car au fil du temps, les instantanés peuvent être moins que parfaitement dépendants des données de paiement / d'utilisation insérées. Si nous avons des instantanés de travail, nous pouvons décider de purger des données vieilles de 10 ans sans affecter notre capacité à calculer les soldes actuels à la demande.

Chris Travers
la source
2
Je peux stocker des totaux calculés et je suis parfaitement en sécurité - des contraintes de confiance garantissent que mes numéros sont toujours corrects: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/…
AK
1
Il n'y a pas de cas limites dans ma solution - une contrainte de confiance ne vous laissera rien oublier. Je ne vois aucun besoin pratique de quantités NULES dans un système réel qui a besoin de connaître les totaux courants - ceux-ci se contredisent. Si vous voyez un besoin pratique, veuillez partager votre sceanrio.
AK
1
D'accord, mais cela ne fonctionnera pas comme sur les bases de données qui autorisent plusieurs NULL sans violer l'unicité, non? De plus, votre garantie va mal si vous purgez les données antérieures, non?
Chris Travers
1
Par exemple, si j'ai une contrainte unique sur (a, b) dans PostgreSQL, je peux avoir plusieurs valeurs (1, null) pour (a, b) parce que chaque null est traité comme potentiellement unique, ce qui je pense est sémantiquement correct pour unknown valeurs .....
Chris Travers
1
Concernant "J'ai une contrainte unique sur (a, b) dans PostgreSQL, je peux avoir plusieurs valeurs (1, nulles)" - dans PostgreSql, nous devons utiliser un index partiel unique sur (a) où b est nul.
AK
7

Pour des raisons de performances, dans la plupart des cas, nous devons stocker le solde actuel - sinon, le calculer à la volée peut éventuellement devenir excessivement lent.

Nous stockons les totaux cumulés précalculés dans notre système. Pour garantir que les nombres sont toujours corrects, nous utilisons des contraintes. La solution suivante a été copiée depuis mon blog. Il décrit un inventaire, qui est essentiellement le même problème:

Le calcul des totaux cumulés est notoirement lent, que vous le fassiez avec un curseur ou avec une jointure triangulaire. Il est très tentant de dénormaliser, de stocker les totaux en cours dans une colonne, surtout si vous la sélectionnez fréquemment. Cependant, comme d'habitude lorsque vous dénormalisez, vous devez garantir l'intégrité de vos données dénormalisées. Heureusement, vous pouvez garantir l'intégrité des totaux cumulés avec des contraintes - tant que toutes vos contraintes sont fiables, tous vos totaux cumulés sont corrects. De cette façon, vous pouvez facilement vous assurer que le solde actuel (totaux cumulés) n'est jamais négatif - l'application par d'autres méthodes peut également être très lente. Le script suivant illustre la technique.

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
AK
la source
Il me semble que l'une des énormes limites de votre approche est que le calcul du solde d'un compte à une date historique spécifique nécessite toujours une agrégation, sauf si vous faites également l'hypothèse que toutes les transactions sont entrées séquentiellement par date (ce qui est généralement une mauvaise supposition).
Chris Travers
@ChrisTravers tous les totaux en cours sont toujours à jour, pour toutes les dates historiques. Les contraintes le garantissent. Aucune agrégation n'est donc nécessaire pour les dates historiques. Si nous devons mettre à jour une ligne historique ou insérer quelque chose de rétroactif, nous mettons à jour les totaux cumulés des lignes suivantes. Je pense que c'est beaucoup plus facile dans postgreSql, car il a des contraintes différées.
AK
6

C'est une très bonne question.

En supposant que vous ayez une table de transactions qui stocke chaque débit / crédit, il n'y a rien de mal avec votre conception. En fait, j'ai travaillé avec des systèmes de télécommunications prépayés qui fonctionnaient exactement de cette façon.

La principale chose que vous devez faire est de vous assurer que vous effectuez SELECT ... FOR UPDATEle solde pendant que vous effectuez INSERTle débit / crédit. Cela garantira le bon équilibre en cas de problème (car toute la transaction sera annulée).

Comme d'autres l'ont fait remarquer, vous aurez besoin d'un instantané des soldes à des périodes spécifiques pour vérifier que toutes les transactions d'une somme donnée avec les soldes de début / fin de période sont correctes. Pour ce faire, écrivez un travail par lots qui s'exécute à minuit à la fin de la période (mois / semaine / jour).

Philᵀᴹ
la source
4

Le solde est un montant calculé en fonction de certaines règles commerciales, donc oui, vous ne voulez pas garder le solde mais plutôt le calculer à partir des transactions sur la carte et donc du compte.

Vous souhaitez garder une trace de toutes les transactions sur la carte pour l'audit et le rapport de déclaration, et même les données de différents systèmes plus tard.

Conclusion - calculez toutes les valeurs qui doivent être calculées au fur et à mesure que vous en avez besoin

Stephen Senkomago Musoke
la source
même s'il peut y avoir des milliers de transactions? Je vais donc devoir le recalculer à chaque fois? ça ne peut pas être un peu dur pour la performance? pouvez-vous ajouter un peu pourquoi il s'agit d'un tel problème?
Mithir
2
@Mithir Parce que cela va à l'encontre de la plupart des règles de comptabilité et rend les problèmes impossibles à tracer. Si vous venez de mettre à jour un total cumulé, comment savez-vous quels ajustements ont été appliqués? Cette facture a-t-elle été créditée une ou deux fois? Avons-nous déjà déduit le montant du paiement? Si vous suivez les transactions, vous connaissez les réponses, si vous suivez un total, vous ne le savez pas.
JNK
4
La référence aux règles de Codd est qu'elle rompt la forme normale. En supposant que vous suivez les transactions QUELQUE PART (ce que vous devrez, je pense) et que vous avez un total cumulé distinct, qui est correct si elles ne sont pas d'accord? Vous avez besoin d'une seule version de la vérité. Ne corrigez pas le problème de performances tant que / sauf s'il existe réellement.
JNK
@JNK tel qu'il est actuellement - nous conservons les transactions et un total, de sorte que tout ce que vous avez mentionné peut être parfaitement suivi si nécessaire, le total du solde est juste pour nous empêcher de recalculer le montant à chaque action.
Mithir
2
Maintenant, cela ne violera pas les règles de Codd si les anciennes données ne peuvent être conservées que pendant, disons, 5 ans, non? À ce stade, le solde n'est pas simplement la somme des enregistrements existants, mais également les enregistrements existants depuis la purge, ou ai-je raté quelque chose? Il me semble que cela ne briserait les règles de Codd que si nous supposons une rétention de données infinie, ce qui est peu probable. Cela étant dit pour les raisons que je dis ci-dessous, je pense que le stockage d'une valeur de mise à jour continue pose des problèmes.
Chris Travers