数据库设计:计算账户余额

98

如何设计数据库以计算账户余额?

1) 目前我是从交易表中计算账户余额。 在我的交易表中,有“描述”和“金额”等。

然后我将所有“金额”值相加,这就能计算出用户的账户余额。


我把这个方法告诉了我的朋友,他说这不是一个好的解决方案,因为当我的数据库变大时,它会变得很慢???? 他说我应该创建单独的表来存储计算出的账户余额。如果我这样做,我将不得不维护两个表,并且存在风险,即账户余额表可能会失去同步。

有什么建议吗?

编辑: 选项2:我是否应该向我的交易表添加一个名为“余额”的额外列? 现在我无需浏览许多数据行即可执行我的计算。

例如 John购买$100的信用额度,他欠款$60,然后添加$200的信用额度。

金额$100,余额$100。

金额-$60,余额$40。

金额$200,余额$240。


1
你预计的交易量是多少? - iDevlop
那个问题是什么鬼,为什么每个人都会得负分? - iDevlop
@iDevlop,回答你的问题...由于交易从前几年开始就没有被删除,我预计会有几百万条记录。 - 001
7
Transactions表中余额字段的问题在于,你不仅在行级别上建立了一个传递性依赖关系,这是不“正常”的,而且还在列级别上增加了一个传递性依赖关系,如果出现问题(触发器失败或其他问题),这将是一个头痛的问题。我的建议是先写下您归一化的结构,然后编写您计划拥有的每个“使用案例”,与他人讨论并根据您的用例审查结构,以查看是否需要某些反规范化。无论如何,设计阶段至关重要,在该点花费时间并不“浪费”时间! - iDevlop
7
“transactions are never deleted from the previous years"的意思是“以前年份的交易记录永远不会被删除”。但我认为这样做需要再三考虑。您可以考虑在一段时间后将旧交易移动到存档表中,并在活动表中创建一个特殊类型的交易(初始余额)。这可以作为每年(或任何适当的时间范围)的一部分进行处理。您应该在“使用案例”中包含这一点。 - iDevlop
显示剩余3条评论
9个回答

85

一个古老的问题,从未被优雅地解决过。

我使用过的所有银行软件包都将余额存储在账户实体中。从交易历史记录中动态计算余额是不可想象的。

正确的方式是:

  • 每个账户在“流水”表中都有一个“期初余额”交易。您需要它,因为几年后需要将旧交易从活动交易表中移出到历史记录表中。
  • 账户实体具有余额字段
  • “流水”表上有一个触发器,用于更新借方和贷方账户的余额。显然,它需要提交控制。如果无法使用触发器,则需要有一个唯一的模块,以提交控制方式写入“流水”
  • 您有一个“安全网”程序,可以脱机运行,重新计算所有余额,并显示(并可选地更正)错误余额。这对测试非常有用。

有些系统将所有交易都存储为正数,并通过反转“来/去”字段或使用标志表示贷记/借记。个人而言,我更喜欢一个贷方字段、一个借方字段和一个带符号的金额字段,这样撤销就更容易跟踪。

请注意,这些方法适用于现金和证券。

证券交易可能会更加棘手,特别是对于公司行动,您需要适应单个交易,该交易更新一个或多个买家和卖家的现金余额、证券头寸余额,以及可能的经纪人/托管。


8
个人而言,我更喜欢有一个借方字段、一个贷方字段和一个带符号金额,这样撤销操作就更容易跟踪了。为什么不只用一个“金额”字段带符号呢? - 001
11
每笔交易都包含3个字段。1/被借记的帐户ID。2/被贷记的帐户ID。3/金额,正数表示从贷方转移,负数表示从借方转移(通常是撤销)。我建议您阅读http://en.wikipedia.org/wiki/Double-entry_bookkeeping_system。 - smirkingman
1
@001 这是两个动作。1/ X 将 5000 积分转给 Y。2/ X 向 Z 支付 500 税款。必须这样做,因为后来有人会问“显示所有支付给税务账户 Z 的款项”,而你不希望在答案中出现 5000。我真的建议你多了解一些记账方面的知识,这会对你很有帮助。 - smirkingman
1
我认为讨论已经偏离了主题,因此我开了一个新话题来回答这个问题。https://dev59.com/82855IYBdhLWcg3wUScW - 001
11
这个答案是错误的,依赖代码且繁琐。如果要使用一种方法来(a)符合审计要求和立法规定,(b) 不需要触发器、离线安全网、复制列或重复数据,以及(c)无论表格数量如何都能良好执行,请查看**此回答**。 - PerformanceDBA
显示剩余17条评论

5
这是我得到的一个数据库设计,只有一个表格,用于存储操作/交易历史记录。它目前在许多小型项目中运作良好。
这并不替代特定的设计。这是一个通用解决方案,适用于大多数应用程序。
id:int 标准行id
operation_type:int 操作类型。付款、收款、利息等
source_type:int 操作来源。目标表或类别:用户、银行、供应商等
source_id:int 数据库中来源的ID
target_type:int 操作对象。目标表或类别:用户、银行、供应商等
target_id:int 数据库中目标的ID
amount:decimal(19,2 signed) 价格值为正或负,将被求和
account_balance:decimal(19,2 signed) 结果余额
extra_value_a:decimal(19,2 signed) [这是最多功能的选项,而无需使用字符串存储] 您可以存储附加数字:利息百分比、折扣、减少等。
created_at:timestamp 对于source_type和target_type,最好使用枚举或单独的表格。
如果您想要特定的余额,只需查询按创建时间倒序排列限制为1的最后一个操作。您可以按源、目标、操作类型等查询。
为了获得更好的性能,建议将当前余额存储在所需的目标对象中。

4
你应该存储当前的账户余额并始终保持其最新状态。交易表只是过去发生情况的记录,不应频繁使用它来获取当前余额。请考虑,许多查询不仅想要余额,还想进行筛选、排序和分组等操作。在复杂查询过程中对您曾经创建的每一笔交易求总和的性能惩罚会使即使是规模较小的数据库也陷入困境。
所有对这对表的更新都应该在事务中进行,并确保两个表始终保持同步(账户从未透支超过其限制)或者事务回滚。作为额外措施,您可以定期运行审核查询以检查这一点。

告诉来审核你的记账的人,他们会惊讶地笑得很久。每个交易都必须编号(以保持顺序),你可以直接将新的账户余额放在那里,不需要第二张表。实际上没有性能损失。顺便说一下,这就是记账的方式。 - TomTom
@TomTom:现在我明白你的观点了。你的想法很好。可惜在你的回答中没有表述清楚,而且你的回答看起来太过激烈! - iDevlop
1
@TomTom,Marcelo的措辞有点模糊,但是你关于审计的评论既不实用也不正确。审计是关于建立正确性,与速度优化几乎没有关系。Marcelo有点不精确,因为他谈到了“当前余额”,而实际上大多数金融系统将根据账户和其他分析维度保持余额,这些维度按某个日期粒度汇总。在交易级别上保持流动余额的想法对于任何需要根据除符合预订顺序的属性之外的任何内容过滤交易的报告都是无用的。 - Unreason

2
这个问题的常见解决方案是在快照模式下维护一个(假设为)每月的期初余额。通过将月度交易数据添加到每月的期初余额中,可以计算出当前余额。这种方法通常用于账户软件中,特别是当您需要货币转换和重新估值时。
如果您的数据量过大,您可以将旧余额存档。
此外,如果您没有专门的外部数据仓库或系统上的管理报告功能,则余额可用于报告。

2

当然,您需要在每一行中存储当前余额,否则速度将会变得太慢。为了简化开发过程,您可以使用约束条件,这样就不需要触发器和定期检查数据完整性。我在这里描述了它:反规范化以强制执行业务规则:运行总计


2
我看到你喜欢在记录文件系统中灌入混凝土。 - PerformanceDBA
FYI,链接已经失效! - ImtiazeA

0

我的方法是将借方存储在借方列中,将贷方存储在贷方列中,当获取数据时创建两个数组,借方数组和贷方数组。然后将所选数据追加到数组中,并对Python执行此操作:

def real_insert(arr, index, value):
    try:
        arr[index] = value
    except IndexError:
        arr.insert(index, value)


def add_array(args=[], index=0):
    total = 0
    if index:
        for a in args[: index]:
            total += a
    else:
        for a in args:
            total += a
    return total

然后

for n in range(0, len(array), 1):
    self.store.clear()
    self.store.append([str(array[n][4])])
    real_insert(self.row_id, n, array[n][0])
    real_insert(self.debit_array, n, array[n][7])
    real_insert(self.credit_array, n, array[n][8])
    if self.category in ["Assets", "Expenses"]:
        balance = add_array(self.debit_array) - add_array(self.credit_array)
    else:
        balance = add_array(self.credit_array) - add_array(self.debit_array)

0

你的朋友是错的,而你是正确的,我建议你现在不要改变任何东西。
如果你的数据库因此变慢了,并且在你验证了所有其他方面(适当的索引)之后,一些去规范化可能会有用。
然后,你可以在账户表中放置一个BalanceAtStartOfYear字段,并仅总结今年的记录(或任何类似的方法)。
但我肯定不建议一开始就采用这种方法。


啊...不,认真点。会计是一个棘手的业务,可能涉及一些繁重的法律要求。临时总结不够准确。 - TomTom
2
@TomTom。当然,如果交易根据会计准则实施(您的不是),那么"ad hoc"就是错误的。此外,在每一行中复制CurrentBalance是非常愚蠢的:当需要进行调整时,需要更新大量的行。 - PerformanceDBA

0

在这里,我想建议您如何以非常简单的方式存储您的期初余额:

  1. 在交易表上创建一个触发器函数,仅在更新或插入后调用。

  2. 在账户主表中创建一个名为“期初余额”的列。

  3. 将您的期初余额保存在主表的期初余额列中的数组中。

  4. 您甚至不需要使用服务器端语言,只需使用数据库数组函数即可,例如PostgreSQL中提供的函数。

  5. 当您想要重新计算数组中的期初余额时,只需使用数组函数对交易表进行分组,并在主表中更新所有数据。

我已经在PostgreSQL中完成了这个过程,并且运行良好。

随着时间的推移,当您的交易表变得繁重时,您可以根据日期对其进行分区,以加快性能。 这种方法非常简单,无需使用任何额外的表格,如果连接表格,则可能会降低性能,因为连接表格越少,性能就越高。


你好,我不太清楚第三点。 "opening balance" 是在同一个交易表中还是在另一个表中? - Axil

-3
简单的回答:三者都要做。
存储当前余额;在每个交易中存储动态和当前时间点的余额快照。这将为任何审计提供额外的对账内容。
我从未在核心银行系统上工作过,但我曾经在投资管理系统上工作过,根据我的经验,就是这样做的。

1
这为审计提供了额外的调和内容。这有助于审计软件(在正确计算和检查交易是否被删除方面),但这不是必需的方式,其中我工作的部门是IT审计的一部分;但这与会计审计没有直接关系(IT审计只是财务审计的一部分)。 - Unreason
正确,但在大型互操作系统中遇到某些过程(例如隔夜批处理)创建异常情况并不罕见;一些去规范化的方法将有助于确保可以追踪到它们。 - Dog Ears
@Dog Ears,我明白了——所以你的大部分条目都来自数据交换/互操作,而IT审计是财务审计的一个更强的组成部分。有趣。不过我仍然坚持我的立场:这是一种额外的控制价值,将存在于所有交易之上。如果我想审计数据交换过程,我会放置一些专注于此的控制(使用某种数据交换的校验和:由发起者计算并简单接收,由接收到的格式中的接收数据计算 - 检查传输的完整性,由导入的数据计算 - 检查您的导入程序)。 - Unreason
@dogs ears 如果你从未在核心银行系统上工作过,你应该克制自己不要提供(糟糕的)建议。 - smirkingman
1
@smirkingman,为了社区的利益,我的建议有什么糟糕之处?实际上,它与你的建议有何不同?看起来我们基本上提出了相同的建议? - Dog Ears
3
@dogs ears 这种糟糕的情况会在每一个动作中储存平衡。数据重复增加了,一旦出现问题,修复起来就像噩梦一样困难。 - smirkingman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接