数据模型设计的过程中往往都需要考虑时间这个因素,在之前文章里讨论过本质标识符的概念以及可以使用5W1H的方法寻找本质标识符,在5W当中就包含了“When”这个时间的概念。
保存到数据库中的数据不是一成不变的,甚至随着时间的变化而产生的变化也同样具有重要的管理意义和价值。
简单来讲,履历管理就是对不同时间数据变更的管理,而随着管理方式、系统类型以及业务上的需要和性能上的需求不同,履历设计的方法也不尽相同,模型的表达也不一样。
大多系统中都会有历史表,历史表都是履历管理的产物,这个没错,不过,这不是履历的管理的全部。因为,有些系统中历史表的作用只是备份数据,而履历管理包含了备份的概念,只不过更多的是从业务角度需求的角度来设计的。
希望能通过这篇文章来整理一下履历管理的一些概念给对数据模型设计感兴趣的同学们。
点履历和线段履历
对时间的管理可以以某个时间点为单位,也可以以某个时间段为单位来管理,所以,点履历和线段履历的概念也就是这么简单,点履历就是以某个时间点来记录某一个事件,而线段履历则是以开始时间和结束时间的组合来管理某一种状态。
例如,客户下订单的时候会记录下订单的时间,在设计订单实体的时候中,比如会有一个订购时间的属性,这就是点履历的形式。而像个人简历一样,需要记录从什么时候开始到什么时候做了什么事情,这样的记录方式自然就是线段履历的形式了,而且,对于当前还在进行中的事情和当前的状态会使用一个默认值(99991231)或未来的某个时间点等方式来表示。
点履历更像是日志信息,某个时间点,发生了什么时间或者状态了什么状态;线段履历更像是一条历史长河,记录了什么时代从什么时间开始到什么时候结束。
在设计数据模型的时候,对这两种方式的选择要根据情况而定。记录的频率过于高的话,不适合用线段履历,线段履历的变更过程相对于点履历要繁琐。点履历只需要不断地写入什么时间怎么样了就可以,但是,线段履历就必须先找到当前的那一行,那结束时间改为当前时间,再将以当前时间为开始时间和“99991231”这样的时间作为结束时间再插入一条新的数据,这个过程就好像对时间线段进行切割一样。
点履历和线段履历的标识符
履历标识符的基本设计方法
对于点履历来说,标识符的设计很简单,在原有基础标识符的基础之上加入事件发生的时间即可,而线段履历则会出现多个选择,线段履历由基础标识符+开始时间+结束时间构成,那么将时间因素包含到标识符的方式就会有三种可能,只将开始时间包含到标识符中、只将结束时间包含到标识符中和将开始时间和结束时间一起包含到标识符中,如果将开始时间和结束时间都包含到标识符当中的话,开始时间和结束时间的顺序也有讲究。所以,在模型中设计的时候就会出现四种情况:开始时间、结束时间、开始时间+结束时间、结束时间+开始时间。
这几种方式用哪一种呢?方式的选择需要根据业务的表达进行选择。如果只将开始时间包含进去的话,则表示开始时间相同结束时间不同的情况会出现,反过来只将结束时间包含到标识符中的话,则表示结束时间相同开始时间不同的情况会出现。将开始时间和结束时间同时包含到标识符中的话,则表示开始时间和结束时间必须不一样。
如果将开始时间和结束时间都包含到标识符中的话,又需要考虑是将开始时间放在前面还是将结束时间放在前面呢?这个需要根据查询的需求来定了,简单来说,经常需要查询当前记录或者大部分靠近当前时间的数据的话,将结束时间放到前面,会更好一些,反之则将开始时间放在前面。这样做的原因跟索引有着密切的关系,至于具体的原因以后在说明索引相关的内容时候再谈吧。
线段重合的问题
将开始时间和结束时间都包含到标识符中的话,虽然,可以表示开始时间和结束时间必须不一样,但是,需要注意的是这样的线段从数据的识别上看起来没有问题,而从逻辑角度来看,并不一定是唯一的,可能会存在线段重合的情况。
例如,20170101 ~ 20170201和 20170102 ~ 20170202 这两个时间线段通过约束关系来校验的话,因为开始时间和结束时间都不一样,所以,它们确实是唯一的,但是,业务角度来看的话,它们却是冗余的。如果说实体中记录了某个人户籍相关的信息,20170101 ~ 20170201 这段时间使用的户籍地址是北京,同时 20170102 ~ 20170202这段时间使用的户籍地址是上海,而户籍地址同时只能存在一个地方啊,这样的话是不是就是数据质量的问题了?
这样的情况仅通过数据库中提供的基础约束条件是没有办法实现的,最好是在程序的逻辑上进行校验。所以,在数据质量校验的过程中也会对一些特有业务中的这些线段重合的问题进行检验。
履历管理的粒度
不管是点履历还是线段履历,到这里只说了履历表达的方式,而从不同时间段实际数据内容管理的角度来看,就需要考虑一些历史信息管理时的粒度了。假如,客户相关的信息的每次变更都需要在系统中记录,客户相关的基本信息那么多,需要怎么管呢?是变更了哪个值就管理哪个值?还是每次变更都需要将所有的数据都管理起来?
因此就产生了履历管理的粒度相关的内容了。粒度可以分为以下三种:
- 属性
- 实例
- 主题
比如说,客户的名称会经常发生变化,可以将客户名称这个属性单独拿出来做一个客户名称变更履历来管理,这个可以说是属性级别的粒度。
如果每次变更都需要把所有的属性都拿出来单独记录一份,记录的粒度是实体中整个一个实例的话,那就是实例级别。
主题级别的粒度则可以按照业务和变更频率这两个角度来看,比如说,客户地址信息分别分为了基础地址(国家、省份、城市、区、阶段)和详细地址(具体多少号、什么小区、哪栋楼、几零几等)方式的变更可以划分为一个主题级别,每次修改都需要记录多个属性,那这就算是一个主题级别的了,当然,很多时候也会根据变更的频率相似度进行划分。
三种方式的选择可以根据存储和使用的便利性来看,如果每次变更只变更某一个属性还采用实例级别的话,自然会产生其他未变更属性的冗余,而且,如果我要之前跟前次相比变更了什么的话,需要对整行的属性进行比对了之后才知道,这样的话,还是属性级别比较方便一点。而最折中的方式就是主题级别的了,根据实际情况来决定会变更什么,设计起来需要一个一个来调查和设计就是了。
实际使用和模型的一些内容
更加灵活的属性粒度履历管理方式
属性级别的履历管理方式中,拿客户名称作为例子说明过了,如果这样的话,客户属性那么多,是不是每一个属性都需要设计一个变更履历的实体?为了解决这个问题说过了实例级别和主题级别两个概念,但是,实例级别变更时可能会导致不必要变更数据的冗余,而按主题级别管理的话,需要根据业务和更新的频率来决定,那如果某些属性既不是同一个业务主题,更新的频率也差异很大的话,怎么办?
所以,利用属性元数据模型设计的思路,可以单独将变更的属性和变更前后的值单独列出来一起存放就可以。
比如说,设计一个实体,继承了客户实体的标识符,在这个基础上,再添加一个变更属性名称、变更前属性值和变更后属性值,这样的话,只需要在发生变更时,先记录上变更属性的名称再记录前后值就可以了。无论多少个属性,都可以被包含到这个实体当中。
将当前信息和历史信息分开管理的方式
这个只是我想说明的一种方式,有时候会将当前的数据值保存在主实体当中,而变更的时候,将之前的值单独放入履历中,更新当前主实体里的具体值。
当前信息和历史信息合并管理,但履历属性不继承给下级实体的模型表达方式
假设,客户实体中的每次变更都会以履历的方式记录到客户实体里,也就是说,主实体和履历实体合二为一了,那么客户实体里的主键中不仅包含了客户号码,还包含了开始时间和结束时间这样的履历属性,主键是会继承给子实体的,如果这样设计的话,所有的子实体中都会包含客户实体中的客户号码、开始时间和结束时间,这个时候,在模型设计的过程中,可以通过一个标记来标明开始时间和结束时间不继承给子实体,这个标记就是“非继承”标示符,在模型中用美元符号($)表示。
线段合并
线段合并是一种对使用交叉线段选取同一时间点不同数据的SQL处理手段,这部分内容,等回头在SQL相关内容中再单独说明吧。
总结
在数据模型设计的过程中,比如会出现对履历进行管理的各种情况,但是,基本的方式和概念理解了之后,对于各种需求就可以灵活的设计,并且,对现有模型进行分析的过程中,也会更容易地理解设计者的意图是什么、会出现什么样的问题、设计的合理还是不合理。
履历相关的知识不仅仅是数据模型设计人员必备的基础,而且,对所有跟数据打交道的人来说,都有理解这部分知识的意义。