Czy można zachować wartość, która aktualizuje się w tabeli?

31

Opracowujemy platformę dla kart przedpłaconych, która zasadniczo przechowuje dane o kartach i ich saldzie, płatnościach itp.

Do tej pory istniała jednostka Karty, która ma kolekcję Konta, a każde Konto ma Kwotę, która aktualizuje się przy każdym Depozycie / Wypłatie.

W zespole toczy się teraz debata; ktoś powiedział nam, że łamie to 12 zasad Codda i że aktualizowanie jego wartości przy każdej płatności jest kłopotliwe.

Czy to naprawdę problem?

Jeśli tak, to jak możemy to naprawić?

Mithir
źródło
3
Na stronie DBA.SE znajduje się obszerna dyskusja techniczna na ten temat: Pisanie prostego schematu bankowego
Nick Chammas
1
Które z zasad Codda przytoczył tu twój zespół? Reguły były jego próbą zdefiniowania systemu relacyjnego i nie wspominały wyraźnie o normalizacji. Codd omówił normalizację w swojej książce Relacyjny model zarządzania bazami danych .
Iain Samuel McLean Starszy

Odpowiedzi:

30

Tak, to nienormalizowane, ale czasami nienormalizowane projekty wygrywają ze względu na wydajność.

Prawdopodobnie jednak podchodziłbym do tego trochę inaczej ze względów bezpieczeństwa. (Oświadczenie: Obecnie nie pracuję ani nigdy nie pracowałem w sektorze finansowym. Po prostu to wyrzucam.)

Przygotuj tabelę dla zaksięgowanych sald na kartach. Oznaczałoby to wstawienie wiersza dla każdego konta, wskazującego zaksięgowane saldo na koniec każdego okresu (dzień, tydzień, miesiąc lub cokolwiek jest właściwe). Indeksuj tę tabelę według numeru konta i daty.

Użyj innej tabeli do przechowywania oczekujących transakcji, które są wstawiane na bieżąco. Na koniec każdego okresu uruchom procedurę dodającą nieopublikowane transakcje do ostatniego salda zamknięcia rachunku, aby obliczyć nowe saldo. Oznacz oczekujące transakcje jako zaksięgowane lub sprawdź daty, aby ustalić, co jest nadal w toku.

W ten sposób masz możliwość obliczenia salda karty na żądanie, bez konieczności sumowania całej historii rachunku, a umieszczając ponowne obliczanie salda w specjalnej procedurze księgowania, możesz zapewnić, że bezpieczeństwo transakcji tego ponownego obliczenia jest ograniczone do jedno miejsce (a także ograniczyć bezpieczeństwo na stole bilansowym, aby tylko procedura księgowania mogła do niego pisać).

Następnie przechowuj tyle danych historycznych, ile jest to wymagane przez audyt, obsługę klienta i wymagania dotyczące wydajności.

db2
źródło
1
Tylko dwie krótkie notatki. Po pierwsze, to bardzo dobry opis podejścia do tworzenia migawek logów, które sugerowałem powyżej, i być może jaśniejsze niż byłem. (Głosowałem na ciebie). Po drugie, podejrzewam, że używasz tutaj terminu „opublikowany” nieco dziwnie, aby oznaczać „część salda końcowego”. Pod względem finansowym, księgowanie zwykle oznacza „pojawienie się w bieżącym saldzie księgi” i dlatego wydawało się, że warto to wyjaśnić, aby nie powodować zamieszania.
Chris Travers
Tak, prawdopodobnie brakuje mi wielu subtelności. Mówię tylko o tym, jak transakcje wydają się być „księgowane” na moim rachunku czekowym przy zamknięciu działalności, a saldo odpowiednio aktualizowane. Ale nie jestem księgową; Po prostu pracuję z kilkoma z nich.
db2
Może to być również wymóg dla SOX itp. W przyszłości, nie wiem dokładnie, jakie wymagania dotyczące mikrotransakcji musisz zalogować, ale odrzuciłbym pytanie kogoś, kto wie, jakie są wymagania dotyczące raportowania później.
jcolebrand
Byłbym skłonny do przechowywania danych wieczystych np. Salda na początku każdego roku, tak aby migawka „sumy” nigdy nie została nadpisana - lista jest po prostu dołączana (nawet jeśli system pozostaje w użyciu wystarczająco długo, aby każde konto mogło gromadzić 1000 rocznych podsumowań [ BARDZO optymistycznych], które trudno byłoby zarządzać). Przechowywanie wielu rocznych sum pozwoliłoby kodowi kontroli na potwierdzenie, że transakcje między ostatnimi latami miały odpowiedni wpływ na sumy [poszczególne transakcje mogą zostać wyczyszczone po 5 latach, ale do tego czasu zostaną dobrze sprawdzone].
supercat
17

Z drugiej strony jest problem, z którym często spotykamy się w oprogramowaniu księgowym. Parafrazowane:

Czy naprawdę muszę agregować dziesięć lat danych, aby dowiedzieć się, ile pieniędzy jest na koncie czekowym?

Oczywiście odpowiedź brzmi „nie”. Jest tu kilka podejść. Jednym z nich jest zapisanie obliczonej wartości. Nie polecam tego podejścia, ponieważ błędy oprogramowania, które powodują nieprawidłowe wartości, są bardzo trudne do wyśledzenia, dlatego unikałbym tego.

Lepszym sposobem na to jest to, co nazywam podejściem log-snapshot-agregate. W tym podejściu nasze płatności i zastosowania są wstawkami i nigdy nie aktualizujemy tych wartości. Okresowo agregujemy dane w określonym przedziale czasu i wstawiamy obliczony rekord migawki, który reprezentuje dane w momencie, gdy migawka stała się ważna (zwykle okres czasu przed obecnym).

Teraz nie łamie to zasad Codda, ponieważ z czasem migawki mogą być mniej niż idealnie zależne od wstawionych danych dotyczących płatności / użytkowania. Jeśli mamy działające migawki, możemy zdecydować się na usunięcie 10-letnich danych bez wpływu na naszą zdolność do obliczania bieżących sald na żądanie.

Chris Travers
źródło
2
Mogę przechowywać obliczone sumy bieżące i jestem całkowicie bezpieczny - zaufane ograniczenia zapewniają, że moje liczby są zawsze poprawne: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/…
AK
1
W moim rozwiązaniu nie ma przypadków skrajnych - zaufane ograniczenie nie pozwoli ci niczego zapomnieć. Nie widzę żadnej praktycznej potrzeby na wartości NULL w prawdziwym systemie, który musi znać sumy całkowite - te rzeczy są ze sobą sprzeczne. Jeśli widzisz praktyczną potrzebę, podziel się swoim sceanrio.
AK
1
Ok, ale to nie zadziała, tak jak w bazach danych, które pozwalają na wiele wartości NULL bez naruszania wyjątkowości, prawda? Twoja gwarancja również się pogarsza, jeśli usuniesz dane z przeszłości, prawda?
Chris Travers
1
Na przykład, jeśli mam unikalne ograniczenie na (a, b) w PostgreSQL, mogę mieć wiele (1, null) wartości dla (a, b), ponieważ każdy null jest traktowany jako potencjalnie unikalny, co moim zdaniem jest semantycznie poprawne dla nieznanego wartości .....
Chris Travers
1
Jeśli chodzi o „Mam unikalne ograniczenie na (a, b) w PostgreSQL, mogę mieć wiele (1, null) wartości” - w PostgreSql musimy użyć unikalnego indeksu częściowego na (a), gdzie b jest null.
AK
7

Ze względów wydajnościowych w większości przypadków musimy przechowywać bieżące saldo - w przeciwnym razie obliczanie go w locie może ostatecznie stać się zbyt wolne.

W naszym systemie przechowujemy wstępnie obliczone sumy bieżące. Aby zagwarantować, że liczby są zawsze poprawne, stosujemy ograniczenia. Poniższe rozwiązanie zostało skopiowane z mojego bloga. Opisuje inwentarz, który jest zasadniczo tym samym problemem:

Obliczanie sum całkowitych jest notorycznie wolne, niezależnie od tego, czy robisz to za pomocą kursora, czy za pomocą połączenia trójkątnego. Denormalizacja jest bardzo kusząca, aby przechowywać bieżące sumy w kolumnie, szczególnie jeśli często ją wybierasz. Jednak, jak zwykle podczas denormalizacji, należy zagwarantować integralność zdormalizowanych danych. Na szczęście możesz zagwarantować integralność działających sum z ograniczeniami - dopóki wszystkie ograniczenia są zaufane, wszystkie działające sumy są poprawne. W ten sposób możesz łatwo upewnić się, że bieżące saldo (sumy bieżące) nigdy nie jest ujemne - egzekwowanie innymi metodami może być również bardzo wolne. Poniższy skrypt demonstruje 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
AK
źródło
Przyszło mi do głowy, że jednym z największych ograniczeń twojego podejścia jest to, że obliczanie salda konta w konkretnym historycznym dniu nadal wymaga agregacji, chyba że przyjmujesz również założenie, że wszystkie transakcje są wprowadzane sekwencyjnie według daty (co zwykle jest złe założenie).
Chris Travers
@ChrisTravers wszystkie bieżące sumy są zawsze aktualne, dla wszystkich historycznych dat. Ograniczenia to gwarantują. Tak więc agregacja nie jest wymagana dla żadnych historycznych dat. Jeśli musimy zaktualizować jakiś historyczny wiersz lub wstawić coś z datą wsteczną, aktualizujemy sumy bieżące wszystkich późniejszych wierszy. Myślę, że jest to o wiele łatwiejsze w postgreSql, ponieważ ma odroczone ograniczenia.
AK
6

To jest bardzo dobre pytanie.

Zakładając, że masz tabelę transakcji, która przechowuje każde polecenie zapłaty / kredyt, nie ma nic złego w twoim projekcie. W rzeczywistości pracowałem z przedpłaconymi systemami telekomunikacyjnymi, które działały dokładnie w ten sposób.

Najważniejsze, co musisz zrobić, to upewnić się, że robisz SELECT ... FOR UPDATEsaldo podczas INSERTdebetu / kredytu. Zapewni to prawidłowe saldo, jeśli coś pójdzie nie tak (ponieważ cała transakcja zostanie wycofana).

Jak zauważyli inni, będziesz potrzebować migawki sald w określonych przedziałach czasu, aby sprawdzić, czy wszystkie transakcje w danym okresie sumują się z saldami początku / końca okresu. Napisz zadanie wsadowe, które będzie uruchamiane o północy pod koniec okresu (miesiąc / tydzień / dzień), aby to zrobić.

Philᵀᴹ
źródło
4

Saldo jest obliczoną kwotą opartą na pewnych zasadach biznesowych, więc tak, nie chcesz zachować salda, a raczej obliczyć go z transakcji na karcie, a zatem rachunku.

Chcesz śledzić wszystkie transakcje na karcie do celów audytu i raportowania wyciągów, a nawet później danych z różnych systemów.

Konkluzja - oblicz wszystkie wartości, które należy obliczyć w dowolnym momencie

Stephen Senkomago Musoke
źródło
nawet jeśli mogą istnieć tysiące transakcji? Więc będę musiał to przeliczać za każdym razem? czy nie może być trochę ciężko pod względem wydajności? czy możesz dodać trochę o tym, dlaczego jest to taki problem?
Mithir,
2
@Mithir Ponieważ jest to sprzeczne z większością zasad rachunkowości i uniemożliwia śledzenie problemów. Jeśli po prostu zaktualizujesz bieżącą sumę, skąd wiesz, jakie korekty zostały zastosowane? Czy faktura została zaksięgowana raz czy dwa? Czy odjęliśmy już kwotę płatności? Jeśli śledzisz transakcje, znasz odpowiedzi, a jeśli śledzisz sumę - nie.
JNK
4
Odniesieniem do zasad Codda jest to, że łamie ono normalną formę. Zakładając, że śledzisz transakcje GDZIEKOLWIEK (które, jak sądzę, będziesz musiał), i masz osobną sumę bieżącą, co jest poprawne, jeśli się nie zgadzają? Potrzebujesz jednej wersji prawdy. Nie naprawiaj problemu z wydajnością, dopóki / o ile nie istnieje.
JNK
@JNK tak, jak jest teraz - zachowujemy transakcje i sumę, więc wszystko, o czym wspomniałeś, może być doskonale śledzone w razie potrzeby, Saldo całkowite ma tylko na celu uniemożliwienie nam ponownego obliczenia kwoty za każde działanie.
Mithir,
2
Teraz nie złamie zasad Codda, jeśli stare dane mogą być przechowywane tylko przez, powiedzmy, 5 lat, prawda? Saldo w tym momencie nie jest tylko sumą istniejących zapisów, ale także wcześniej istniejących zapisów od czasu wyczyszczenia, czy coś mi brakuje? Wydaje mi się, że złamałoby to zasady Codda tylko, jeśli założymy nieskończone zatrzymywanie danych, co jest mało prawdopodobne. Biorąc to pod uwagę z powodów, które mówię poniżej, myślę, że przechowywanie ciągle aktualizowanej wartości wymaga kłopotów.
Chris Travers