你的直觉很适合你。不要使用结束日期。这增加了可能的异常数据的复杂性和来源。采取以下顺序条目:
ID <attr> StartDate EndDate
1 ... Jan 1 Jan 20
1 ... Jan 20 Jan 22
1 ... Feb 1 Jul 30
在 1 月 1 日记录的状态更改一直有效,直到 1 月 20 日的下一次状态更改。现在我们遇到了问题。根据那个版本的 EndDate,1 月 22 日还有一次状态变化,但下一个版本是 2 月 1 日开始的。
这在时间流中形成了一个缺口,我们无法指出问题出在哪里。 1 月 22 日的 EndDate 是错误的吗? 2 月 1 日的 StartDate 是错误的吗?或者有没有连接间隙两端的缺失版本?没有办法说。
ID <attr> StartDate EndDate
1 ... Jan 1 Jan 20
1 ... Jan 20 Feb 20
1 ... Feb 1 Jul 30
现在有状态重叠。第二个状态应该持续到 2 月 20 日,但第三个状态说它从 2 月 1 日开始。但是一个状态的开始在逻辑上意味着前一个状态的结束。同样,我们不知道(仅通过查看数据)哪个日期是错误的。
知道一个状态的开始也表示前一个状态的结束,看看当我们简单地删除 EndDate 列时会发生什么。
ID <attr> EffDate
1 ... Jan 1
1 ... Jan 20
1 ... Feb 1
现在间隙和重叠是不可能的。每个状态从生效日期开始,到下一个状态开始时结束。由于 EffDate 字段是 PK 的一部分,因此对于给定的 ID 值,任何条目都不能具有相同的 EffDate 值。
此设计不与主实体表一起使用。它被实现为第二范式的一种特殊形式,我可以对范式进行版本化(vnf)。
您的 Employee 表将包含不会随时间变化的字段,有些字段会随时间变化。您可能还有一些字段发生了变化,但您不希望跟踪这些变化。
create table Employees(
ID int auto_generated primary key,
Hired date not null,
FName varchar not null,
LName varchar not null,
Sex enum -- M or F
BDay date,
Position enum not null,
PayRate currency,
DeptID int references Depts( ID )
);
如果我们希望跟踪数据的变化,我们可以添加一个生效日期字段。但是,请考虑,诸如雇用日期和出生日期之类的数据不会从一个版本更改为另一个版本。因此,它们仅依赖于 ID 字段。确实发生变化的数据(Position、PayRate、DeptID)取决于 ID 和有效日期字段。该表不再是 2nf。
所以我们归一化:
create table Employees(
ID int auto_generated primary key,
Hired date not null,
FName varchar not null,
Sex enum -- M or F
BDay date
);
create table Employees_V(
ID int not null references Employees( ID ),
EffDate date not null,
LName varchar not null,
Position enum not null,
PayRate currency,
DeptID int references Depts( ID ),
constraint PK_Employees_V primary key( ID, EffDate )
);
姓氏可能会不时改变,尤其是女性员工。
此方法的主要优点之一是外键不能引用版本。现在所有 FK 都可以正常引用主实体表了。
获取“当前”数据的查询比较简单:
select e.ID, e.Hired, e.FName, v.Lname, e.Sex, e.BDay, v.Position, v.PayRate, v.DeptID
from Employees e
join Employees)V v
on v.ID = e.ID
and v.EffDate =(
select Max( EffDate )
from Employees_V
where ID = v.ID
and EffDate <= GetDate())
where e.ID = 123;
与查询具有开始/结束日期的表相比。
select ID, Hired, FName, Lname, Sex, BDay, Position, PayRate, DeptID
from Employees
where ID = 123
and StartDate >= GetDate()
and EndDate < GetDate();
这假定当前版本的 EndDate 值是一个魔术值,例如 12/31/9999。
第二个查询看起来比第一个简单得多。即使数据如上所示进行了规范化,也有连接但没有子查询。它看起来也会执行得更快。
我已经使用这种技术大约 8 年了,我从来没有因为性能问题而改变它。 vnf 查询运行最坏情况比开始/结束版本慢不到 10%。因此,一分钟的查询大约需要 1 分 5 秒。但是,在某些情况下,vnf 查询会执行得更快。
以具有很多很多变化(数千个版本)的实体为例。开始/结束查询执行索引扫描。它从最早的版本开始,并且必须按顺序检查每个版本,直到找到 EndDate 小于目标日期的版本。通常,这是最后一个版本。在 vnf 查询中,子查询可以执行索引查找。
所以不要因为你认为它很慢而拒绝这个设计。它并不慢。尤其是当您认为插入新版本只需要一个 INSERT 语句时。使用开始/结束日期时,插入新版本需要 UPDATE 和 INSERT。在两个现有版本之间插入新版本时,它是两个 UPDATE 和一个 INSERT。要删除一个开始/结束版本需要一个或两个 UPDATE 和一个 DELETE 语句。要删除 vnf 版本,只需删除该版本即可。
如果版本之间的开始日期和结束日期不同步,您就会有差距或重叠,祝您找到正确的值。
因此,我将承担较小的性能损失,以确保数据永远不会失去同步并导致异常。事实证明,这个 (vnf) 确实是更简单的设计。