SQL表中的版本控制 - 如何处理它?

时间:2010-09-22 19:34:21

标签: sql orm versioning

这是一个虚构的场景,其中包含一些填充数据。出于税收目的,我的虚构公司必须保留历史数据的记录。出于这个原因,我在表中添加了一个版本列。

TABLE EMPLOYEE: (with personal commentary)

|ID | VERSION | NAME       | Position | PAY |
+---+---------+------------+----------+-----+
| 1 |    1    | John Doe   | Owner    | 100 | Started company
| 1 |    2    | John Doe   | Owner    |  80 | Pay cut to hire a coder
| 2 |    1    | Mark May   | Coder    |  20 | Hire said coder
| 2 |    2    | Mark May   | Coder    |  30 | Productive coder gets raise
| 3 |    1    | Jane Field | Admn Asst|  15 | Need office staff
| 2 |    3    | Mark May   | Coder    |  35 | Productive coder gets raise
| 1 |    3    | John Doe   | Owner    | 120 | Sales = profit for owner!
| 3 |    2    | Jane Field | Admn Asst|  20 | Raise for office staff
| 4 |    1    | Cody Munn  | Coder    |  20 | Hire another coder
| 4 |    2    | Cody Munn  | Coder    |  25 | Give that coder raise
| 3 |    3    | Jane Munn  | Admn Asst|  20 | Jane marries Cody <3
| 2 |    4    | Mark May   | Dev Lead |  40 | Promote mark to Dev Lead
| 4 |    3    | Cody Munn  | Coder    |  30 | Give Cody a raise
| 2 |    5    | Mark May   | Retired  |   0 | Mark retires
| 5 |    1    | Joey Trib  | Dev Lead |  40 | Bring outside help for Dev Lead
| 6 |    1    | Hire Meplz | Coder    |  10 | Hire a cheap coder
| 3 |    4    | Jane Munn  | Retired  |   0 | Jane quits
| 7 |    1    | Work Fofre | Admn Asst|  10 | Hire Janes replacement
| 8 |    1    | Fran Hesky | Coder    |  10 | Hire another coder
| 9 |    1    | Deby Olav  | Coder    |  25 | Hire another coder
| 4 |    4    | Cody Munn  | VP Ops   |  80 | Promote Cody
| 9 |    2    | Deby Olav  | VP Ops   |  80 | Cody fails at VP Ops, promote Deby
| 4 |    5    | Cody Munn  | Retired  |   0 | Cody retires in shame
| 5 |    2    | Joey Trib  | Dev Lead |  50 | Give Joey a raise
+---+---------+------------+----------+-----+

现在,如果我想做“获取当前程序员列表”之类的内容,我不能只执行SELECT * FROM EMPLOYEE WHERE Position = 'Coder',因为这会返回大量历史数据......这很糟糕。

我正在寻找处理这种情况的好主意。我看到了一些跳出来的选项,但是我肯定有人会说“哇,这是一个新手的错误,发光......试试这个大小:”这就是这个地方的全部,对吧? : - )

创意编号1 :保留包含当前版本的版本表

TABLE EMPLOYEE_VERSION:

|ID |VERSION|
+---+-------+
| 1 |   3   |
| 2 |   5   |
| 3 |   4   |
| 4 |   6   |
| 5 |   2   |
| 6 |   1   |
| 7 |   1   |
| 8 |   1   |
| 9 |   2   |     
+---+-------+

虽然我不确定如何用一个查询来做到这一点,但我确信它可以完成,而且我打赌我能用相当少的努力来解决这个问题。

当然,每次插入EMPLOYEE表时,我都必须更新此表,以增加给定ID的版本(或在创建新ID时插入版本表)。

这种开销似乎是不可取的。

创意编号2:保留存档表和主表。在更新主表之前,将我要覆盖的行插入到归档表中,并像往常一样使用主表,就像我不关心版本控制一样。

创意编号3 :查找添加SELECT * FROM EMPLOYEE WHERE Position = 'Coder' and version=MaxVersionForId(EMPLOYEE.ID)行内容的查询...不完全确定我是如何做到这一点的。这对我来说似乎是最好的主意,但我现在还不确定。

创意号4:为“当前”创建一列并添加“WHERE current = true AND ...”

我发现,人们之前肯定已经做过这件事,遇到同样的问题,并且有分享的见解,所以我来收集它! :)我已经尝试在这里找到问题的例子,但它们似乎专门用于特定场景。

谢谢!

编辑1:

首先,我感谢所有答案,你们都说了同样的话 - DATEVERSION NUMBER好。我使用VERSION NUMBER的一个原因是简化服务器中的更新过程以防止出现以下情况

人员A在他的会话中加载员工记录3,它具有版本4。 人员B在他的会话中加载员工记录3,并且它具有版本4。 人员A进行更改和提交。这是有效的,因为数据库中的最新版本是4.它现在是5。 B人进行更改和提交。这失败了,因为最新版本是5,而他的版本是4.

EFFECTIVE DATE模式如何解决此问题?

编辑2:

我想我可以通过这样做来做到这一点: 人员A在他的会话中加载员工记录3,其生效日期是1-1-2010,1:00 pm,没有任何费用。 B人在他的会话中加载员工记录3,其生效日期为1-1-2010,1:00 pm,没有任何费用。 人员A进行更改和提交。旧副本进入存档表(基本上是想法2),考试日期为9/22/2010下午1:00。主表的更新版本的生效日期为2010年9月22日下午1:00。 B人进行更改和提交。提交失败,因为生效日期(在数据库和会话中)不匹配。

7 个答案:

答案 0 :(得分:32)

我认为你已经走错了路。

通常,对于版本控制或存储历史数据,您可以执行两项(或两项)操作之一。

  1. 您有一个单独的表格,可以模仿原始表格+更改日期的日期/时间列。每当更新记录时,您都会在更新之前将现有内容插入到历史记录表中。

  2. 您有一个单独的仓库数据库。在这种情况下,您可以像上面的#1一样对其进行版本修改,或者您只需每隔一段时间对其进行一次快照(每小时,每天,每周......)

  3. 将您的版本号保存在与普通版本相同的表格中有几个问题。首先,表格大小会像疯了一样增长。这将对正常的生产查询施加持续的压力。

    其次,为了确保使用每个记录的最新版本,它将大大增加连接等的查询复杂性。

答案 1 :(得分:28)

这里有什么称为慢变维度(SCD)。有一些经过验证的方法可以解决它:

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

我想补充一点,因为似乎没有人按名称来称呼它。

答案 2 :(得分:10)

这是我建议的方法,过去对我来说效果非常好:

  • 忘记版本号。相反,请使用StartDateEndDate
  • 编写一个触发器,以确保同一ID没有重叠的日期范围,并且只有一条记录NULL EndDate用于同一ID (这是你目前有效的记录)
  • 将索引放在StartDateEndDate上;这应该会给你合理的表现

这将很容易让您按日期报告:

select *
from MyTable 
where MyReportDate between StartDate and EndDate

或获取最新信息:

select *
from MyTable 
where EndDate is null

答案 3 :(得分:9)

我为最近的数据库设计的方法是使用如下修订:

  • 将您的实体保存在两个表中:

    1. “employee”存储主键ID以及您不希望进行版本控制的任何数据(如果有)。

    2. “employee_revision”存储有关员工的所有重要数据,其中包含employee表的外键和外键,“RevisionID”表示名为“revision”的表。

  • 创建一个名为“revision”的新表。这可以由数据库中的所有实体使用,而不仅仅是员工。它包含主键的标识列(或自动编号,或者数据库调用的任何内容)。它还包含EffectiveFrom和EffectiveTo列。我还在表上有一个文本列 - entity_type - 出于人类可读性的原因,它包含主修订表的名称(在本例中为“employee”)。修订表不包含外键。 EffectiveFrom的默认值是19-Jan-1900,EffectiveTo的默认值是31-Dec-9999。这使我无法简化日期查询。

我确保修订表已在(EffectiveFrom,EffectiveTo,RevisionID)以及(RevisionID,EffectiveFrom,EffectiveTo)上编入索引。

然后我可以使用连接和简单的&lt;&gt;比较以选择任何日期的适当记录。这也意味着实体之间的关系也是完全版本化的。实际上,我发现使用SQL Server表值函数可以非常简单地查询任何日期。

以下是一个示例(假设您不想对员工姓名进行版本设置,以便在他们更改名称时,此更改在历史上有效。)

--------
employee
--------
employee_id  |  employee_name
-----------  |  -------------
12351        |  John Smith

-----------------
employee_revision
-----------------
employee_id  |  revision_id  |  department_id  |  position_id  |  pay
-----------  |  -----------  |  -------------  |  -----------  |  ----------
12351        |  657442       |  72             |  23           |  22000.00
12351        |  657512       |  72             |  27           |  22000.00
12351        |  657983       |  72             |  27           |  28000.00

--------
revision
--------
revision_id  |  effective_from  |  effective_to  |  entity_type
-----------  |  --------------  |  ------------  |  -----------
657442       |  01-Jan-1900     |  03-Mar-2007   |  EMPLOYEE
657512       |  04-Mar-2007     |  22-Jun-2009   |  EMPLOYEE
657983       |  23-Jun-2009     |  31-Dec-9999   |  EMPLOYEE

将修订元数据存储在单独的表中的一个优点是,可以轻松地将其一致地应用于所有实体。另一个是它更容易扩展它以包括其他东西,例如分支或场景,而不必修改每个表。我的主要原因是它使您的主要实体表保持清晰和整洁。

(上面的数据和示例都是虚构的 - 我的数据库没有为员工建模。)

答案 4 :(得分:3)

尽管这个问题已经问了8年前,但值得一提的是SQL Server 2016中确实有此功能。系统版本的临时表

SQL Server 2016及更高版本中的每个表都可以有一个历史记录表,该历史记录数据将由SQL Server本身自动填充。

您需要做的就是在表中添加两列datetime2列和一个子句:

CREATE TABLE Employee 
(
    Id int NOT NULL PRIMARY KEY CLUSTERED,
    [Name] varchar(50) NOT NULL,
    Position varchar(50)  NULL,
    Pay money NULL,
    ValidFrom datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    ValidTo datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
        PERIOD FOR SYSTEM_TIME (ValidFrom,ValidTo)
)  
WITH (SYSTEM_VERSIONING = ON);

系统版本控制表创建一个临时表,该表维护数据的历史记录。您可以使用自定义名称WITH (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.EmployeeHistory ) );

this link中,您可以找到有关系统版本时态表的更多详细信息。

正如@NotMe所述,历史表可以非常快速地增长,因此有几种解决方法。 Take a look here

答案 5 :(得分:2)

创意3将起作用:

SELECT * FROM EMPLOYEE AS e1
WHERE Position = 'Coder'
AND Version = (
    SELECT MAX(Version) FROM Employee AS e2
    WHERE e1.ID=e2.ID)

你真的想要使用像日期这样的东西,这更容易编程和跟踪,并将使用相同的逻辑(类似 EffectiveDate 列)

修改

Chris完全正确地将此信息从生产表中移出以获得性能,特别是如果您希望经常更新。另一种选择是制作一个 VIEW ,它只显示每个人信息的最新版本,即你在此表格中构建的。

答案 6 :(得分:2)

你肯定做错了。保持数据库运行甜蜜要求您只需要生产表中所需的最少量数据。不可避免地保留历史数据与实时添加冗余会使查询复杂化并降低性能,而且您的继任者在将其提交给DailyWTF之前会看起来非常歪斜!

而是创建表的副本 - 例如EmployeeHistorical - 但ID列未设置为标识(您可以选择添加其他新ID列和dateCreated时间戳列)。然后在您的Employee表中添加一个触发器,该触发器将在update&amp;删除并将完整行的副本写入Historical表。当你正在捕获用户的ID时,编辑通常会出于审计目的而派上用场。

通常,当我在活动表上执行此操作时,我尝试在不同的数据库中创建历史表,以减少主数据库中的碎片(并因此维护),并且更容易处理备份 - 作为归档可以长得很大。

有关编辑争用的问题应该使用普通的数据库事务和锁定机制来处理。编码adhoc hacks up to emulate这样自己总是耗时且容易出错(一些边缘条件,你没有想到总是弹出,并且正确地写锁,你真的需要grok sempahores,这是显然是非平凡的)