È corretto mantenere un valore che si aggiorna in una tabella?


31

Stiamo sviluppando una piattaforma per carte prepagate, che fondamentalmente contiene dati sulle carte e sul loro saldo, pagamenti, ecc.

Fino ad ora avevamo un'entità Carta che ha una raccolta di entità Conto e ogni Conto ha un Importo, che si aggiorna in ogni Deposito / Prelievo.

C'è un dibattito ora nella squadra; qualcuno ci ha detto che ciò infrange le 12 regole di Codd e che aggiornare il suo valore su ogni pagamento è un problema.

E 'veramente un problema?

Se lo è, come possiamo risolvere questo problema?


3
C'è una discussione tecnica approfondita su questo argomento qui su DBA.SE: Scrivere un semplice schema bancario
Nick Chammas,

1
Quale delle regole di Codd ha citato la tua squadra qui? Le regole erano il suo tentativo di definire un sistema relazionale e non menzionavano esplicitamente la normalizzazione. Codd ha discusso della normalizzazione nel suo libro Il modello relazionale per la gestione dei database .
Iain Samuel McLean Elder il

Risposte:


30

Sì, i progetti non normalizzati, ma a volte non normalizzati, vincono per motivi di prestazioni.

Tuttavia, probabilmente lo approccerei in modo leggermente diverso, per motivi di sicurezza. (Dichiarazione di non responsabilità: attualmente non ho mai lavorato nel settore finanziario. Lo sto solo lanciando.)

Avere un tavolo per i saldi registrati sulle carte. Ciò avrebbe una riga inserita per ciascun conto, che indica il saldo registrato alla fine di ogni periodo (giorno, settimana, mese o qualunque cosa sia appropriata). Indicizza questa tabella per numero di conto e data.

Utilizzare un'altra tabella per conservare le transazioni in sospeso, che vengono inserite al volo. Alla fine di ogni periodo, eseguire una routine che aggiunge le transazioni non registrate all'ultimo saldo di chiusura del conto per calcolare il nuovo saldo. Contrassegna le transazioni in sospeso come registrate o osserva le date per determinare cosa è ancora in sospeso.

In questo modo, hai un modo per calcolare un saldo della carta su richiesta, senza dover riassumere tutta la cronologia dell'account e inserendo il ricalcolo del saldo in una routine di registrazione dedicata, puoi garantire che la sicurezza delle transazioni di questo ricalcolo sia limitata a un unico posto (e anche limitare la sicurezza nella tabella dei bilanci in modo che solo la routine di registrazione possa scrivergli).

Quindi mantieni tutti i dati storici necessari per il controllo, il servizio clienti e i requisiti di prestazione.


1
Solo due brevi note. Innanzitutto è un'ottima descrizione dell'approccio log-aggregate-snapshot che stavo suggerendo sopra, e forse più chiaro di me. (Ti ho votato). In secondo luogo, sospetto che tu stia usando il termine "pubblicato" in qualche modo stranamente qui, per indicare "parte del saldo finale". In termini finanziari, postare di solito significa "presentarsi nel saldo del libro mastro corrente" e quindi sembrava che valesse la pena spiegarlo, quindi non ha creato confusione.
Chris Travers,

Sì, probabilmente ci sono molte sottigliezze che mi stanno perdendo. Mi riferisco solo al modo in cui le transazioni sembrano "registrate" sul mio conto corrente alla chiusura delle attività e il saldo viene aggiornato di conseguenza. Ma non sono un ragioniere; Lavoro solo con molti di loro.
db2,

Questo potrebbe anche essere un requisito per SOX o simili in futuro, non so esattamente quale tipo di requisiti di micro-transazione devi registrare, ma vorrei chiedere a qualcuno che sa quali sono i requisiti di reporting in seguito.
jcolebrand

Sarei propenso a conservare i dati perpetui, ad esempio l'equilibrio all'inizio di ogni anno, in modo che l'istantanea "totali" non venga mai sovrascritta - l'elenco viene semplicemente aggiunto (anche se il sistema è rimasto in uso abbastanza a lungo per ogni account da accumulare 1.000 totali annui [ MOLTO ottimista], che difficilmente sarebbe ingestibile). Mantenere molti totali annuali consentirebbe al codice di audit di confermare che le transazioni tra gli ultimi anni hanno avuto gli effetti corretti sui totali [le singole transazioni potrebbero essere eliminate dopo 5 anni, ma a quel punto sarebbero ben verificate].
supercat

17

D'altra parte, c'è un problema che incontriamo frequentemente nel software di contabilità. parafrasato:

Devo davvero aggregare dieci anni di dati per scoprire quanti soldi ci sono nel conto corrente?

La risposta ovviamente è no. Ci sono alcuni approcci qui. Uno è la memorizzazione del valore calcolato. Non consiglio questo approccio perché i bug del software che causano valori errati sono molto difficili da rintracciare e quindi eviterei questo approccio.

Un modo migliore per farlo è quello che chiamo l'approccio log-snapshot-aggregate. In questo approccio i nostri pagamenti e utilizzi sono inserti e non aggiorniamo mai questi valori. Periodicamente aggreghiamo i dati per un periodo di tempo e inseriamo un record di istantanea calcolato che rappresenta i dati nel momento in cui l'istantanea è diventata valida (di solito un periodo di tempo prima del presente).

Ora questo non infrange le regole di Codd perché nel tempo le istantanee potrebbero essere meno che perfettamente dipendenti dai dati di pagamento / utilizzo inseriti. Se disponiamo di istantanee funzionanti, possiamo decidere di eliminare i dati di 10 anni senza compromettere la nostra capacità di calcolare i saldi correnti su richiesta.


2
Posso memorizzare i totali di funzionamento calcolati e sono perfettamente al sicuro - vincoli di fiducia assicurano che i miei numeri siano sempre corretti: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/…
AK

1
Non ci sono casi limite nella mia soluzione: un vincolo di fiducia non ti farà dimenticare nulla. Non vedo alcun bisogno pratico di quantità NULL in un sistema di vita reale che ha bisogno di conoscere i totali correnti - questi a cose si contraddicono a vicenda. Se vedi un'esigenza pratica, ti preghiamo di condividere il tuo sceanrio.
AK,

1
Ok, ma allora questo non funzionerà come sui database che consentono più NULL senza violare l'unicità, giusto? Inoltre, la garanzia decade se si eliminano i dati passati, giusto?
Chris Travers,

1
Ad esempio, se ho un vincolo univoco su (a, b) in PostgreSQL, posso avere più valori (1, null) per (a, b) perché ogni null è trattato come potenzialmente unico, che penso sia semanticamente corretto per sconosciuto valori .....
Chris Travers,

1
Per quanto riguarda "Ho un vincolo univoco su (a, b) in PostgreSQL, posso avere più valori (1, null)" - in PostgreSql dobbiamo usare un indice parziale univoco su (a) dove b è nullo.
AK,

7

Per motivi di prestazioni, nella maggior parte dei casi è necessario memorizzare il saldo corrente, altrimenti il ​​calcolo al volo può eventualmente diventare proibitivamente lento.

Archiviamo i totali correnti precalcolati nel nostro sistema. Per garantire che i numeri siano sempre corretti, utilizziamo i vincoli. La seguente soluzione è stata copiata dal mio blog. Descrive un inventario, che è essenzialmente lo stesso problema:

Il calcolo dei totali correnti è notoriamente lento, sia che lo si faccia con un cursore che con un join triangolare. È molto allettante denormalizzare, archiviare i totali in esecuzione in una colonna, specialmente se lo selezioni frequentemente. Tuttavia, come al solito quando denormalizzi, devi garantire l'integrità dei tuoi dati denormalizzati. Fortunatamente, è possibile garantire l'integrità dei totali in esecuzione con vincoli - purché tutti i vincoli siano attendibili, tutti i totali in esecuzione siano corretti. Inoltre, in questo modo puoi facilmente assicurarti che il saldo corrente (totali correnti) non sia mai negativo - l'applicazione con altri metodi può anche essere molto lenta. Il seguente script dimostra la tecnica.

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

Mi viene in mente che uno degli enormi limiti del tuo approccio è che il calcolo del saldo di un conto in una data storica specifica richiede ancora l'aggregazione, a meno che tu non presuma anche che tutte le transazioni siano inserite in sequenza in base alla data (che di solito è un male assunzione).
Chris Travers,

@ChrisTravers tutti i totali in esecuzione sono sempre aggiornati, per tutte le date storiche. I vincoli lo garantiscono. Quindi non è necessario aggregare per nessuna data storica. Se dobbiamo aggiornare alcune righe storiche o inserire qualcosa di obsoleto, aggiorniamo tutti i totali in esecuzione delle righe successive. Penso che questo sia molto più facile in postgreSql, perché ha differito i vincoli.
AK,

6

Questa è un'ottima domanda

Supponendo che tu abbia una tabella delle transazioni che memorizza ogni debito / credito, non c'è nulla di sbagliato nel tuo design. In effetti, ho lavorato con sistemi di telefonia prepagata che hanno funzionato esattamente in questo modo.

La cosa principale che devi fare è assicurarti di fare una SELECT ... FOR UPDATEparte del saldo mentre fai INSERTil debito / credito. Ciò garantirà il saldo corretto se qualcosa va storto (perché verrà eseguito il rollback dell'intera transazione).

Come altri hanno sottolineato, avrai bisogno di un'istantanea dei saldi in determinati periodi di tempo per verificare che tutte le transazioni in un determinato periodo si sommino con i saldi di inizio / fine periodo correttamente. Scrivere un processo batch che viene eseguito a mezzanotte alla fine del periodo (mese / settimana / giorno) per farlo.


4

Il saldo è un importo calcolato in base a determinate regole aziendali, quindi sì, non si desidera mantenere il saldo ma piuttosto calcolarlo dalle transazioni sulla carta e quindi sul conto.

Si desidera tenere traccia di tutte le transazioni sulla carta per il controllo e la rendicontazione degli estratti conto e anche i dati provenienti da sistemi diversi in seguito.

Linea di fondo: calcola tutti i valori che devono essere calcolati come e quando è necessario


anche se potrebbero esserci migliaia di transazioni? Quindi dovrò ricalcolarlo ogni volta? non può essere un po 'difficile per le prestazioni? puoi aggiungere qualcosa sul perché questo è un problema?
Mithir,

2
@Mithir Perché va contro la maggior parte delle regole di contabilità e rende impossibile rintracciare i problemi. Se aggiorni solo un totale parziale, come fai a sapere quali aggiustamenti sono stati applicati? La fattura è stata accreditata una o due volte? Abbiamo già detratto l'importo del pagamento? Se segui le transazioni conosci le risposte, se segui un totale non lo fai.
JNK,

4
Il riferimento alle regole di Codd è che rompe la forma normale. Supponendo di tenere traccia delle transazioni OVUNQUE (cosa che dovrò pensare) e di avere un totale parziale separato, che è corretto se non sono d'accordo? Hai bisogno di un'unica versione della verità. Non risolvere il problema di prestazioni fino a quando non è effettivamente presente.
JNK,

@JNK così com'è ora: manteniamo le transazioni e un totale, quindi tutto ciò che hai menzionato può essere tracciato perfettamente se necessario, il totale del saldo è solo per impedirci di ricalcolare l'importo di ogni azione.
Mithir,

2
Ora, non infrange le regole di Codd se i vecchi dati possono essere conservati per, diciamo 5 anni, giusto? Il saldo a quel punto non è semplicemente la somma dei record esistenti, ma anche i record esistenti in precedenza da quando sono stati eliminati o mi manca qualcosa? Mi sembra che infrangerebbe le regole di Codd se assumessimo la conservazione infinita dei dati, il che è improbabile. Detto questo per motivi che dico di seguito, penso che memorizzare un valore in continuo aggiornamento richieda problemi.
Chris Travers,
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.