具有变更历史的数据库设计

时间:2013-06-12 21:21:06

标签: sql postgresql

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

Database A 

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

     1        Kyle      30

如果我将行的'property'字段更改为50,则应将该行更新为:

1    Kyle    50

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

1    Kyle    70

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

1    Kyle    30
1    Kyle    50

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

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

处理此数据库设计的最佳方法是什么?

4 个答案:

答案 0 :(得分:13)

一种方法是为数据库中的每个表创建一个MyTableNameHistory,并使其模式与表MyTableName的模式相同,只是History表的主键有一个附加列将effectiveUtc命名为DateTime。例如,如果您有一个名为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表中插入一条新记录,其中所有常规字段的值都完全相同,以及effectiveUtc列中的当前UTC日期时间。

然后,要在过去的任何时间点找到值,只需从历史记录表中选择记录,其中effectiveUtc值是您想要该值的asOf日期时间之前的最高值。

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

答案 1 :(得分:3)

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

基本上,你会像这样创建 一个 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条记录。

按照你的例子,当你添加凯尔&#39;在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          |

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

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

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

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

此设计的一个体系结构优势是您可以解决应用程序和历史/审计功能的问题。这种设计与使用与应用程序数据库分开的关系甚至NoSQL数据库的微服务一样有效。

答案 2 :(得分:1)

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

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)

答案 3 :(得分:1)

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

有关实施的详情here