如何为用户资金操作构建正确的sql模式?

1 sql database database-design

我正在尝试构建简单的应用程序,供用户执行货币操作(相互转移硬币等)。我决定将帐户信息与用户数据分开,所以我此时的架构 如下所示

现在我有一个问题:用户应该能够一直看到他当前的余额。他还可以查看与他相关的操作(收入和支出)列表,列表中的每一项都必须包含该操作后用户余额状态(多少币)的信息

我只提出了两个解决方案,但从我的角度来看,它们都看起来很糟糕:

  1. 在“操作”表中创建“account_from_balance”和“account_to_balance”等字段。这里还存在其他问题 - 例如“操作”表中的余额和“user_account”表中的余额之间可能不同步
  2. 使用以下字段重新制作整个“操作”表:user_1、user_2、金额、类型(收入/费用)、user_1_balance

    关键是,例如,两个用户 A 和 B 各有 80 个币,并且用户 A 决定向用户 B 发送 30 个币,则将在“操作”表中创建接下来的两行:

    A B 30 expense 50 
    
    B A 30 income 110 
    
    Run Code Online (Sandbox Code Playgroud)

    但这个解决方案只是复制了除余额信息之外的所有信息,并且也存在与解决方案 1 相同的问题。


有没有更简单、更好的方法来做我想做的事?我错过了什么吗?

Chr*_*ler 6

您在示例中描述的是一个交易表,它是一种常见的复式记账方案,对于两个实体或帐户之间的任何交易,一个帐户中有一个借方条目,另一个帐户中有相应的贷方条目,从而导致 2 个条目交易表。您绝对应该以某种方式将这两行链接在一起,或者使用相同的时间戳,或者通过事务标头或有关物理事务的唯一内容,因此您可能有一个标头表(可能称为操作),用于记录操作,以及事务表保存操作表的链接。这种结构可以轻松支持单个货币交易事件,从单个账户提取资金并分配给多个账户。

理论上,您现在可以对任何给定帐户的交易表求和以查找当前余额,因此您不需要在任何地方将余额记录为特定的存储字段

很高兴您自然地找到了解决此问题的方法,我不想在这个回答中涉及太多技术性内容,因为这是一个非常广泛的主题。

理论上,如上所述,您可能不需要存储“当前”余额,但在实践中,除非您有一个好的 RDBMS 引擎、好的索引和对必要语法的良好掌握,否则拥有一个相应的字段来保存累积的余额值,而不是为每个查询重新计算该值。

如果您确实尝试存储余额,则应确保您可以控制表的所有输入,触发器可能有用,应用程序逻辑是可以接受的,但要警惕计算字段,确保对它们进行编译以便对其进行评估关于写入操作,而不是读取操作...假设会有更多的读取与写入。


更新:关于each of the items in the list must contain information about the state of users balance (how much coins) after that operation.

根据您选择的 RDBMS,应该有计算读取运行总计的标准机制,以便您无需记录它们。您必须权衡存储运行总计或计算运行总计对性能、存储成本和维护的影响,以下是为 SQL Server 2012(及更高版本)设计的示例,并使用窗口函数计算运行总计,保持

即使您存储了运行总计,您也可能会发现窗口函数对于定期回填或审核交易以确保余额字段正确非常有用。

-- Example Operation Header Table
DECLARE @Operation as Table
(
    Id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY,
    TxDate DateTimeOffset(7) NOT NULL DEFAULT(SysDateTimeOffset()),
    [Description] char(120) NOT NULL
)

-- Example Transaction Table
DECLARE @Transaction as Table
(
    Id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY,
    OperationId bigint NULL,
    TxDate DateTimeOffset(7) NOT NULL DEFAULT(SysDateTimeOffset()),
    [Description] char(120) NOT NULL,
    Account char(1) NOT NULL,
    Amount MONEY NOT NULL
)

-- Insert Starting balances
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('A','Initial Balance',100.00);
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('B','Initial Balance',100.00);
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('C','Initial Balance',0.00);

-- INSERT Some Transactions
DECLARE @opId bigint;
INSERT INTO @Operation ([Description]) VALUES ('A pays B and C $25.60 for services rendered')
SELECT @opId = SCOPE_IDENTITY()
INSERT INTO @Transaction (OperationId, Account,[Description],Amount) VALUES (@opId,'A','A pays B and C $25.60 for services rendered',-51.20);
INSERT INTO @Transaction (OperationId, Account,[Description],Amount) VALUES (@opId,'B','A pays B and C $25.60 for services rendered',25.60);
INSERT INTO @Transaction (OperationId, Account,[Description],Amount) VALUES (@opId,'C','A pays B and C $25.60 for services rendered',25.60);

INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('C','Buy lunch',-8.20);
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('A','Buy petrol',40.00);
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('A','Sell Goods',120.00);

INSERT INTO @Operation ([Description]) VALUES ('B lends $50 to C')
SELECT @opId = SCOPE_IDENTITY()
INSERT INTO @Transaction (OperationId, Account,[Description],Amount) VALUES (@opId,'B','B lends $50 to C',-50);
INSERT INTO @Transaction (OperationId, Account,[Description],Amount) VALUES (@opId,'C','B lends $50 to C',50);

-- Example of checking current balance on the spot
DECLARE @balance MONEY = (SELECT SUM(Amount) FROM @Transaction WHERE Account = 'C')
SELECT @balance as 'C Account Balance'
if(@balance > 80)
    INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('C','Go out for dinner',-80);
else
    INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('C','Get a pizza',-10);


SELECT * from @Operation
SELECT * from @Transaction

-- Current Balance of all accounts
SELECT Account, Balance = SUM(Amount)
FROM @Transaction
GROUP BY Account

-- Running Balance with Transactions
SELECT t.*, Balance = SUM(Amount) OVER(Partition By Account ORDER BY Id ROWS UNBOUNDED PRECEDING) 
FROM @Transaction t
ORDER BY Id

-- Running Balance, just for Account=C
SELECT t.*, Balance = SUM(Amount) OVER(Partition By Account ORDER BY Id ROWS UNBOUNDED PRECEDING) 
FROM @Transaction t
WHERE Account = 'C'
ORDER BY Id
Run Code Online (Sandbox Code Playgroud)

上面的脚本可以安全运行,它使用表变量,因此不应留下任何内容:)

最终,这会导致所有交易的以下输出(我省略了 TxDate 列,在本示例中,日期全部相同,因此没有用):

+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 身份证 | 操作 ID | 描述 | 账户 | 金额 | 平衡|
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 1 | 空 | 初始余额 | 一个 | 100.00 | 100.00 |
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 2 | 空 | 初始余额 | 乙| 100.00 | 100.00 |
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 3 | 空 | 初始余额 | C | 0.00 | 0.00 0.00 | 0.00
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 4 | 1 | A 向 B 和 C 提供的服务支付 25.60 美元 | 一个 | -51.20 | -51.20 48.80 |
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 5 | 1 | A 向 B 和 C 提供的服务支付 25.60 美元 | 乙| 25.60 | 25.60 125.60 | 125.60
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 6 | 1 | A 向 B 和 C 提供的服务支付 25.60 美元 | C | 25.60 | 25.60 25.60 | 25.60
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 7 | 空 | 买午餐| C | -8.20 | 17.40 | 17.40
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 8 | 空 | 购买汽油 | 一个 | 40.00 | 88.80 |
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 9 | 空 | 销售商品 | 一个 | 120.00 | 208.80 | 208.80
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 10 | 10 2 | B 借给 C 50 美元 | 乙| -50.00 | 75.60 | 75.60
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 11 | 11 2 | B 借给 C 50 美元 | C | 50.00 | 67.40 | 67.40
+----+-------------+---------------------------- ---------------+---------+--------+---------+
| 12 | 12 空 | 吃个披萨| C | -10.00 | 57.40 | 57.40
+----+-------------+---------------------------- ---------------+---------+--------+---------+