테이블에서 업데이트되는 값을 유지해도 괜찮습니까?


31

우리는 선불 카드 플랫폼을 개발하고 있으며 기본적으로 카드와 그 잔액, 지불 등에 관한 데이터를 보유하고 있습니다.

지금까지 우리는 계좌 엔터티가있는 카드 엔터티를 가지고 있었고 각 계좌에는 금액이 있으며 모든 예금 / 인출에서 업데이트됩니다.

이제 팀에 토론이 있습니다. 누군가 이것이 이것이 Codd의 12 규칙을 어 기고 각 지불에 대한 가치를 업데이트하는 것은 문제 가 있다고 우리에게 말했습니다 .

이것이 정말로 문제입니까?

그렇다면 어떻게 해결할 수 있습니까?


3
이 주제에 대한 광범위한 기술적 인 논의는 DBA.SE 여기에 있습니다 : 간단한 은행 스키마 작성
닉 Chammas

1
Codd의 규칙 중 어느 팀이 여기에 인용 했습니까? 규칙은 관계형 시스템을 정의하려는 시도였으며 정규화는 명시 적으로 언급하지 않았습니다. Codd는 그의 저서 데이터베이스 관리를위한 관계형 모델 에서 정규화에 대해 논의했습니다 .
Iain Samuel McLean 장로

답변:


30

예, 정규화되지 않았지만 때로는 정규화되지 않은 디자인이 성능상의 이유로 이길 수 있습니다.

그러나 안전상의 이유로 조금 다르게 접근 할 것입니다. (면책 조항 : 저는 현재 금융 분야에서 일한 적이 없으며 금융 분야에서 일한 적이 없습니다. 그냥 버리고 있습니다.)

카드에 게시 된 잔액에 대한 표를 준비하십시오. 각 계정에 대해 행이 삽입되어 각 기간이 끝날 때 (일, 주, 월 또는 적절한 것) 전기 된 잔액을 나타냅니다. 계좌 번호와 날짜별로이 테이블을 색인화하십시오.

보류중인 트랜잭션을 보류하기 위해 다른 테이블을 사용하십시오. 각 기간이 끝날 때 전기되지 않은 거래를 계정의 마지막 결산 잔액에 추가하여 새 잔액을 계산하는 루틴을 실행하십시오. 보류중인 거래를 전기 된 것으로 표시하거나 날짜를보고 아직 보류중인 항목을 판별하십시오.

이렇게하면 모든 계정 내역을 요약하지 않고도 주문형 카드 잔액을 계산할 수 있으며, 잔액 재 계산을 전용 전기 루틴으로 설정하면이 재 계산의 거래 안전성이 다음과 같이 제한됩니다. 단일 장소 (또한 전기 루틴 만 쓸 수 있도록 잔액 테이블의 보안을 제한 함).

그런 다음 감사, 고객 서비스 및 성능 요구 사항에 필요한만큼의 과거 데이터를 유지하십시오.


1
두 개의 간단한 메모입니다. 첫째, 위에서 제안한 로그 집계 스냅 샷 방식에 대한 아주 좋은 설명입니다. (당신을 찬성했습니다). 두 번째로, "종결 잔액의 일부"를 의미하기 위해 "게시 됨"이라는 용어를 여기서 이상하게 사용하고 있다고 생각합니다. 재정적 인 용어로 게시 된 것은 일반적으로 "현재 원장 잔액에 표시"를 의미하므로 혼란을 유발하지 않는 것으로 설명 할 가치가있는 것처럼 보입니다.
Chris Travers

네, 제가 놓치고있는 미묘한 부분이 많이있을 것입니다. 거래가 끝날 때 거래가 내 당좌 계좌에 "게시 된"것처럼 보이는데 잔액이 그에 따라 업데이트되었습니다. 그러나 저는 회계사가 아닙니다. 나는 그들 중 몇 명과 함께 일합니다.
db2

이것은 향후 SOX 등에 대한 요구 사항 일 수도 있습니다. 어떤 종류의 소액 결제 요구 사항을 정확하게 기록해야할지 모르겠지만 나중에보고 요구 사항이 무엇인지 아는 사람에게 물을 것입니다.
jcolebrand

매년 초에 균형을 유지하기 위해 영구적 인 데이터를 유지하려는 경향이있어 "총계"스냅 샷을 절대 덮어 쓰지 않습니다. 목록이 단순히 추가됩니다 (시스템이 각 계정에 대해 오랫동안 오래 사용 된 경우에도) 1,000 년간 총계 [ VERY optimistic]는 누적하기 어렵습니다 ( 매우 관리하기 어렵습니다). 연간 총계를 많이 유지하면 감사 코드에서 최근 몇 년 간의 트랜잭션이 총계에 적절한 영향을 미쳤음을 확인할 수 있습니다 (개별 트랜잭션은 5 년 후에 제거 될 수 있지만 그 당시에는 잘 검증 될 것임).
supercat

17

반면에 회계 소프트웨어에서 자주 문제가 발생합니다. 해석 :

나는 마십시오 정말 점검 계정에 얼마나 많은 돈을 찾아 데이터의 집계 십 년이 필요하십니까?

물론 대답은 아닙니다. 여기에는 몇 가지 접근 방식이 있습니다. 하나는 계산 된 값을 저장하는 것입니다. 잘못된 값을 유발하는 소프트웨어 버그를 추적하기가 매우 어려우므로이 방법을 사용하지 않는 것이 좋습니다.

그것을하는 더 좋은 방법은 내가 log-snapshot-aggregate 접근법이라고 부르는 것입니다. 이 접근 방식에서 지불 및 사용은 삽입이며 이러한 값은 업데이트되지 않습니다. 주기적으로 데이터를 집계하고 스냅 샷이 유효한 시간 (일반적으로 현재 이전 의 시간 ) 을 나타내는 계산 된 스냅 샷 레코드를 삽입합니다 .

이제 시간이 지남에 따라 스냅 샷이 삽입 된 지불 / 사용 데이터에 완전히 의존하지 않기 때문에 Codd의 규칙을 위반하지 않습니다. 작업중인 스냅 샷이있는 경우 현재 주문형 잔액을 계산하는 기능에 영향을주지 않으면 서 10 년 된 데이터를 제거하기로 결정할 수 있습니다.


2
계산 된 누적 합계를 저장할 수 있으며 완벽하게 안전합니다. 신뢰할 수있는 제한 조건으로 인해 항상 정확한 숫자를 확인할 수 있습니다. sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/…
AK

1
내 솔루션에는 특별한 경우가 없습니다. 신뢰할 수있는 제약 조건으로 인해 아무것도 잊을 수 없습니다. 누적 합계를 알아야하는 실제 시스템에서 NULL 수량에 대한 실질적인 요구는 없습니다. 실제적인 필요가있는 경우, 귀하의 시나리오를 공유하십시오.
AK

1
좋아, 그러나 이것은 고유성을 위반하지 않고 여러 NULL을 허용하는 db에서와 같이 작동하지 않습니다. 또한 과거 데이터를 제거하면 보증이 잘못됩니다.
Chris Travers

1
예를 들어 PostgreSQL에서 (a, b)에 대한 고유 제약 조건이있는 경우 각 null이 잠재적으로 고유 한 것으로 취급되기 때문에 (a, b)에 대해 여러 (1, null) 값을 가질 수 있습니다. 값 .....
크리스 가로 질러

1
"PostgreSQL의 (a, b)에 대한 고유 제약 조건이 있습니다. 여러 개의 (1, null) 값을 가질 수 있습니다"-PostgreSql에서는 (a)에 b가 null 인 고유 부분 인덱스를 사용해야합니다.
AK

7

성능상의 이유로 대부분의 경우 현재 잔액을 저장해야합니다. 그렇지 않으면 즉석에서 계산할 때 결과적으로 엄청나게 느려질 수 있습니다.

우리는 미리 계산 된 누적 합계를 시스템에 저장합니다. 숫자가 항상 올바른지 확인하기 위해 제약 조건을 사용합니다. 내 블로그에서 다음 솔루션이 복사되었습니다. 본질적으로 동일한 문제인 인벤토리를 설명합니다.

커서를 사용하거나 삼각 조인을 사용하여 누적 합계를 계산하면 속도가 느려집니다. 특히 자주 선택하는 경우 누적 합계를 열에 저장하는 것은 비정규 화하는 것이 매우 유혹적입니다. 그러나 비정규화할 때 평소와 같이 비정규 화 된 데이터의 무결성을 보장해야합니다. 다행히 모든 제약 조건을 신뢰할 수있는 한 모든 누적 합계가 올바른 경우 제약 조건을 적용하여 누적 합계의 무결성을 보장 할 수 있습니다. 또한이 방법을 사용하면 현재 잔액 (총계)이 절대로 음수가되지 않도록 쉽게 확인할 수 있습니다. 다른 방법으로 시행하는 것도 매우 느릴 수 있습니다. 다음 스크립트는이 기술을 보여줍니다.

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

접근 방식의 가장 큰 한계 중 하나는 특정 거래 날짜에 계정 잔액을 계산할 때 여전히 집계가 필요하다는 것입니다. 모든 거래가 날짜별로 순차적으로 입력된다고 가정하지 않는 한 인수).
Chris Travers

@ChrisTravers 모든 누적 합계는 모든 역사적인 날짜에 대해 항상 최신 상태입니다. 제약 조건이 보장됩니다. 따라서 역사적인 날짜에 대한 집계가 필요하지 않습니다. 과거 행을 업데이트하거나 이전 날짜를 삽입해야하는 경우 모든 이후 행의 누적 합계를 업데이트합니다. postgreSql에서는 제약 조건이 지연 되었기 때문에 이것이 훨씬 쉽다고 생각합니다.
AK

6

이것은 매우 좋은 질문입니다.

각 직불 / 신용을 저장하는 거래 테이블이 있다고 가정하면 디자인에는 아무런 문제가 없습니다. 사실, 나는 정확히 이런 방식으로 작동하는 선불 통신 시스템을 사용했습니다.

당신이해야 할 가장 중요한 것은 당신 이 차변 / 신용 SELECT ... FOR UPDATE동안 균형을 유지하는 것 INSERT입니다. 전체 거래가 롤백되기 때문에 문제가 발생하면 올바른 잔액을 보장합니다.

다른 사람들이 지적했듯이, 특정 기간의 모든 거래가 주어진 기간의 모든 거래가 기간 시작 / 종료 잔액과 올바르게 합쳐 졌는지 확인하려면 잔액 스냅 샷이 필요합니다. 이를 위해 기말 (월 / 주 / 일) 자정에 실행되는 배치 작업을 작성하십시오.


4

잔액은 특정 비즈니스 규칙에 따라 계산 된 금액이므로 잔액을 유지하지 않고 카드와 계좌의 거래에서 잔액을 계산해야합니다.

감사 및 명세서보고, 그리고 나중에 다른 시스템의 데이터까지 카드의 모든 트랜잭션을 추적하려고합니다.

결론-필요할 때와 계산해야 할 값을 계산하십시오.


1000 번의 거래가 있더라도? 매번 다시 계산해야합니까? 성능이 조금 어려울 수 있습니까? 이것이 왜 이런 문제인지에 대해 조금 추가 할 수 있습니까?
Mithir

2
@Mithir 대부분의 회계 규칙에 위배되므로 문제를 추적 할 수 없습니다. 누적 합계 만 업데이트하면 어떤 조정이 적용되었는지 어떻게 알 수 있습니까? 해당 인보이스가 한두 번 적립 되었습니까? 우리는 이미 지불 금액을 공제 했습니까? 당신이 거래를 추적하면 당신은 답변을 알고, 당신이 총계를 추적하면 당신은하지 않습니다.
JNK

4
Codd의 규칙에 대한 참조는 정상적인 형식을 위반한다는 것입니다. 당신이 (어쩌면 내가 생각해야 할) 거래를 추적한다고 가정하고, 당신이 동의하지 않으면 올바른 총계가 있습니까? 진실의 단일 버전이 필요합니다. 실제로 존재하지 않는 한 성능 문제를 해결하지 마십시오.
JNK

@JNK는 현재의 방식으로 거래와 총액을 유지하므로 필요한 경우 언급 한 모든 것을 완벽하게 추적 할 수 있습니다. 잔액 총액은 모든 조치 금액을 다시 계산하지 못하게하는 것입니다.
Mithir

2
이제 오래된 데이터가 5 년 동안 만 보존 될 수 있다면 Codd의 규칙을 어 기지 않을 것입니다. 이 시점의 잔액은 기존 레코드의 합계 일뿐 만 아니라 제거 된 이후의 기존 레코드도 합한 것입니까, 아니면 뭔가 빠졌습니까? 우리가 무한한 데이터 보존을 가정한다면 Codd의 규칙을 어기는 것 같습니다. 이것은 아래에서 말하는 이유 때문에 지속적으로 업데이트되는 값을 저장하면 문제가 발생한다고 생각합니다.
Chris Travers
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.