如何有效地对 SQL 数据库中的记录进行版本控制

Posted

技术标签:

【中文标题】如何有效地对 SQL 数据库中的记录进行版本控制【英文标题】:How to efficiently version records in an SQL database 【发布时间】:2014-01-27 14:41:34 【问题描述】:

在至少一个应用程序中,我需要将旧版本的记录保存在关系数据库中。当应该更新某些内容时,将添加一个新副本并将旧行标记为非当前行。当应该删除某些内容时,应该将其标记为不是当前的或已删除。

有一个简单的用例:只能在当前时间添加记录的新版本,每行取代一行。这可用于在保存新数据时归档以前的记录。为此,我会在每个表中添加以下列:

VersionTime datetime -- Time when this versions becomes effective
IsCurrent bool -- Indicates whether this version is the most current (and not deleted)

如果您只需要知道记录的最新版本是什么,并且只单独列举单个记录的以前版本,这很好。时间点查询比第二种变体更痛苦。

更通用的变体是:可以在任何指定的有效时间范围内随时添加记录版本。所以我可以声明一个实体的某些设置在 2013 年底之前有效,另一个版本在 2014 年有效,另一个版本从 2015 年开始有效。这既可以用于存档旧数据(如上所述),也可以用于提前计划在未来某个时间使用不同的数据(并将此信息保存为存档)。为此,我会在每个表中添加以下列:

ValidFrom datetime -- Time when this version becomes valid (inclusive)
ValidTo datetime -- Time when this version becomes invalid (exclusive)

第二种方法基本上也可以代表第一种,但很难知道哪个版本是最新的 - 因为您还可以为将来添加版本。此外,ValidFrom/ValidTo 设计能够声明重叠范围,根据定义,ValidFrom 最高的行应适用于这种情况。

现在我想知道如何实施有效的解决方案来管理和查询此类数据。通常,您可以使用任何类型的 WHERE、GROUP BY 和 JOIN 编写任何 SQL 查询来获取您想要的记录。但是应用版本控制后,您需要考虑每条记录的正确版本。因此,不是连接另一个表中记录的每个版本,而是必须添加适当的条件以仅选择在给定时间有效的版本。

一个例子:

SELECT a, b, c
FROM t1

必须改为:

SELECT a, b, c
FROM t1
WHERE t1.ValidFrom <= :time AND t1.ValidTo > :time
ORDER BY t1.ValidFrom
LIMIT 1

表连接更复杂:

SELECT a, b, c
FROM t1
    LEFT JOIN t2 ON (t2.a = t1.a)

必须改为:

SELECT a, b, c
FROM t1
    LEFT JOIN t2 ON (t2.a = t1.a)
WHERE t1.ValidFrom <= :time AND t1.ValidTo > :time
    AND t2.ValidFrom <= :time AND t2.ValidTo > :time

这仍然无法处理选择正确版本的重叠时间跨度。我可以添加一些清理方法来消除重叠的版本时间范围,但我不知道这样做的效率如何。

我正在寻求创建一个类(在我的例子中是 C#),它提供了读取和写入此类版本化记录的方法。编写相对容易,因为查询简单且易于通过事务控制。但是查询需要构建一个 API 来接受 SQL SELECT 查询的每个片段,并智能地构建 SQL 查询以从中执行。该查询方法应仅接受一个附加参数,该参数指定从中获取数据的时间。根据每个实体的有效范围,会为每个实体选择不同的版本。

这些基本上是我关于版本化数据和提供 API 来管理它的不完整想法。你有没有做过这样的事情,想告诉我你的想法?你有另一个行之有效的想法吗?你能给我一些关于如何实现这个 API 的建议吗?虽然理论上我知道该怎么做,但我认为这需要做很多工作,而且我无法估计它的效果如何。

【问题讨论】:

我不能假装完全理解你的问题......为什么你没有一个审计表,每次更新、插入或删除发生时,每个表都填充有日期标记的记录- 并从那里获取您的数据?我想我一定是错过了什么。 我的理解是否正确:您的意思是复制表并将最近写入的数据保存在一个表中,并将所有以前的版本(带有日期)保存在另一个表中?这只会涵盖第一个更简单的场景,而不是第二个。此外,它没有说明执行时间点查询。 见***.com/questions/39281。 【参考方案1】:

如果您需要将旧数据作为业务逻辑的一部分,那么:

在主表中保存最新版本。(插入和更新,删除只会改变状态列) 在明细表发生更新时拍摄快照(在任何更新之前都会创建快照)。

另一种选择是Event Sourcing 模式。

如果旧数据只是更改的跟踪日志,那么:

Entity–attribute–value 方法可能会派上用场。一个 实现示例can be found here。

【讨论】:

【参考方案2】:

我知道这是一个老帖子,但我想回复不仅是为了提供解决方案,也是为了与您交流我的想法,并讨论针对这个重要的版本控制问题的最有效的解决方案。

我的想法是,

创建一个包含 5 个主要版本控制字段的表

序列号(递增数字)是真正的标识符,用于连接 ID(自外键)等于创建记录时的(序列)字段值 ValidFrom(记录生效的数据) ValidTo(记录变为非活动的数据)=>对于当前版本将为空 IsCurrent(指示记录处于活动状态的标志)

更新记录时

更新字段以将 (ValidTo) 设置为 NOW 日期时间并将 (IsCurrent) 设置为 false

通过递增 (Serial) 字段并保持更新记录的完全相同的字段 (ID) (ValidFrom ) 将为 NOW,(ValidTo) 将为 null 且 IsCurrent 将为 false。

删除记录时

ValidTo 将设置为现在时间 IsCurrent 设置为 false

通过这种方式,您不会遇到连接问题,因为使用字段 ID 连接表会显示所有记录历史记录。

如果您有父表的 FK,您可能希望删除 FK 字段的值。

【讨论】:

IsCurrent 插入仍然是假的?也许它应该设置为true? 可能IsCurrent 可以跳过,只要ValidTo 就足以知道它是否是最新版本...【参考方案3】:

我正在使用 Oracle 产品(数据库 11g)中的 SQL。我们有庞大的项目,版本控制是其中的重要组成部分。您提到的两种方法都很有用。 如果您的数据库支持触发器并且您可以使用 PL/SQL,您可以轻松地分离旧数据。您可以创建before updatebefore delete 触发器,然后将所有旧数据存储在特殊的历史表中(带有更改日期和类型 - 删除或更新)

假设:您要进行版本控制的所有表都必须具有主键。

伪代码:

CREATE TRIGGER TRIGGER_ON_VERSIONED_TABLE
BEFORE UPDATE
  ON VERSIONED_TABLE
BEGIN 
  INSERT INTO VERSIONED_TABLE_HISTORY_PART VALUES (:OLD.COLUMN_A, USER, TIMESTAMP);
END

如果你想要关于一个主键的所有历史数据,你可以从“生产”表中选择数据,而历史表只选择你想要的键并按时间戳排序(对于活动记录将是时间戳 SYSTIMESTAMP)。如果您想查看哪个记录处于哪个状态,您可以选择您的日期高于历史记录(或生产表)日期的第一行。

For before update trigger look here.

如果您有现有解决方案(因此,您的原始数据库模型不包含版本控制部分) 并且您想创建版本化表,或者您不能使用 PL/SQL,请使用您的方法 2。我们的工作项目(在 Oracle 数据库上)也使用这种方法。假设我们有一个包含文档的表(在现实生活中,您有一个版本标识符,它将作为该表的主键,但这只是为了说明原则)

CREATE TABLE DOC(
    DOC_NAME    VARCHAR(10)
  , DOC_NOTE    VARCHAR(10)
  , VALID_FROM  TIMESTAMP
  , VALID_TO    TIMESTAMP
  , CONSTRAINT DOC_PK PRIMARY KEY(DOCUMENT_NAME, VALID_FROM)
);

INSERT INTO doc VALUES ('A', 'FIRST VER', systimestamp, date'2999-12-31');
INSERT INTO doc VALUES ('B', 'FIRST VER', systimestamp, date'2999-12-31');

你不需要这样的地方:

WHERE VALID_FROM <= :time AND VALID_TO > :time
ORDER BY VALID_FROM LIMIT 1

因为在版本化表中,只有一个版本的记录在任何时候都有效。所以,你只需要这个:

SELECT * FROM DOC 
WHERE SYSTIMESTAMP BETWEEN VALID_FROM AND VALID_TO;

这总是只返回一行,您可以使用任何其他日期来代替 SYSTIMESTAMP。 但是您不能直接更新记录,首先,您必须更新结束时间戳(但这对您来说不是问题,正如我所见)。所以如果我更新 XK-04,我会这样做:

UPDATE doc SET VALID_TO = systimestamp 
WHERE DOC_NAME='A' AND SYSTIMESTAMP BETWEEN VALID_FROM AND VALID_TO;
INSERT INTO doc VALUES ('A', 'SECOND VER', systimestamp, date'2999-12-31');

您可以再次使用与上面相同的选择。

SELECT * FROM DOC WHERE :CUSTOM_DATE BETWEEN VALID_FROM AND VALID_TO;

最佳实践是为版本化表同时创建 ACTIVE 和 HISTORICAL 视图。 在基表中,您拥有所有数据,并且任何时候您想要实际记录都必须写BETWEEN VALID_FROM AND VALID_TO。更好的方法是创建视图:

CREATE VIEW DOC_ACTIVE 
AS SELECT * FROM DOC WHERE SYSTIMESTAMP BETWEEN VALID_FROM AND VALID_TO;

或者,如果您需要旧数据:

CREATE VIEW DOC_INACTIVE 
AS SELECT * FROM DOC WHERE NOT SYSTIMESTAMP BETWEEN VALID_FROM AND VALID_TO;

现在,代替原来的 SQL:

SELECT a, b, c FROM t1

您不需要使用复杂的结构,只需将表更改为“活动”视图(如 DOC_ACTIVE):

SELECT a, b, c FROM t1_VIEW

请也看看这个答案:Versioning in SQL Tables - how to handle it?

我不知道你是否看到了有效记录和有效“对象”之间的区别。在我们的工作项目中,我们没有任何有效的重叠范围..例如,带有文档的表,来自文档名称和版本号的主键组合...我们有文档 A(该文档的有效期为 2010 - 2050 ) 它有 2 个版本。

Document A, version 1 (2010-2020), record valid 2014-9999: VALID   (NEW)
Document A, version 2 (2021-2050), record valid 2014-9999: VALID   (NEW)

在版本 1 是从 2010 年到 2020 年有效的文档(对象版本,不是记录版本) 处于某个状态 P 的文档。此记录的有效时间是 2014-9999。

在版本 2 文档有效期为 2021 年至 2050 年(对象版本,非记录版本)此记录在 2014-9999 之间再次有效。并且文档处于状态 Q。

假设是 2016 年。您在两个版本的文档中都发现了笔误。您为两个文档版本创建到实际年份 (2016) 的新记录版本。完成所有更改后,您拥有此文档版本:

Document A, version 1 (2010-2020), record valid 2014-2015: INVALID   (UPDATED)
Document A, version 2 (2021-2050), record valid 2014-2015: INVALID   (UPDATED)
Document A, version 1 (2010-2020), record valid 2016-9999: VALID NOW (NEW)
Document A, version 2 (2021-2050), record valid 2016-9999: VALID NOW (NEW)

此后,在 2018 年,有人创建了新版本的文档,有效期仅为 2021-2030 年。 (文件在未来有效,但他的版本在今天有效)现在必须更新 VALID 版本 2 并创建版本 3。实际状态:

Document A, version 1 (2010-2020), record valid 2014-2015: INVALID   (NO CHANGE)
Document A, version 2 (2021-2050), record valid 2014-2015: INVALID   (NO CHANGE)
Document A, version 1 (2010-2020), record valid 2016-9999: VALID NOW (NO CHANGE)
Document A, version 2 (2021-2050), record valid 2016-2018: INVALID   (UPDATED)
Document A, version 2 (2031-2050), record valid 2018-9999: VALID NOW (NEW)
Document A, version 3 (2021-2030), record valid 2018-9999: VALID NOW (NEW)

我们工作项目中的所有这些操作都执行 PL/SQL 代码。 在 2018 年,如果您为有效记录选择文档,您将获得 3 行:A1 A2 A3。 如果您选择 2015 年有效的版本,您只会得到 A1(INVALID) A2(INVALID)。

因此,您拥有完整的历史记录,即使文档有 3 个有效版本,在同一点有效(记录有效性)。并且对象有效性是分开的。这是一个非常好的方法,必须满足您的所有要求。

您也可以轻松地在 VIEWS 中使用 BETWEEN 来处理具有 NULL(指示最小值或最大值)的列,如下所示:

CREATE VIEW DOC_ACTIVE AS
SELECT * FROM DOC 
 WHERE SYSTIMESTAMP BETWEEN NVL(VALID_FROM, SYSTIMESTAMP) 
                        AND NVL(VALID_TO, SYSTIMESTAMP);

【讨论】:

是的,我有主键。有趣的链接在最后。但是您的描述不处理我的第二个(更全面的)变体的重叠有效范围和未来有效范围。此外,BETWEEN 无法处理无限开始/结束时间的 NULL 值,因此我有两个条件。 我更新了我的答案,因为我需要写的东西超出了这个评论的范围。 好吧,这与我对有效范围的定义不同。我不区分数据记录和内容。以一组规则为例。直到四月,某些规则才有效。之后,将适用不同的规则。也许仅在 11 月,使用了临时规则集。这是规则集的 3 个版本。此外,我对在查询中使用视图有点犹豫。在性能方面,我有过糟糕的经历。 Oracle 不能使用视图源中的键,这会使事情变得比必要的慢。 (但我在这里不是特定于 Oracle 的。) Oracle 优化执行整个语句。因此,如果您将A_VIEW 视为SELECT X, Y FROM A_TAB WHERE Y&gt;2000,然后将此视图称为SELECT X FROM A_VIEW WHERE Y&lt;2001,Oracle 会在考虑所有条件的情况下对此进行评估。所以它和你写SELECT X FROM A_TAB WHERE Y&gt;2000 AND Y&lt;2001 .. 一样,直到你在视图中使用 secific left join 或 UNION :) 我认为这是在 SQL 中使用视图的通用方法。 .. 好的,但我想我能帮上一点忙【参考方案4】:

我曾使用过记录的跟踪版本,但从未使用过重叠范围。但是,我有在类似标准下选择记录的经验。这是一个应该做你想做的查询。

select  *
from    t1
where   VersionId = (select top 1 VersionId
                     from   t1 as MostRecentlyValid
                     where  MostRecentlyValid.ValidFrom <= @AsOfDate
                            and (MostRecentlyValid.ValidTo >= @AsOfDate
                                 or MostRecentlyValid.ValidTo is null)
                            and t1.Id = MostRecentlyValid.Id
                     order by MostRecentlyValid.ValidFrom desc)

这假定 ValidTo 也可以为 null 以指示没有结束日期。如果 ValidTo 不能为空,那么您可以删除 or 条件。这还假设记录在 ValidTo 日期结束时有效。如果记录在 ValidTo 日期开始时变旧,则 >= 改为 >。

这适用于我尝试过的少数测试数据,但我相当肯定它适用于所有情况。

至于效率,我不是 SQL 专家,所以我真的不知道这是否是最有效的解决方案。

要加入另一个表,你可以这样做

select  *
from    (select *
         from  t1
         where VersionId = (select  top 1 VersionId
                from  t1 as MostRecentlyValid
                where MostRecentlyValid.ValidFrom <= '2014/2/11'
                      and (MostRecentlyValid.ValidTo >= '2014/2/1'
                           or MostRecentlyValid.ValidTo is null)
                      and t1.Id = MostRecentlyValid.Id
                      order by MostRecentlyValid.ValidFrom desc ) ) as SelectedRecords
         inner join t2
            on SelectedRecords.Id = t2.Id

【讨论】:

以上是关于如何有效地对 SQL 数据库中的记录进行版本控制的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 PHP 和 MySQL 有效地对大型数据集进行分页?

如何务实地对配置文件进行版本控制?

如何绕过 NHibernate 版本控制更新 SQL 中的记录

如何对 SQL Server 数据库进行版本控制?

R:根据一天中的时间有效地对数据框进行子集化

如何在 javascript 中最有效地对规范化数据进行非规范化