【问题标题】:Database versioning without history tables没有历史表的数据库版本控制
【发布时间】:2016-12-07 01:03:57
【问题描述】:

我正在通过 post 对表进行记录级版本控制。我注意到该架构处理历史表的使用。但是,我的场景不需要回滚,而是检索时间点记录。这是我尝试使用单个表进行版本控制的设计。请注意,这是一个裸骨表数据(没有约束、索引等)。我打算根据 id 进行索引,因为这涉及列上的 group by 子句。

例如,我有一个表测试,其中

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 |               |             |

一旦获得记录,这些是处理数据的场景(假设参考时间[ref_time]是流程运行的时间):

  1. 正常插入。

  2. 更新:使用 local_modstamp = ref_time 更新最新记录。然后插入新记录。 查询将是: 更新测试集 local_modstamp = where id = and local_modstamp is not null and del_modstamp is not null 插入测试值(...)

  3. 删除:使用 del_modstamp = ref_time 更新最新记录。 更新测试集 del_modstamp = where id = and local_modstamp is not null and del_modstamp is not null

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

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

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

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

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

【问题讨论】:

    标签: database-design database-versioning


    【解决方案1】:

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

    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
    );
    

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

    将 volatile 属性移动到单独的表中:

    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 )
    );
    

    实体表不再包含可变属性。

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

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

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

    EmployeeNum, HireDate, FirstName, LastName, PayRate, Dept, PhoneExt
    

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

    Employees_S
    ===========
      EmployeeNum (PK), HireDate, FirstName, PhoneExt
    
    Employees_V
    ===========
      EmployeeNum (PK), Effective (PK), IsDeleted, LastName, PayRate, Dept
    

    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
    

    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
    

    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
    

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

    要查看员工 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;
    

    这是最酷的部分。要查看员工 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;
    

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

    这是另一个很酷的功能。现在是 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
    

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

    Here 是我在技术展览会上做过几次演示的幻灯片。它包含有关如何完成上述所有操作的更多详细信息,包括查询。而here 是一个包含更多细节的文档。

    【讨论】:

    • 如果我执行删除,是最新版本标记为 1 还是插入然后标记删除为 1?这会影响查询中的查询and v.IsDeleted = 0;。例如,Sally 于 2016-10-01 离开公司,但我要查询 2016-06-01 的状态是什么,忽略子句中的 isDeleted 会达到目的吗?我很困惑是否要规范化表格。我在一个模式中备份了大约几千个,并且将它们规范化为两个是否不会使表格膨胀。这可以移植到同一张桌子上吗?
    • 如果没有静态数据,并且版本表中的每一列都进行了版本化,上述解决方案可以用一个表来完成。无论何时选择时间点或最新数据,都需要创建一个临时表(从版本中选择 distinct(id)),然后根据需要执行连接。
    • PK是静态的,经验表明很少没有其他静态属性。但是,即使主表仅包含 PK,您仍然需要两个表。您需要一个未版本化的主表作为其他表中 FK 的目标。如果您仔细阅读不同的版本控制方案,最大的、无法克服的问题一直是缺乏参照完整性。这个 (vnf) 解决了这个问题。
    • @dmacho 我错过了您的第一条评论(8 月 3 日)。删除时,将写入与最后一个条目相同但 Deleted = 1 的新版本。查询将返回在 2016-06-01 或之前处于活动状态的最新版本,即 2016-05-01 版本.这是你所期望的。对于 2016-09-01 到 2016-09-30 的任何日期,将返回最后一个版本。在那之后,什么都不会被退回。正如预期的那样——从那时起它被“删除”了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-03
    • 1970-01-01
    相关资源
    最近更新 更多