SQL 用于保存更改历史的架构模式
SQL schema pattern for keeping history of changes
考虑一个维护人员列表及其联系信息(包括地址等)的数据库。
有时,联系信息会发生变化。与其简单地将单人记录更新为新值,不如保留更改历史记录。
我喜欢以这样一种方式保存历史,当我查看一个人的记录时,我可以快速确定该人的数据也有更早的记录。但是,我也希望避免构建非常复杂的 SQL 查询来仅检索每个人记录的最新版本(虽然使用单个 [=24= 这可能很容易,但一旦 table 连接到其他 tables).
我想出了一些方法,我将在下面添加这些方法作为答案,但我想知道是否有更好的方法(虽然我是一个经验丰富的代码编写者,但我对数据库设计还是比较陌生,所以我缺乏经验,已经 运行 陷入了一些死胡同。
哪个数据库? 我目前正在使用 sqlite,但计划最终迁移到基于服务器的数据库引擎,可能是 Postgres。但是,我的意思是以更一般的形式提出这个问题,而不是特定于任何特定的引擎,尽管出于普遍的兴趣,也感谢如何在某些引擎中解决这个问题的建议。
添加一个 "active" 标志或添加一个 "version" 数字。
使用标志需要在涉及 table.
的每个查询的 WHERE 子句中添加条件,例如 active=1
使用版本号需要添加子查询如:
version = (SELECT MAX(version) FROM MyTable t2 WHERE MyTable.id = t2.id)
优点:
保持数据库设计简单。
检测历史条目很简单 - 只需从查询中删除额外的条件。
缺点:
更新数据需要相应地设置活动或版本值。 (虽然我猜这可以用 SQL 触发器来处理。)
使查询复杂化。虽然这可能不会影响性能,但查询越复杂,手动编写和维护此类查询就越困难,尤其是在涉及连接查询时。
这个 table 中的外键不能使用 rowid 来引用一个人,因为对这个人的更新会在 table 中创建一个新条目,从而有效地更改最新数据的 rowid这个人。
在 sqlite 中仅针对最新版本的数据维护 FTS(全文搜索)table 稍微困难一些 由于自动更新 FTS 的触发器需要考虑活动或版本值,以确保仅存储最新数据,同时删除过时数据。
将旧版本移至单独的 "history" table。
通过使用 SQL 触发器,旧数据会自动写入 "history" table。
优点:
仅请求最新数据的查询仍然很简单。
通过使用触发器,更新数据不需要关心维护历史。
在 sqlite 中仅针对最新版本的数据维护 FTS(全文搜索)table 容易因为触发器将仅附加到 "current"(非历史)table,从而避免存储过时的数据。
缺点:
检测历史条目需要解析一个单独的 table(不过这不是什么大问题)。这也可以通过将反向链接列作为外键添加到历史记录来缓解 table.
每个 table 维护历史的人都需要一个副本 table 作为历史。除非编写程序代码来动态创建这样的 "history" table,否则编写模式会很乏味。
这通常被称为 Slowly Changing Dimension 并且链接的维基百科页面提供了几种方法来使这个东西起作用。
Martin Fowler 有一个 Temporal Patterns 的列表,它们不完全是特定于数据库的,但提供了一个很好的起点。
最后,Microsoft SQL 服务器提供 Change Data Capture and Change Tracking。
很多时候,更改历史记录不必结构化,因为历史记录仅用于审计目的,实际上不需要能够对历史数据执行查询。因此,通常 只需记录对数据库所做的每个修改 就足够了,为此您只需要一个日志 table 带有一个日期时间字段和一些可变长度的文本字段,您可以在其中格式化人类可读的消息,例如谁更改了什么,旧值是什么,新值是什么。无需向实际数据添加任何内容 table,也无需向查询添加额外的复杂性。
如果您必须将历史信息保存在数据库中以便能够对其执行查询,那么我建议使用views. 将每个 table 从 "NAME" 重命名为 "NAME_HISTORY",然后创建一个名为 "NAME" 的视图,它只向您显示最新记录.如果修改 table 的代码因必须将 table 引用为 "NAME_HISTORY" 而不是 "NAME" 而负担重重,那也没关系,因为该代码可能还必须考虑事实上,它没有更新 table,它正在向它附加新的历史记录。事实上,使用视图可以防止您在不考虑历史性的情况下意外修改 table,这是一件好事。
我们使用所谓的 Verity-Block 模式。
真实性包含周期性,块包含不可变数据。
对于个人数据,我们有具有有效期的 Identity
真实性,以及包含 Name
、LastName
等数据的 IdentificationBlock
, BirthDate
块是不可变的,因此每当我们更改某些内容时,应用程序都会确保创建一个新块。
因此,如果您的姓氏在 2015 年 1 月 1 日从 Smits 更改为 Johnson,那么我们有一个从 [mindate] 到 2014 年 12 月 31 日有效的真实身份,它链接到一个 IdentificationBlock,其中 Lastname = Smits 和从 01/01/2014 到 [maxdate] 有效的身份链接到 IdentificationBlock,其中 LastName = Johnson.
所以在数据库中我们有表:
Identification
ID_Identification [PK]
Identity
ID_Identity [PK]
ID_Identification [FK]
ID_IdentificationBlock [FK]
ValidFrom
ValidTo
IdentificationBlock
ID_IdentificationBlock [PK]
ID_Identification [FK]
FirstName
LastName
BirthDate
获取当前名称的典型查询是
Select idb.Name, idb.LastName from IdentificationBlock idb
join Identity i on idb.ID_Identification = i.ID_Identification
where getDate() between i.ValidFrom and i.ValidTo
我们使用历史整数列。插入的新行始终具有 0 的历史记录,并且该条目的任何先前行的历史记录递增 1。
根据历史数据的使用频率,明智的做法是将历史行存储在单独的 table 中。如果需要组合数据,可以使用一个简单的视图,如果您通常只需要当前行,它应该会加快速度。
考虑一个维护人员列表及其联系信息(包括地址等)的数据库。
有时,联系信息会发生变化。与其简单地将单人记录更新为新值,不如保留更改历史记录。
我喜欢以这样一种方式保存历史,当我查看一个人的记录时,我可以快速确定该人的数据也有更早的记录。但是,我也希望避免构建非常复杂的 SQL 查询来仅检索每个人记录的最新版本(虽然使用单个 [=24= 这可能很容易,但一旦 table 连接到其他 tables).
我想出了一些方法,我将在下面添加这些方法作为答案,但我想知道是否有更好的方法(虽然我是一个经验丰富的代码编写者,但我对数据库设计还是比较陌生,所以我缺乏经验,已经 运行 陷入了一些死胡同。
哪个数据库? 我目前正在使用 sqlite,但计划最终迁移到基于服务器的数据库引擎,可能是 Postgres。但是,我的意思是以更一般的形式提出这个问题,而不是特定于任何特定的引擎,尽管出于普遍的兴趣,也感谢如何在某些引擎中解决这个问题的建议。
添加一个 "active" 标志或添加一个 "version" 数字。
使用标志需要在涉及 table.
的每个查询的 WHERE 子句中添加条件,例如 使用版本号需要添加子查询如:
version = (SELECT MAX(version) FROM MyTable t2 WHERE MyTable.id = t2.id)
active=1
优点:
保持数据库设计简单。
检测历史条目很简单 - 只需从查询中删除额外的条件。
缺点:
更新数据需要相应地设置活动或版本值。 (虽然我猜这可以用 SQL 触发器来处理。)
使查询复杂化。虽然这可能不会影响性能,但查询越复杂,手动编写和维护此类查询就越困难,尤其是在涉及连接查询时。
这个 table 中的外键不能使用 rowid 来引用一个人,因为对这个人的更新会在 table 中创建一个新条目,从而有效地更改最新数据的 rowid这个人。
在 sqlite 中仅针对最新版本的数据维护 FTS(全文搜索)table 稍微困难一些 由于自动更新 FTS 的触发器需要考虑活动或版本值,以确保仅存储最新数据,同时删除过时数据。
将旧版本移至单独的 "history" table。
通过使用 SQL 触发器,旧数据会自动写入 "history" table。
优点:
仅请求最新数据的查询仍然很简单。
通过使用触发器,更新数据不需要关心维护历史。
在 sqlite 中仅针对最新版本的数据维护 FTS(全文搜索)table 容易因为触发器将仅附加到 "current"(非历史)table,从而避免存储过时的数据。
缺点:
检测历史条目需要解析一个单独的 table(不过这不是什么大问题)。这也可以通过将反向链接列作为外键添加到历史记录来缓解 table.
每个 table 维护历史的人都需要一个副本 table 作为历史。除非编写程序代码来动态创建这样的 "history" table,否则编写模式会很乏味。
这通常被称为 Slowly Changing Dimension 并且链接的维基百科页面提供了几种方法来使这个东西起作用。
Martin Fowler 有一个 Temporal Patterns 的列表,它们不完全是特定于数据库的,但提供了一个很好的起点。
最后,Microsoft SQL 服务器提供 Change Data Capture and Change Tracking。
很多时候,更改历史记录不必结构化,因为历史记录仅用于审计目的,实际上不需要能够对历史数据执行查询。因此,通常 只需记录对数据库所做的每个修改 就足够了,为此您只需要一个日志 table 带有一个日期时间字段和一些可变长度的文本字段,您可以在其中格式化人类可读的消息,例如谁更改了什么,旧值是什么,新值是什么。无需向实际数据添加任何内容 table,也无需向查询添加额外的复杂性。
如果您必须将历史信息保存在数据库中以便能够对其执行查询,那么我建议使用views. 将每个 table 从 "NAME" 重命名为 "NAME_HISTORY",然后创建一个名为 "NAME" 的视图,它只向您显示最新记录.如果修改 table 的代码因必须将 table 引用为 "NAME_HISTORY" 而不是 "NAME" 而负担重重,那也没关系,因为该代码可能还必须考虑事实上,它没有更新 table,它正在向它附加新的历史记录。事实上,使用视图可以防止您在不考虑历史性的情况下意外修改 table,这是一件好事。
我们使用所谓的 Verity-Block 模式。
真实性包含周期性,块包含不可变数据。
对于个人数据,我们有具有有效期的 Identity
真实性,以及包含 Name
、LastName
等数据的 IdentificationBlock
, BirthDate
块是不可变的,因此每当我们更改某些内容时,应用程序都会确保创建一个新块。
因此,如果您的姓氏在 2015 年 1 月 1 日从 Smits 更改为 Johnson,那么我们有一个从 [mindate] 到 2014 年 12 月 31 日有效的真实身份,它链接到一个 IdentificationBlock,其中 Lastname = Smits 和从 01/01/2014 到 [maxdate] 有效的身份链接到 IdentificationBlock,其中 LastName = Johnson.
所以在数据库中我们有表:
Identification
ID_Identification [PK]
Identity
ID_Identity [PK]
ID_Identification [FK]
ID_IdentificationBlock [FK]
ValidFrom
ValidTo
IdentificationBlock
ID_IdentificationBlock [PK]
ID_Identification [FK]
FirstName
LastName
BirthDate
获取当前名称的典型查询是
Select idb.Name, idb.LastName from IdentificationBlock idb
join Identity i on idb.ID_Identification = i.ID_Identification
where getDate() between i.ValidFrom and i.ValidTo
我们使用历史整数列。插入的新行始终具有 0 的历史记录,并且该条目的任何先前行的历史记录递增 1。
根据历史数据的使用频率,明智的做法是将历史行存储在单独的 table 中。如果需要组合数据,可以使用一个简单的视图,如果您通常只需要当前行,它应该会加快速度。