【问题标题】:Database Design with Change History具有变更历史的数据库设计
【发布时间】:2013-06-12 21:21:05
【问题描述】:

我希望设计一个数据库来跟踪每组更改,以便我将来可以参考它们。比如:

Database A 

+==========+========+==========+
|   ID     |  Name  | Property |

     1        Kyle      30

如果我将行的“属性”字段更改为 50,它应该将该行更新为:

1    Kyle    50

但应该保存该行的属性在某个时间点为 30 的事实。那么如果该行再次更新为70:

1    Kyle    70

应该保留该行的属性为 50 和 70 这两个事实,以便通过一些查询我可以检索到:

1    Kyle    30
1    Kyle    50

它应该认识到这些只是在不同时间点的“相同条目”。

编辑:这段历史需要在某个时间点呈现给用户,因此理想情况下,应该了解哪些行属于同一个“修订集群”

设计这个数据库的最佳方法是什么?

【问题讨论】:

  • 您的应用程序是否需要了解历史记录(即向最终用户展示此历史记录),还是出于审计目的?
  • 是否需要将其存储在数据库中?通常这是在应用程序中完成的,因此它可以在版本控制中并在多个开发人员之间应用。
  • 是的,应用程序需要向用户呈现此历史记录。
  • 在这种情况下,您可能希望使用区块链,因为它保留了底层数据库的更改历史记录并允许您导航到以前的提交——这对于建立审计跟踪很有用。

标签: sql postgresql change-tracking


【解决方案1】:

一种方法是为数据库中的每个表设置一个MyTableNameHistory,并使其架构与表MyTableName 的架构相同,只是历史表的主键有一个名为effectiveUtc 的附加列作为日期时间。例如,如果您有一个名为 Employee 的表,

Create Table Employee
{
  employeeId integer Primary Key Not Null,
  firstName varChar(20) null,
  lastName varChar(30) Not null,
  HireDate smallDateTime null,
  DepartmentId integer null
}

那么历史表将是

Create Table EmployeeHistory
{
  employeeId integer Not Null,
  effectiveUtc DateTime Not Null,
  firstName varChar(20) null,
  lastName varChar(30) Not null,
  HireDate smallDateTime null,
  DepartmentId integer null,
  Primary Key (employeeId , effectiveUtc)
}

然后,您可以在 Employee 表上放置一个触发器,这样每次您在 Employee 表中插入、更新或删除任何内容时,都会在 EmployeeHistory 表中插入一条新记录,其中所有常规字段的值完全相同,以及当前 UTC 日期时间在 EffectiveUtc 列中。

然后要查找过去任何时间点的值,您只需从历史记录表中选择其有效Utc 值是您想要的值的 asOf 日期时间之前的最大值的记录。

 Select * from EmployeeHistory h
 Where EmployeeId = @EmployeeId
   And effectiveUtc =
    (Select Max(effectiveUtc)
     From EmployeeHistory 
     Where EmployeeId = h.EmployeeId
        And effcetiveUtc < @AsOfUtcDate) 

【讨论】:

  • 但是...查询效率低下,并且架构不允许轻易获取 OP 的修订集群(如使用易于写入的查询),即知道部门何时出现,何时被删除,当它被重新创建时......所有员工都在其中t
  • 可以使用 postgres DICTINCT ON () 子句简化查询。类似Select distinct on (EmployeeId) * from EmployeeHistory where effectiveUtc &lt;= @AsOfUtcDate order by EmployeeId, effectiveUtc desc
  • @Igor Typo: DISTINCT ON () (因为我的一个坏习惯是轻率的复制粘贴)
  • History 表为什么不直接变成普通表?然后我们总是可以只拥有一张表而仍然拥有我们所有的数据
  • 因为提取最新记录的查询效率低下,并且如果该查询使用非常频繁,并且比历史数据查询更频繁,那么将当前值分成自己的值是有意义的(小得多)表。
【解决方案2】:

要添加到Charles' answer,我会使用Entity-Attribute-Value model,而不是为数据库中的每个其他表创建不同的历史表。

基本上,您会像这样创建 one History 表:

Create Table History
{
  tableId varChar(64) Not Null,
  recordId varChar(64) Not Null,
  changedAttribute varChar(64) Not Null,
  newValue varChar(64) Not Null,
  effectiveUtc DateTime Not Null,
  Primary Key (tableId , recordId , changedAttribute, effectiveUtc)
}

然后,您将在任何时候在您的一个表中创建修改数据时创建History 记录。

按照您的示例,当您将“Kyle”添加到您的 Employee 表时,您将创建两条记录(每个非 id 属性一个),然后您将在每次属性更改时创建一条新记录:

History 
+==========+==========+==================+==========+==============+
| tableId  | recordId | changedAttribute | newValue | effectiveUtc |
| Employee | 1        | Name             | Kyle     | N            |
| Employee | 1        | Property         | 30       | N            |
| Employee | 1        | Property         | 50       | N+1          |
| Employee | 1        | Property         | 70       | N+2          |

或者,正如this comment 中的a_horse_with_no_name 建议的那样,如果您不想为每个字段更改存储新的History 记录,您可以存储分组更改(例如将Name 更改为“Kyle”和 Property 到 30 在同一更新中)作为单个记录。在这种情况下,您需要以 JSON 或其他一些 blob 格式表示更改的集合。这会将changedAttributenewValue 字段合并为一个(changedValues)。例如:

History 
+==========+==========+================================+==============+
| tableId  | recordId | changedValues                  | effectiveUtc |
| Employee | 1        | { Name: 'Kyle', Property: 30 } | N            |

这可能比为数据库中的每个其他表创建一个历史表更困难,但它有很多好处:

  • 向数据库中的表添加新字段不需要向另一个表添加相同的字段
  • 使用的表更少
  • 随着时间的推移将更新关联到不同的表会更容易

这种设计的一个架构优势是您可以将应用的关注点与历史记录/审计功能分离。这种设计与使用与应用程序数据库分开的关系甚至 NoSQL 数据库的微服务一样好。

【讨论】:

  • 将所有行值存储在单个 JSON 或 hstore 列中而不是为每个修改的列存储一行可能更有效。例如遵循各种审计触发器中使用的模式,请参阅:okbob.blogspot.de/2015/01/…8kb.co.uk/blog/2015/01/19/…cjauvin.blogspot.de/2013/05/…
  • @a_horse_with_no_name 是的。那肯定也行。我还将添加一条说明该选项的注释。谢谢!
  • History.newValue 列名不应该是 History.oldValue?那是一个历史表,它所讲述的值应该是旧的。正确的表保留 newValue(当前的),如果我错了,请纠正我。
  • 我只是想为我的 30 列表准备我的第一个 EAV 表,然后我意识到我已经花了好几个星期来保持正确的数据类型。 EAV 丢失它们而不返回,并且将数据类型保存在 History 表的下一列中使这件事复杂得无法衡量
  • @Marecky 您可以使用旧值或新值。我喜欢使用新值并使用包含初始化时当前值的种子记录来初始化表。您也可以在进行更改时只记录旧值,并依赖实际表的数据来获取当前值。
【解决方案3】:

最好的方法取决于你在做什么。您想更深入地了解缓慢变化的维度:

https://en.wikipedia.org/wiki/Slowly_changing_dimension

在 Postgres 9.2 中也不要错过 tsrange 类型。它允许将 start_dateend_date 合并到一个列中,并使用 GIST(或 GIN)索引以及排除约束来索引这些内容,以避免日期范围重叠。


编辑:

应该了解哪些行属于同一个“修订集群”

在这种情况下,您希望在表格中以某种方式显示日期范围,而不是修订号或实时标志,否则您最终会在各处复制相关数据。

另外,请考虑将审计表与实时数据区分开来,而不是将所有内容都存储在同一个表中。它更难实施和管理,但可以更高效地查询实时数据。


也请参阅此相关帖子:Temporal database design, with a twist (live vs draft rows)

【讨论】:

    【解决方案4】:

    记录所有更改的方法之一是创建所谓的audit triggers。此类触发器可以将对其所在表的任何更改记录到单独的日志表中(可以查询该表以查看更改的历史记录)。

    关于实现的细节here

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-11-19
      • 2013-02-22
      • 1970-01-01
      • 2019-04-28
      • 1970-01-01
      • 1970-01-01
      • 2018-10-28
      相关资源
      最近更新 更多