Tabloda güncellenen bir değeri saklamak doğru mudur?


31

Ön ödemeli kartlar için bir platform geliştiriyoruz; kartlar ve bunların bakiyesi, ödemeleri, vs.

Şimdiye kadar bir Hesap varlığı koleksiyonuna sahip bir Kart varlığına sahipti ve her Hesap, her Para Yatırma / Çekme işleminde güncellenen bir Tutar'a sahipti.

Şimdi takımda bir tartışma var; Birisi bize bunun Codd'un 12 Kurallarını ihlal ettiğini ve her ödemedeki değerini güncellemenin sorun olduğunu söyledi.

Bu gerçekten bir sorun mu?

Öyleyse, Bunu nasıl düzeltebiliriz?


3
Bu konuda burada DBA'da kapsamlı bir teknik tartışma var. SE: Basit bir banka şeması yazma
Nick Chammas,

1
Ekibiniz burada hangi Codd kurallarından bahsetti? Kurallar, ilişkisel bir sistemi tanımlama çabasıydı ve açıkça normalleşmeden bahsetmiyordu. Codd kitabında normalleşmeyi tartıştı Veritabanı yönetimi için ilişkisel model .
Iain Samuel McLean Elder,

Yanıtlar:


30

Evet, normalleştirilmemiş, ancak bazen normalleştirilmemiş tasarımlar performans nedeniyle kazanıyor.

Bununla birlikte, güvenlik nedenleriyle muhtemelen biraz daha farklı bir şekilde yaklaşacağım. (Feragatname: Şu anda finansal sektörde hiç çalışmadım ya da hiç çalışmadım. Bunu sadece oraya fırlatıyorum.)

Kartlara kaydedilen bakiyeler için bir tablo var. Bu, her bir hesap dönemi için, her dönemin kapanışındaki kaydedilen bakiyeyi (gün, hafta, ay veya uygun olanı) gösteren bir satır ekler. Bu tabloyu hesap numarası ve tarihe göre indeksleyin.

Anında yerleştirilmiş bekleyen işlemleri tutmak için başka bir tablo kullanın. Her dönemin kapanışında, yeni bakiyeyi hesaplamak için gönderilmemiş işlemleri hesabın son kapanış bakiyesine ekleyen bir rutin çalıştırınız. Beklemedeki işlemleri kaydedilmiş olarak işaretleyin veya hala beklemede olanı belirlemek için tarihlere bakın.

Bu şekilde, tüm hesap geçmişini toplamanıza gerek kalmadan talep üzerine bir kart bakiyesi hesaplamak için bir araca sahipsiniz ve bakiye yeniden hesaplamayı özel bir kayıt işlemine koyarak, bu yeniden hesaplamanın işlem güvenliğinin sınırlı kalmasını sağlayabilirsiniz. tek bir yer (ve aynı zamanda bilançodaki güvenliği de sınırlayın, böylece yalnızca kayıt rutini ona yazabilir).

Öyleyse, denetim, müşteri hizmetleri ve performans gereklilikleri gereği tarihi verileri saklayın.


1
Sadece iki hızlı not. Birincisi, yukarıda öne sürdüğüm log-aggregate-snapshot yaklaşımının çok iyi bir açıklaması ve belki de benden daha net. (Seni oyladı). İkincisi, burada "garip bir şekilde garip" gönderilen "terimini" kapanış bakiyesinin bir parçası "olarak kullandığınızdan şüpheleniyorum. Finansal açıdan bakıldığında, yayınlanan "genellikle genel muhasebe dengesinde görünmek" anlamına gelir ve bu nedenle karışıklığa yol açmayacağını açıklamaya değecek gibi görünüyordu.
Chris,

Evet, muhtemelen kaçırdığım birçok incelik var. İşlerin kapandığı sırada çek hesabıma işlemlerin nasıl "kaydedildiğini" ve buna göre güncellenen bakiyeyi kastediyorum. Ama ben bir muhasebeci değilim; Sadece onlardan biriyle çalışıyorum.
db2

Bu olabilir de ileride böyle bir SOX gereklilik veya ben tür bir giriş yapmak zorunda mikro işlem şartlarının tam olarak ne olduğunu bilmiyorum, ama ben defo raporlama gereksinimleri sonrası için ne bilen birisi sorardı.
jcolebrand

Her yılın başlangıcında bakiye gibi kalıcı veri tutma eğiliminde olurdum, böylece "toplamlar" anlık görüntüsünün üzerine asla yazılmaz - liste sadece eklenir (sistem her hesap için yeterince uzun süre kullanılsa bile) yönetilmesi zor olan 1000 yıllık toplamları [ ÇOK iyimser] birikir . Yıllık toplamların tutulması, denetim kodunun, son yıllar arasındaki işlemlerin toplamlar üzerinde uygun etkileri olduğunu doğrulamasını sağlayacaktır (bireysel işlemler 5 yıl sonra temizlenebilir, ancak o zamanlar iyi değerlendirilir).
supercat

17

Diğer taraftan, muhasebe yazılımlarında sıkça karşılaştığımız bir sorun var. Başka kelimelerle yazılmış:

Ben musunuz gerçekten çek hesabı içindedir ne kadar para bulmak için veri toplama on yıla ihtiyaç?

Tabii ki cevabınız hayır değil. Burada birkaç yaklaşım var. Biri hesaplanan değeri saklıyor. Bu yaklaşımı önermiyorum, çünkü yanlış değerlere neden olan yazılım hatalarının izini sürmek çok zor ve bu yüzden bu yaklaşımı önleyeceğim.

Bunu yapmanın daha iyi bir yolu, log-snapshot-aggregate yaklaşımı olarak adlandırmam. Bu yaklaşımda ödemelerimiz ve kullanımlarımız ek niteliğindedir ve bu değerleri asla güncellemeyiz. Periyodik olarak verileri belirli bir süre boyunca topluyoruz ve anlık görüntünün geçerli olduğu andaki verileri temsil eden hesaplanmış bir anlık görüntü kaydı ekliyoruz (genellikle mevcut zamandan önce bir süre ).

Şimdi bu, Codd kurallarını bozmaz çünkü zaman içinde anlık görüntüler eklenen ödeme / kullanım verilerine mükemmel şekilde bağlı olmayabilir. Çalışan anlık görüntülere sahipsek, talep üzerine mevcut bakiyeleri hesaplama yeteneğimizi etkilemeden 10 yıllık verileri temizlemeye karar verebiliriz.


2
Hesaplanan koşu toplamlarını saklayabilirim ve tamamen güvenliyim - güvenilen kısıtlamalar numaralarımın her zaman doğru olmasını sağlıyor: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/…
AK

1
Çözümümde son vakalar yok - güvenilir bir kısıtlama hiçbir şeyi unutmanıza izin vermez. Toplamları bilmeyi gerektiren gerçek bir yaşam sisteminde NULL miktarlarına herhangi bir pratik ihtiyaç görmüyorum - bunlar birbirleriyle çelişen şeyler için. Pratik bir ihtiyaç görürseniz, lütfen sceanrio'nuzu paylaşın.
AK

1
Tamam, ama o zaman bu, benzersizliği ihlal etmeden birden fazla NULL'a izin veren db'lerde olduğu gibi çalışmayacak, değil mi? Ayrıca geçmiş verileri temizlerseniz güvenceniz de bozulur, değil mi?
Chris,

1
Örneğin, PostgreSQL'de (a, b) konusunda benzersiz bir kısıtlamam varsa, (a, b) için birden fazla (1, null) değere sahip olabilirim çünkü her bir null potansiyel olarak benzersiz olarak kabul edilir, ki bunun bilinmeyen için anlamsal olarak doğru olduğunu düşünüyorum değerler .....
Chris Travers

1
"PostgreSQL'de (a, b) konusunda benzersiz bir kısıtlamam var, birden fazla (1, null) değere sahip olabilirim" ile ilgili olarak - PostgreSql'de (a) 'da b' nin boş olduğu konusunda benzersiz bir kısmi indeks kullanmamız gerekir.
AK,

7

Performans nedenleriyle, çoğu durumda mevcut dengeyi kaydetmeliyiz - aksi halde anında hesaplanması son derece yavaşlayabilir.

Önceden hesaplanmış koşu toplamlarını sistemimizde depolarız. Sayıların her zaman doğru olduğunu garanti etmek için kısıtlamalar kullanırız. Aşağıdaki çözüm blogumdan kopyalandı. Temelde aynı problem olan bir envanteri tanımlar:

Akan toplamları hesaplamak, imleçle veya üçgen birleştirme ile yapsanız bile, oldukça yavaş. Özellikle sık sık seçtiyseniz, bir sütunda akan toplamları saklamak denormalize etmek için çok caziptir. Bununla birlikte, normalize ettiğiniz zamanki gibi normalleştirilen verilerinizin bütünlüğünü garanti etmeniz gerekir. Neyse ki, toplam çalışanların kısıtlamalarla bütünlüğünü garanti edebilirsiniz - tüm kısıtlamalarınıza güvenildiği sürece, tüm çalışan toplamlarınız doğrudur. Ayrıca bu yolla, cari bakiyenizin (çalışan toplamların) asla negatif olmadığından kolayca emin olabilirsiniz - başka yöntemlerle zorlamak da çok yavaş olabilir. Aşağıdaki komut dosyası tekniği göstermektedir.

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

Yaklaşımınızın en büyük sınırlarından birinin, belirli bir tarihsel tarihte bir hesap bakiyesinin hesaplanmasının, tüm işlemlerin tarihe göre sıralı olarak girildiğini varsaymadığınız sürece (genellikle kötü olan varsayım).
Chris

@ChrisTravers tüm koşu toplamları, tüm tarihi tarihler için daima günceldir. Kısıtlamalar bunu garanti eder. Bu nedenle, herhangi bir tarihi tarih için toplulaştırmaya gerek yoktur. Bazı tarihi satırları güncellememiz veya geçmişe ait bir şeyler eklememiz gerekirse, sonraki tüm satırların toplamlarını güncelleriz. PostgreSql'de bunun daha kolay olduğunu düşünüyorum, çünkü ertelenmiş kısıtlamaları var.
AK

6

Bu çok iyi bir soru.

Her bir borç / krediyi depolayan bir işlem tablonuz olduğunu varsayarsak, tasarımınızda yanlış bir şey yoktur. Aslında, tam olarak bu şekilde çalışan ön ödemeli telco sistemleri ile çalıştım.

Yapmanız gereken asıl şey , borç / alacak SELECT ... FOR UPDATEvarken bakiyenizden birisini yaptığınızdan emin olmaktır INSERT. Bir şeyler ters giderse doğru dengeyi garanti edersiniz (çünkü tüm işlem geri alınacaktır).

Diğerlerinin de belirttiği gibi, belirli bir dönemdeki tüm işlemlerin dönem başlangıç ​​/ bitiş bakiyeleri ile doğru şekilde toplandığını doğrulamak için belirli bir zaman diliminde bakiyelerin anlık görüntüsüne ihtiyacınız olacaktır. Bunu yapmak için dönem sonunda gece yarısında çalışan bir toplu iş yazın (ay / hafta / gün).


4

Bakiye, belli iş kurallarına dayanan hesaplanmış bir meblağdır, bu nedenle evet, bakiyeyi korumak istemez, bunun yerine karttaki işlemlerden ve dolayısıyla hesaptan hesaplamanız gerekir.

Karttaki tüm işlemlerin denetim ve beyan raporlama işlemlerini ve hatta daha sonra farklı sistemlerden gelen verileri takip etmek istersiniz.

Alt satır - İhtiyaç duyduğunuzda ve gerektiğinde hesaplanması gereken değerleri hesaplayın.


1000lerce işlem olsa bile mi? Yani her seferinde yeniden hesaplamaya ihtiyacım olacak? performans konusunda biraz zor olamaz mı? Bunun neden böyle bir sorun olduğu hakkında biraz ekler misiniz?
Mithir

2
@Mithir Çünkü muhasebe kurallarının çoğuna aykırıdır ve izlenmesi imkansız kılar. Sadece toplamı güncellerseniz, hangi ayarların yapıldığını nasıl bildiniz? Bu fatura bir ya da iki kere alacaklandı mı? Ödeme tutarını zaten düşürdük mü? İşlemleri izlerseniz cevapları bilirsiniz, toplamı izlerseniz bilmezsiniz.
JNK,

4
Codd kurallarına atıfta bulunarak normal formunu kırar. SORU YERDE (hangisini düşünmek zorunda kalacağınız) işlemleri izlediğinizi varsayalım ve ayrı ayrı bir toplam çalışıyorsunuz, hangileri aynı fikirde değilse? Gerçeğin tek bir versiyonuna ihtiyacınız var. Gerçekte varolmadığı sürece / ile ilgili performans sorununu çözmeyin.
JNK,

Şu anda olduğu gibi @JNK - işlemleri tutarız ve toplam tutarız, bu sayede bahsettiğiniz her şey gerektiğinde mükemmel bir şekilde izlenebilir, Toplam bakiye sadece her işlemin tutarını yeniden hesaplamamızı engellemek içindir.
Mithir

2
Şimdi, eğer eski veriler sadece 5 yıl boyunca saklanabilirse, Codd kurallarını çiğnemez, değil mi? Bu noktadaki denge sadece mevcut kayıtların toplamı değil, aynı zamanda daha önce temizlenmiş olduğundan daha önce var olan kayıtların toplamı mı yoksa bir şeyleri mi eksik? Bana öyle geliyor ki, yalnızca olası bir veri saklama olduğunu kabul edersek, sadece Codd kurallarını ihlal edecekti. Bu, aşağıda söylediğim nedenlerden dolayı söyleniyor, sürekli güncellenen bir değer kaydetmenin sorun istediğini düşünüyorum.
Chris Travers
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.