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

时间:2018-02-06 14:18:43

标签: sql database database-design

我试图为用户构建简单的应用程序来执行资金操作(相互转移硬币等)。我已经决定将帐户信息与用户数据分开,所以此刻我的架构 looks like this

现在我遇到了一个问题:用户应该能够始终看到他当前的平衡。此外,他还可以查看与他相关的操作列表(收入和支出),列表中的每个项目必须包含有关用户余额状态(操作后的硬币数量)的信息

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

  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 
    

    但是这个解决方案只是复制了除了之外的所有信息 平衡信息,也与解决方案编号1具有相同的问题。

  3. 有没有更简单,更好的方式来做我想要的?我错过了什么吗?

1 个答案:

答案 0 :(得分:1)

您在示例中描述的是交易表,它是常见的双重记账方案,对于2个实体之间的任何交易,或者帐户有借记< / strong>在一个帐户中输入,在另一个帐户中输入相应的信用,从而在交易表中生成2个条目。您肯定应该以某种方式将这两行链接在一起,或者使用相同的时间戳,或者通过事务头或物理事务的唯一内容,因此您可能有一个记录操作的Header表(可能称为操作)和Transaction表保存操作表的链接。这种结构可以轻松支持单一货币交易事件,该事件从单个账户获取资金并分配给许多账户。

  

理论上,您现在可以将任何给定帐户的交易表相加以查找当前余额,因此您无需将余额记录在任何位置作为特定存储字段

你很自然地找到了解决这个问题的方法,我不想在这个回答中过于技术化,因为这是一个非常广泛的主题。

理论上,如上所述,您可能不需要存储“当前”信息。平衡,但在实践中,除非你有一个好的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
  

上面的脚本可以安全运行,它使用表变量,所以不应该留下任何东西:)

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

+----+-------------+---------------------------------------------+---------+--------+---------+
| Id | OperationId | Description                                 | Account | Amount | Balance |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 1  | NULL        | Initial Balance                             | A       | 100.00 | 100.00  |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 2  | NULL        | Initial Balance                             | B       | 100.00 | 100.00  |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 3  | NULL        | Initial Balance                             | C       | 0.00   | 0.00    |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 4  | 1           | A pays B and C $25.60 for services rendered | A       | -51.20 | 48.80   |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 5  | 1           | A pays B and C $25.60 for services rendered | B       | 25.60  | 125.60  |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 6  | 1           | A pays B and C $25.60 for services rendered | C       | 25.60  | 25.60   |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 7  | NULL        | Buy lunch                                   | C       | -8.20  | 17.40   |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 8  | NULL        | Buy petrol                                  | A       | 40.00  | 88.80   |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 9  | NULL        | Sell Goods                                  | A       | 120.00 | 208.80  |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 10 | 2           | B lends $50 to C                            | B       | -50.00 | 75.60   |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 11 | 2           | B lends $50 to C                            | C       | 50.00  | 67.40   |
+----+-------------+---------------------------------------------+---------+--------+---------+
| 12 | NULL        | Get a pizza                                 | C       | -10.00 | 57.40   |
+----+-------------+---------------------------------------------+---------+--------+---------+