没有历史表的数据库版本控制
Database versioning without history tables
我正在通过这个 post 来进行 table 的记录级版本控制。我注意到该架构处理 history tables 的使用。但是,我的场景不需要回滚,而是需要检索时间点记录。这是我尝试使用单个 table 进行版本控制的设计的地方。请注意,这是一个简单的 table 数据(没有约束、索引等)。我打算基于 id 进行索引,因为这涉及列上的 group by 子句。
例如,我有一个 table 测试,其中
id is the identifier,
modstamp is the timestamp of the data (never null)
除上述列外,table 还将包含簿记列
local_modstamp is the timestamp at which the record was updated
del_modstamp is the timestamp at which the record was deleted
在备份过程中,所有记录都从源中获取并插入到记录值 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]是处理运行):
正常插入。
更新:更新最近的记录local_modstamp = ref_time。然后插入新记录。
查询将是:
更新测试集 local_modstamp = where id = and local_modstamp is not null and del_modstamp is not null
插入测试值(...)
删除:更新最近的记录为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 作为占位符来标识 table 的最新记录。有没有办法使用现有的设计来获取时间点记录?
如果不是,我想可能的解决方案是将 local_modstamp 设置为最新记录。这将需要在更新的情况下使用 max(local_modstamp) 更新逻辑。我可以坚持我现有的架构来实现检索时间点数据吗?
我现在正在使用 SQL-Server,但这种设计也可以扩展到其他数据库产品。我打算使用更通用的方法来检索数据,而不是使用特定于供应商的 hacks.
引入版本范式。考虑这个 table:
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
);
静态数据是在实体的生命周期内不会更改或不需要跟踪的数据。易失性数据更改,必须跟踪这些更改。
将可变属性移动到单独的 table:
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 )
);
实体 table 不再包含可变属性。
插入操作使用静态数据创建主实体记录,生成唯一 ID 值。该值用于插入具有易失性数据初始值的第一个版本。更新通常不会对主 table 执行任何操作(除非实际更改了静态值)并且将新版本的新易失性数据写入版本 table。请注意,不会对现有版本进行任何更改,尤其是最新版本或 "current" 版本。新版本插入,运行结束
要"undo"最新版本,或者实际上任何版本,只需从版本table中删除那个版本。
例如,具有以下属性的 Employees table:
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;
这是同一个查询 -- 除了子查询的最后一行。当前和历史数据驻留在同一个 table 中,并使用相同的语句进行查询。
这是另一个很棒的功能。现在是 7 月 1 日,我们知道 Sally 将在 9 月 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, 2016-09-01, 0, Jones, 50.00, Mkt
倒数第二个版本仍将显示为当前版本,但在 9 月 1 日或之后执行的第一个查询将显示营销数据。
Here are the slides of a presentation I have made a few times at tech fairs. It contains more details about how all the above can be done including the queries. And here 是一份包含更多细节的文档。
我正在通过这个 post 来进行 table 的记录级版本控制。我注意到该架构处理 history tables 的使用。但是,我的场景不需要回滚,而是需要检索时间点记录。这是我尝试使用单个 table 进行版本控制的设计的地方。请注意,这是一个简单的 table 数据(没有约束、索引等)。我打算基于 id 进行索引,因为这涉及列上的 group by 子句。
例如,我有一个 table 测试,其中
id is the identifier,
modstamp is the timestamp of the data (never null)
除上述列外,table 还将包含簿记列
local_modstamp is the timestamp at which the record was updated
del_modstamp is the timestamp at which the record was deleted
在备份过程中,所有记录都从源中获取并插入到记录值 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]是处理运行):
正常插入。
更新:更新最近的记录local_modstamp = ref_time。然后插入新记录。 查询将是: 更新测试集 local_modstamp = where id = and local_modstamp is not null and del_modstamp is not null 插入测试值(...)
删除:更新最近的记录为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 作为占位符来标识 table 的最新记录。有没有办法使用现有的设计来获取时间点记录?
如果不是,我想可能的解决方案是将 local_modstamp 设置为最新记录。这将需要在更新的情况下使用 max(local_modstamp) 更新逻辑。我可以坚持我现有的架构来实现检索时间点数据吗?
我现在正在使用 SQL-Server,但这种设计也可以扩展到其他数据库产品。我打算使用更通用的方法来检索数据,而不是使用特定于供应商的 hacks.
引入版本范式。考虑这个 table:
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
);
静态数据是在实体的生命周期内不会更改或不需要跟踪的数据。易失性数据更改,必须跟踪这些更改。
将可变属性移动到单独的 table:
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 )
);
实体 table 不再包含可变属性。
插入操作使用静态数据创建主实体记录,生成唯一 ID 值。该值用于插入具有易失性数据初始值的第一个版本。更新通常不会对主 table 执行任何操作(除非实际更改了静态值)并且将新版本的新易失性数据写入版本 table。请注意,不会对现有版本进行任何更改,尤其是最新版本或 "current" 版本。新版本插入,运行结束
要"undo"最新版本,或者实际上任何版本,只需从版本table中删除那个版本。
例如,具有以下属性的 Employees table:
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;
这是同一个查询 -- 除了子查询的最后一行。当前和历史数据驻留在同一个 table 中,并使用相同的语句进行查询。
这是另一个很棒的功能。现在是 7 月 1 日,我们知道 Sally 将在 9 月 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, 2016-09-01, 0, Jones, 50.00, Mkt
倒数第二个版本仍将显示为当前版本,但在 9 月 1 日或之后执行的第一个查询将显示营销数据。
Here are the slides of a presentation I have made a few times at tech fairs. It contains more details about how all the above can be done including the queries. And here 是一份包含更多细节的文档。