Apakah boleh menyimpan nilai yang diperbarui dalam tabel?

31

Kami sedang mengembangkan platform untuk kartu prabayar, yang pada dasarnya menyimpan data tentang kartu dan saldo, pembayaran, dll.

Hingga kini kami memiliki entitas Kartu yang memiliki koleksi entitas Akun, dan setiap Akun memiliki Jumlah, yang diperbarui di setiap Setoran / Penarikan.

Sekarang ada perdebatan di tim; seseorang telah memberi tahu kami bahwa ini melanggar 12 Aturan Codd dan memperbarui nilainya pada setiap pembayaran adalah masalah.

Apakah ini benar-benar masalah?

Jika ya, Bagaimana kita bisa memperbaikinya?

Mithir
sumber
3
Ada diskusi teknis yang luas tentang topik ini di DBA.SE: Menulis skema bank sederhana
Nick Chammas
1
Manakah dari aturan Codd yang dikutip tim Anda di sini? Aturan adalah upayanya untuk mendefinisikan sistem relasional, dan tidak menyebutkan normalisasi secara eksplisit. Codd memang membahas normalisasi dalam bukunya Model relasional untuk manajemen basis data .
Iain Samuel McLean Penatua

Jawaban:

30

Ya, itu non-normalisasi, tetapi kadang-kadang desain non-normalisasi menang karena alasan kinerja.

Namun, saya mungkin akan mendekatinya sedikit berbeda, untuk alasan keamanan. (Penafian: Saat ini saya tidak, saya juga tidak pernah bekerja di sektor keuangan. Saya hanya membuang ini di sana.)

Siapkan tabel untuk saldo yang diposting pada kartu. Ini akan memiliki baris yang dimasukkan untuk setiap akun, menunjukkan saldo yang diposting pada penutupan setiap periode (hari, minggu, bulan, atau apa pun yang sesuai). Indeks tabel ini berdasarkan nomor dan tanggal akun.

Gunakan tabel lain untuk menahan transaksi yang tertunda, yang dimasukkan dengan cepat. Pada penutupan setiap periode, jalankan rutin yang menambahkan transaksi yang belum diposkan ke saldo akhir akun untuk menghitung saldo baru. Entah menandai transaksi yang tertunda sebagai diposting, atau lihat tanggal untuk menentukan apa yang masih tertunda.

Dengan cara ini, Anda dapat menghitung saldo kartu sesuai permintaan, tanpa harus menjumlahkan semua riwayat akun, dan dengan menempatkan perhitungan ulang saldo dalam rutin posting khusus, Anda dapat memastikan bahwa keamanan transaksi perhitungan ulang ini terbatas untuk satu tempat (dan juga membatasi keamanan di tabel saldo sehingga hanya rutin posting yang dapat menulisnya).

Kemudian simpan data historis sebanyak yang diperlukan oleh audit, layanan pelanggan, dan persyaratan kinerja.

db2
sumber
1
Hanya dua catatan cepat. Pertama itu deskripsi yang sangat bagus tentang pendekatan log-agregat-snapshot yang saya sarankan di atas, dan mungkin lebih jelas daripada saya. (Terpilih untuk Anda). Kedua, saya curiga Anda menggunakan istilah "diposting" agak aneh di sini, berarti "bagian dari saldo akhir." Dalam istilah keuangan, diposting biasanya berarti "muncul dalam saldo buku besar saat ini" dan sepertinya layak dijelaskan sehingga tidak menimbulkan kebingungan.
Chris Travers
Ya, mungkin ada banyak kehalusan yang saya lewatkan. Saya hanya merujuk pada bagaimana transaksi tampaknya "diposting" ke rekening giro saya pada akhir bisnis, dan saldo diperbarui sesuai. Tapi saya bukan seorang akuntan; Saya hanya bekerja dengan beberapa dari mereka.
db2
Ini mungkin juga persyaratan untuk SOX atau sejenisnya di masa depan, saya tidak tahu persis seperti apa persyaratan transaksi mikro yang harus Anda catat, tetapi saya akan bertanya kepada seseorang yang tahu apa persyaratan pelaporan untuk nanti.
jcolebrand
Saya akan cenderung menyimpan data abadi misalnya saldo pada awal setiap tahun, sehingga snapshot "total" tidak pernah ditimpa - daftar hanya ditambahkan (bahkan jika sistem tetap digunakan cukup lama untuk setiap akun untuk mengakumulasi 1.000 total tahunan [ SANGAT optimis], yang hampir tidak bisa dikelola). Menjaga total tahunan akan memungkinkan kode audit untuk mengkonfirmasi bahwa transaksi antara tahun-tahun terakhir memiliki efek yang tepat pada total [transaksi individu mungkin dibersihkan setelah 5 tahun, tetapi akan diperiksa dengan baik pada saat itu].
supercat
17

Di sisi lain, ada masalah yang sering kita hadapi dalam perangkat lunak akuntansi. Diparafrasekan:

Apakah saya benar - benar perlu mengumpulkan data sepuluh tahun untuk mencari tahu berapa banyak uang dalam rekening giro?

Jawabannya tentu saja bukan Anda tidak. Ada beberapa pendekatan di sini. Salah satunya adalah menyimpan nilai yang dihitung. Saya tidak merekomendasikan pendekatan ini karena bug perangkat lunak yang menyebabkan nilai yang salah sangat sulit dilacak sehingga saya akan menghindari pendekatan ini.

Cara yang lebih baik untuk melakukannya adalah apa yang saya sebut pendekatan log-snapshot-agregat. Dalam pendekatan ini, pembayaran dan penggunaan kami adalah sisipan dan kami tidak pernah memperbarui nilai-nilai ini. Secara berkala kami mengumpulkan data selama periode waktu dan menyisipkan rekaman snapshot yang dihitung yang mewakili data pada saat snapshot menjadi valid (biasanya periode waktu sebelum sekarang).

Sekarang ini tidak melanggar aturan Codd karena seiring waktu snapshots mungkin kurang dari tergantung pada data pembayaran / penggunaan yang dimasukkan. Jika kami memiliki snapshot yang berfungsi, kami dapat memutuskan untuk membersihkan data berusia 10 tahun tanpa memengaruhi kemampuan kami untuk menghitung saldo saat ini berdasarkan permintaan.

Chris Travers
sumber
2
Saya dapat menyimpan total penghitungan yang berjalan, dan saya sangat aman - batasan tepercaya memastikan angka saya selalu benar: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/…
AK
1
Tidak ada kasus tepi dalam solusi saya - kendala tepercaya tidak akan membiarkan Anda melupakan apa pun. Saya tidak melihat kebutuhan praktis untuk jumlah NULL dalam sistem kehidupan nyata yang perlu tahu total berjalan - ini untuk hal-hal yang saling bertentangan. Jika Anda melihat kebutuhan praktis, silakan bagikan sceanrio Anda.
AK
1
Ok, tapi kemudian ini tidak akan berfungsi seperti pada db yang memungkinkan beberapa NULL tanpa melanggar keunikan, kan? Juga jaminan Anda menjadi buruk jika Anda membersihkan data masa lalu, bukan?
Chris Travers
1
Sebagai contoh jika saya memiliki batasan unik pada (a, b) di PostgreSQL, saya dapat memiliki beberapa (1, null) nilai untuk (a, b) karena setiap null diperlakukan sebagai berpotensi unik, yang menurut saya secara semantik benar untuk tidak diketahui nilai .....
Chris Travers
1
Mengenai "Saya memiliki batasan unik pada (a, b) di PostgreSQL, saya dapat memiliki beberapa (1, null) nilai" - dalam PostgreSql kita perlu menggunakan indeks parsial unik pada (a) di mana b adalah nol.
AK
7

Untuk alasan kinerja, dalam kebanyakan kasus, kita harus menyimpan saldo saat ini - jika tidak menghitungnya dengan cepat, pada akhirnya akan menjadi sangat lambat.

Kami menyimpan total total berjalan yang telah dihitung dalam sistem kami. Untuk menjamin bahwa angka selalu benar, kami menggunakan batasan. Solusi berikut telah disalin dari blog saya. Ini menggambarkan inventaris, yang pada dasarnya masalah yang sama:

Menghitung total yang berjalan sangat lambat, apakah Anda melakukannya dengan kursor atau gabungan segitiga. Sangat menggoda untuk melakukan denormalkan, untuk menyimpan total yang berjalan dalam kolom, terutama jika Anda sering memilihnya. Namun, seperti biasa ketika Anda melakukan denormalkan, Anda perlu menjamin integritas data yang didenormalisasi. Untungnya, Anda dapat menjamin integritas menjalankan total dengan kendala - selama semua kendala Anda tepercaya, semua total menjalankan Anda benar. Juga dengan cara ini Anda dapat dengan mudah memastikan bahwa saldo saat ini (total berjalan) tidak pernah negatif - menegakkan dengan metode lain juga bisa sangat lambat. Script berikut menunjukkan tekniknya.

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
sumber
Terpikir oleh saya bahwa salah satu batasan besar dari pendekatan Anda adalah bahwa menghitung saldo akun pada tanggal historis tertentu masih memerlukan agregasi, kecuali jika Anda juga membuat asumsi bahwa semua transaksi dimasukkan secara berurutan berdasarkan tanggal (yang biasanya buruk anggapan).
Chris Travers
@ChrisTravers semua total berjalan selalu terkini, untuk semua tanggal bersejarah. Kendala menjamin itu. Jadi tidak diperlukan agregasi untuk tanggal bersejarah apa pun. Jika kami harus memperbarui beberapa baris bersejarah, atau memasukkan sesuatu yang sudah ketinggalan zaman, kami memperbarui semua total baris yang berjalan nanti. Saya pikir ini jauh lebih mudah di postgreSql, karena memiliki kendala yang ditangguhkan.
AK
6

Ini pertanyaan yang sangat bagus.

Dengan asumsi bahwa Anda memiliki tabel transaksi yang menyimpan setiap debit / kredit, tidak ada yang salah dengan desain Anda. Bahkan, saya telah bekerja dengan sistem telekomunikasi prabayar yang telah bekerja dengan cara ini.

Hal utama yang perlu Anda lakukan adalah memastikan bahwa Anda melakukan SELECT ... FOR UPDATE saldo saat Anda INSERTmendebit / kredit. Ini akan menjamin saldo yang benar jika terjadi kesalahan (karena seluruh transaksi akan dibatalkan).

Seperti yang telah ditunjukkan orang lain, Anda akan memerlukan snapshot saldo pada periode waktu tertentu untuk memverifikasi bahwa semua transaksi dalam jumlah periode tertentu dengan periode mulai / akhir saldo dengan benar. Tulis pekerjaan batch yang berjalan pada tengah malam di akhir periode (bulan / minggu / hari) untuk melakukan ini.

Philᵀᴹ
sumber
4

Saldo adalah jumlah yang dihitung berdasarkan aturan bisnis tertentu, jadi ya Anda tidak ingin menyimpan saldo tetapi menghitungnya dari transaksi pada kartu dan karenanya akun.

Anda ingin melacak semua transaksi pada kartu untuk audit dan pelaporan pernyataan, dan bahkan data dari sistem yang berbeda di kemudian hari.

Intinya - hitung nilai apa saja yang perlu dihitung seperti dan ketika Anda perlu

Stephen Senkomago Musoke
sumber
bahkan jika mungkin ada 1000-an transaksi? Jadi saya harus menghitung ulang setiap waktu? tidakkah itu bisa sedikit sulit pada kinerja? dapatkah Anda menambahkan sedikit tentang mengapa ini merupakan masalah?
Mithir
2
@Mithir Karena bertentangan dengan sebagian besar aturan akuntansi, dan membuat masalah tidak mungkin dilacak. Jika Anda hanya memperbarui total yang berjalan, bagaimana Anda tahu penyesuaian mana yang telah diterapkan? Apakah faktur itu dikreditkan sekali atau dua kali? Apakah kita sudah mengurangi jumlah pembayaran? Jika Anda melacak transaksi, Anda tahu jawabannya, jika Anda melacak totalnya, Anda tidak tahu.
JNK
4
Referensi aturan Codd adalah ia merusak form normal. Dengan asumsi Anda melacak transaksi di suatu tempat (yang Anda harus saya pikir), dan Anda memiliki total menjalankan terpisah, yang benar jika mereka tidak setuju? Anda membutuhkan satu versi kebenaran. Jangan memperbaiki masalah kinerja sampai / kecuali itu benar-benar ada.
JNK
@JNK seperti sekarang - kami melakukan transaksi dan total, sehingga semua yang Anda sebutkan dapat dilacak dengan sempurna jika diperlukan, total Saldo hanya untuk mencegah kami menghitung ulang jumlah setiap tindakan.
Mithir
2
Sekarang, itu tidak akan melanggar aturan Codd jika data lama hanya dapat disimpan selama, katakanlah 5 tahun, kan? Saldo pada titik itu bukan hanya jumlah dari catatan yang ada, tetapi juga catatan yang sudah ada sejak dibersihkan, atau apakah saya kehilangan sesuatu? Menurut saya itu hanya akan melanggar aturan Codd jika kita mengasumsikan retensi data tak terbatas, yang tidak mungkin. Ini dikatakan karena alasan yang saya katakan di bawah ini, saya pikir menyimpan nilai yang terus diperbarui meminta masalah.
Chris Travers