没有历史表的数据库版本控制

dma*_*hop 2 database-design database-versioning

我正在阅读这篇文章以实现表的记录级版本控制。我注意到该架构涉及历史表的使用。但是,我的场景不需要回滚,而是检索时间点记录。这是我尝试使用单个表进行版本控制的设计。请注意,这是一个简单的表数据(没有约束、索引等)。我打算基于 id 建立索引,因为这涉及列上的 group by 子句。

例如,我有一个表 Test ,其中

id 是标识符,

modstamp 是数据的时间戳(绝不为空)

除了上面的列外,该表还将包含簿记列

local_modstamp 是更新记录的时间戳

del_modstamp 是删除记录的时间戳

在备份期间,所有记录均从源获取并插入到记录具有值 local_modstamp = null 和 del_stamp = null 的位置。

id |modstamp                   |local_modstamp |del_modstamp |
---|---------------------------|---------------|-------------|
1  |2016-08-01 15:35:32 +00:00 |               |             |
2  |2016-07-29 13:39:45 +00:00 |               |             |
3  |2016-07-21 10:15:09 +00:00 |               |             |
Run Code Online (Sandbox Code Playgroud)

获得记录后,处理数据的场景如下(假设参考时间[ref_time]是进程运行的时间):

  1. 正常插入。

  2. 更新:使用 local_modstamp = ref_time 更新最新记录。然后插入新记录。查询将是:更新测试集 local_modstamp = 其中 id = 且 local_modstamp 不为空且 del_modstamp 不为空 插入测试值(...)

  3. 删除:用 del_modstamp = ref_time 更新最近的记录。更新测试集 del_modstamp = 其中 id = 且 local_modstamp 不为 null 并且 del_modstamp 不为 null

设计目的是获取local_modstamp不为null且del_modstamp不为null的最新记录。但是,我遇到了一个问题,我打算使用查询(最内部的查询)检索时间点:

select id, max(modstamp) from test where modstamp <= <ref_time> and (del_modstamp is null || del_modstamp <= <ref_time>) group by id;
Run Code Online (Sandbox Code Playgroud)

看来我犯了一个错误(是吗?),使用 null 作为占位符来标识表的最新记录。有没有办法利用现有的设计来获取时间点记录?

如果没有,我想可能的解决方案是将 local_modstamp 设置为最新记录。这需要在更新时使用 max(local_modstamp) 更新逻辑。我可以坚持现有的架构来实现检索时间点数据吗?

我现在正在使用 SQL-Server,但这种设计也可以扩展到其他数据库产品。我打算使用更通用的方法来检索数据,而不是使用特定于供应商的hack

Tom*_*att 5

引入版本范式。考虑这个表:

create table Entities(
    ID     int identity primary key,
    S1     [type],  -- Static data
    Sn     [type],  -- more static data
    V1     [type],  -- Volatile data
    Vn     [type]   -- more volatile data
);
Run Code Online (Sandbox Code Playgroud)

静态数据是在实体的生命周期内不会更改或不需要跟踪的数据。易失性数据更改以及必须跟踪这些更改。

将易失性属性移至单独的表中:

create table EntityVersions(
    ID        int  not null,
    Effective date not null default sysdate(),
    Deleted   bit  not null default 0,
    V1        [type],
    Vn        [type],
    constraint PK_EntityVersions primary key( ID, Effective ),
    constraint FK_EntityVersionEntity foreign key( ID )
        references Entities( ID )
);
Run Code Online (Sandbox Code Playgroud)

实体表不再包含易失性属性。

插入操作使用静态数据创建主实体记录,并生成唯一的 ID 值。该值用于插入具有易失性数据的初始值的第一个版本。更新通常不会对主表执行任何操作(除非实际更改静态值),并且新的易失性数据的新版本会写入版本表。请注意,不会对现有版本进行任何更改,特别是最新或“当前”版本。新版本插入,操作结束。

要“撤消”最新版本或实际上任何版本,只需从版本表中删除该版本即可。

例如,具有以下属性的员工表:

EmployeeNum, HireDate, FirstName, LastName, PayRate, Dept, PhoneExt
Run Code Online (Sandbox Code Playgroud)

当然,EmployeeNum 将与 HireDate 和 FirstName 一样是静态的。PhoneExt 可能会不时发生变化,但我们不在乎。所以它被指定为静态的。最终的设计是:

Employees_S
===========
  EmployeeNum (PK), HireDate, FirstName, PhoneExt

Employees_V
===========
  EmployeeNum (PK), Effective (PK), IsDeleted, LastName, PayRate, Dept
Run Code Online (Sandbox Code Playgroud)

2016 年 1 月 1 日,我们聘请了 Sally Smith。静态数据被插入到Employees_S 中,生成EmployeeNum 值1001。我们还使用该值插入第一个版本。

Employees_S
===========
  1001, 2016-01-01, Sally, 12345

Employees_V
===========
  1001, 2016-01-01, 0, Smith, 35.00, Eng
Run Code Online (Sandbox Code Playgroud)

3 月 1 日,她加薪了:

Employees_S
===========
  1001, 2016-01-01, Sally, 12345

Employees_V
===========
  1001, 2016-01-01, 0, Smith, 35.00, Eng
  1001, 2016-03-01, 0, Smith, 40.00, Eng
Run Code Online (Sandbox Code Playgroud)

5月1日,她结婚了:

Employees_S
===========
  1001, 2016-01-01, Sally, 12345

Employees_V
===========
  1001, 2016-01-01, 0, Smith, 35.00, Eng
  1001, 2016-03-01, 0, Smith, 40.00, Eng
  1001, 2016-05-01, 0, Jones, 40.00, Eng
Run Code Online (Sandbox Code Playgroud)

请注意,同一实体的版本除了生效日期不能相同的限制外,彼此完全独立。

要查看员工 1001 的当前状态,请执行以下查询:

select  s.EmployeeNum, s.HireDate, s.FirstName, v.LastName, v.PayRate, v.Dept, s.PhoneExt
from    Employees_S s
join    Employees_V v
    on  v.EmployeeNum = s.EmployeeNum
    and v.Effective = ( select  Max( Effective )
                        from    Employees_V
                        where   EmployeeNum = v.EmployeeNum
                            and Effective <= SysDate() )
where   s.EmployeeNum = 1001
    and v.IsDeleted = 0;
Run Code Online (Sandbox Code Playgroud)

这是最酷的部分。要查看员工 1001 在 2 月 11 日的状态,查询如下:

select  s.EmployeeNum, s.HireDate, s.FirstName, v.LastName, v.PayRate, v.Dept, s.PhoneExt
from    Employees_S s
join    Employees_V v
    on  v.EmployeeNum = s.EmployeeNum
    and v.Effective = ( select  Max( Effective )
                        from    Employees_V
                        where   EmployeeNum = v.EmployeeNum
                            and Effective <= '2016-02-11' )
where   s.EmployeeNum = 1001
    and v.IsDeleted = 0;
Run Code Online (Sandbox Code Playgroud)

这是相同的查询——除了子查询的最后一行。当前数据和历史数据位于同一张表中,并使用同一条语句进行查询。

这是另一个很酷的功能。现在是 7 月 1 日,我们知道 9 月 1 日,Sally 将调到营销部门,并再次加薪。文书工作已经完成。继续插入新数据:

Employees_S
===========
  1001, 2016-01-01, Sally, 12345

Employees_V
===========
  1001, 2016-01-01, 0, Smith, 35.00, Eng
  1001, 2016-03-01, 0, Smith, 40.00, Eng
  1001, 2016-05-01, 0, Jones, 40.00, Eng
  1001, 2016-09-01, 0, Jones, 50.00, Mkt
Run Code Online (Sandbox Code Playgroud)

倒数第二个版本仍将显示为当前版本,但 9 月 1 日或之后执行的第一个查询将显示营销数据。

以下是我在技术展会上做过几次演示的幻灯片。它包含有关如何完成上述所有操作(包括查询)的更多详细信息。这一份更详细的文档。