Jac*_*123 4 sql sql-server database-design date
我是一名业务分析师,并且为我们正在实施的系统准备了表/ erd。
上下文本质上是一个员工管理系统,员工可以加入公司,变更职位,晋升,降职,离职等。所有这些都需要进行跟踪,以进行过滤和报告。因此,我们需要对记录进行历史跟踪。
我的建议和表格的原始设计包括一个称为“有效日期”的字段,因此从某个日期开始,特定的“操作”实际上就有效。
举例来说,John于2017年1月1日加入一家组织担任顾问,因此他被录用了,因此生效日期是2017年1月1日,他担任了一段时间的顾问,直到他成为该公司的高级顾问为止。生效日期为2017年9月6日,因此生效日期为2017年9月6日。
顺便说一句,我们还将根据雇员的职位和其他参数对雇员的薪水进行计算,因此将有派生的字段以及从其他表中引用的字段等。
现在,我的老板和解决方案架构师建议不要使用“有效日期”,我的老板说计算中会出现“问题”,但没有详细说明,解决方案架构师说,使用“开始日期”会更容易和结束日期,而不是生效日期。他的理由是,如果没有活动/事件处于活动状态的结束日期,但提供结束日期后该活动/事件将不处于活动状态。
我的问题是,我们必须维护一个我认为完全不必要的附加列。
StackOverflow的大脑信任有什么建议?
谢谢 :)
您的直觉会很好地为您服务。不要使用结束日期。这增加了可能的异常数据的复杂性和来源。采取以下顺序输入:
ID <attr> StartDate EndDate
1 ... Jan 1 Jan 20
1 ... Jan 20 Jan 22
1 ... Feb 1 Jul 30
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
现在状态重叠了。第二个状态应该持续到2月20日,但第三个状态则说它始于2月1日。但是从逻辑上说,一个状态的开始意味着前一个状态的结束。同样,我们不知道(仅通过查看数据)哪个日期是错误的。
知道一个状态的开始也表示上一个状态的结束,就可以看出当我们简单地删除EndDate列时会发生什么。
ID <attr> EffDate
1 ... Jan 1
1 ... Jan 20
1 ... Feb 1
Run Code Online (Sandbox Code Playgroud)
现在,差距和重叠是不可能的。每个状态从生效日期开始,到下一个状态开始时结束。由于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 )
);
Run Code Online (Sandbox Code Playgroud)
如果我们希望跟踪数据的更改,则可以添加一个有效的日期字段。但是,请考虑一下,诸如雇用日期和出生日期之类的数据不会从一个版本更改为另一个版本。因此,它们仅取决于ID字段。确实发生更改的数据(位置,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 )
);
Run Code Online (Sandbox Code Playgroud)
可以预计姓氏会不时发生变化,尤其是在女性雇员中。
此方法的主要优点之一是外键不能引用版本。现在,所有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;
Run Code Online (Sandbox Code Playgroud)
与查询具有开始/结束日期的表相比。
select ID, Hired, FName, Lname, Sex, BDay, Position, PayRate, DeptID
from Employees
where ID = 123
and StartDate >= GetDate()
and EndDate < GetDate();
Run Code Online (Sandbox Code Playgroud)
假定当前版本的EndDate值为魔术值,例如12/31/9999。
第二个查询看起来比第一个查询简单得多。即使如上所述对数据进行了规范化,也存在联接但没有子查询。它看起来也将执行得更快。
我已经使用这种技术大约8年了,由于性能问题,我从没有改变过它。最糟糕的是,vnf查询的运行速度比开始/结束版本慢10%。因此,一分钟的查询大约需要1分钟5秒。但是,在某些情况下,vnf查询将执行得更快。
以具有很多很多变化(许多版本)的实体为例。开始/结束查询执行索引扫描。它始于最早的版本,必须按顺序检查每个版本,直到找到EndDate小于目标日期的版本为止。通常,这是最新版本。在vnf查询中,子查询使执行索引查找成为可能。
因此,请不要拒绝这种设计,因为您认为它很慢。它并不慢。特别是当您考虑插入一个新版本仅需要一个INSERT语句时。使用开始/结束日期时,插入新版本需要先执行UPDATE,然后再执行INSERT。在两个现有版本之间插入新版本时,这是两个UPDATE和一个INSERT。要删除开始/结束版本,需要一个或两个UPDATE和一个DELETE语句。要删除vnf版本,只需删除该版本。
而且,如果两个版本之间的开始日期和结束日期不同步,则您之间会有差距或重叠,并且祝您找到正确的值。
因此,我会降低性能,以确保数据永远不会失去同步并反常。事实证明,这个(vnf)实际上是更简单的设计。
绝对执行结束日期。编写时要花很多时间,但是您只编写一次,但是您将报告很多次,并且当结束日期已经存在时,它将使一切变得更加容易(并且更快)。记录。
在整个stackoverflow上,您将发现有关编写查询以查找给定记录的结束日期(在“下一个”记录而不是“当前”记录上定义)的结束日期的问题,这些查询丑陋且缓慢
如果查看SAP等企业系统的后端,则会发现记录已定义了开始和结束日期。
关于您的同事关于不使用生效日期的评论:您没有提供太多信息,所以我猜。我猜这件事发生时有一个真正的“生效日期”,但是还有另一组开始日期和结束日期,这是该更改适用的薪资生效日期。因此,如果某人从1号开始,则工资生效日期实际上可能是15号。这也可以用于FTE计算。工资和薪资周期确实是一个大问题,而且非常复杂,因此您不应低估那里的复杂性。如果您要在此系统中包括薪资计算,那么至少您需要了解什么是有效的薪资日期。
您不必担心存储四个日期列而不是一个。数据库的存在使您轻松工作变得轻松。