【问题标题】:Should I use Effective Date or Start Date and End Date for historical recording?历史记录应该使用生效日期还是开始日期和结束日期?
【发布时间】:2017-05-22 12:23:10
【问题描述】:

我是一名业务分析师,并为我们正在实施的系统准备了表格/erd。

上下文本质上是一个员工管理系统,员工可以加入公司、更改职位、升职、降职、终止等。所有这些都需要进行跟踪以进行过滤和报告。因此,我们需要对记录进行历史跟踪。

我的建议和表格的原始设计包括一个名为“生效日期”的字段,因此从某个日期开始,一个特定的“操作”基本上是有效的。

例如,John 于 2017 年 1 月 1 日加入一个组织担任顾问,因此行动是他被聘用,因此生效日期是 2017 年 1 月 1 日,他在一段时间内担任顾问,直到他成为2017 年 9 月 6 日担任高级顾问,因此生效日期为 2017 年 9 月 6 日,并为此记录晋升。

顺便说一下,我们还将根据员工的职位和其他参数对员工的薪水进行计算,因此会有派生字段和从其他表等引用的字段。

现在我的老板和解决方案架构师建议不要使用“生效日期”,我的老板说计算会有“问题”但没有详细说明,解决方案架构师说它更容易使用开始日期和结束日期,而不是生效日期。他的理由是,如果没有结束日期,则表示动作/事件处于活动状态,但一旦提供结束日期即处于非活动状态。

我的问题是我们必须维护一个我认为完全没有必要的额外专栏。

StackOverflow 的智囊团有何建议?

谢谢:)

【问题讨论】:

  • 是否有机会使用临时表功能 (msdn.microsoft.com/en-us/library/mt631669.aspx)?
  • 这个问题最好在dba.se 上提出。
  • 感谢大家的回复!
  • 您的解决方案架构师是对的。您正在解决业务管理系统中的一个非常常见的需求。需要结束日期,因为 (1) 一个职位后面可能不会有另一个职位,并且 (2) 没有它,构建查询将非常困难。

标签: sql sql-server date database-design


【解决方案1】:

明确执行结束日期。编写时工作量要多一点,但你只写一次,但你会多次报告它,你会发现当结束日期已经存在时,它让一切变得更容易(和更快)记录。

在整个 stackoverflow 中,您都会发现有关编写查询以查找给定记录的结束日期的问题,该记录是在“下一个”记录而不是“当前”记录上定义的这些查询

如果您查看 SAP 等企业系统的后端,您会发现记录已定义开始和结束日期。

关于你的同事 cmet 关于不使用生效日期:你没有提供太多信息,所以我猜。我猜想事情发生时有一个真正的“生效日期”,但还有另一组开始和结束日期,这是更改适用的工资单生效日期。因此,如果有人从 1 日开始,工资单的生效日期实际上可能是 15 日。这也可能用于 FTE 计算。工资单和支付期真的很重要,而且相当复杂,所以你不应该低估那里的复杂性。如果您在此系统中包含工资计算,那么至少您需要了解有效的工资单日期是什么。

您不应该害怕存储四个日期列而不是一个。数据库可以让您轻松而不难。

【讨论】:

  • 非常感谢! :) 我想我喜欢“生效日期”的一个原因是因为 PeopleSoft 在其后端使用的就是这个。但你的答案是完美的,我会选择开始/结束日期组合。
  • 只要它们一致,它们都是有效的名称。比如 EffectiveStartDate、EffectiveEndDate、PayrollStartDate、PayrollEndDate。
【解决方案2】:

你的直觉很适合你。不要使用结束日期。这增加了可能的异常数据的复杂性和来源。采取以下顺序条目:

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) 确实是更简单的设计。

【讨论】:

  • 感谢您提供的广泛信息,这是非常好的详细信息,很适合我了解。我会让开发人员知道,但最终由他们决定实施。
  • 恕我不同意,我认为这里的逻辑有两个问题:1)对间隙和重叠日期的关注应该由将数据插入这些表的程序中的逻辑来管理 - 如果这些条件不'逻辑上不可能,那些程序不应该允许它们。 2)我不确定删除结束日期会消除差距问题。如果有的话,它只是隐藏它们 - 数据中可能会出现间隙,但它们不会在数据库中记录下来。事实上,如果您需要,我不确定这种方法能否为您提供一种模拟差距的方法。
  • 1) 我只能说,祝你好运!每天都要花费大量的开发、测试和 cpu 时间来尝试开发代码,以确保只有可行的数据才能进入数据库。仅维护工作就可能相当艰巨。然后,数据库开发人员运行一个脚本,进行绕过所有代码的更改。良好数据设计的第一定律:在尽可能低的位置确保数据完整性。应用代码是最高的地方。 2) 是的,如果允许或需要间隙和/或重叠,则此设计将不起作用。但是要模拟一个完整的状态变化链,我认为没有比这更好的了。
  • 另外,如果开始/结束日期显示有差距,没有办法,只看数据,确定是否1)上一行的结束日期是错误的,2)开始下一行的日期是错误的,或者 3) 两者之间缺少一行。使用一个日期,任何日期都可能是错误的,或者在任何两个记录之间存在缺失记录。某些数据是错误的以及正确的数据应该是什么,这一事实必须发生在数据库之外。但是一个单一的日期可以保证,即使是错误的,数据在任何时候都是有效的。仅此一项就可以大大简化应用逻辑。
  • 感谢您的回复。我正在寻找类似的东西,它帮助了我。尽管从 cmets 可以看出答案可能值得商榷。
【解决方案3】:

使用startDateendDate 会使更新变得混乱,但它有助于更​​轻松、更快速地获取有效日期。

异步更新同一条记录可能会导致日期重叠,因为我们需要获取更新范围内的所有记录并单独更新这些记录。

另一方面,使用effectiveDate 只会加快更新过程,并且会终止日期重叠的问题。但是这种方式 fetch 似乎太复杂了。

例如:

ID  Data  EffDate  
1   ...   Jan 1 2020
1   ...   Jan 30 2020
1   ...   Feb 1 2020

在上面的例子中,如果我们想要获取生效日期为 2 月 1 日的记录,我们必须比较前 3 条记录以匹配最高日期(如果我们正在获取列表,这是不可能的)。那样的话,加入其他有效日期的表格会很麻烦。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-08-19
    • 2023-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-23
    • 2022-01-23
    • 1970-01-01
    相关资源
    最近更新 更多