如何有效地在SQL数据库中记录记录

时间:2014-01-27 14:41:34

标签: c# sql database database-design version

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

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

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#),它提供了读取和编写此类版本化记录的方法。写作相对容易,因为查询很简单,易于通过事务进行控制。但是查询需要构建一个接受SQL SELECT查询的每个片段的API,并智能地构建SQL查询以从中执行。该查询方法应该只接受一个额外的参数,该参数指定从中获取数据的时间。根据每个实体的有效范围,将选择不同的版本。

这些基本上是关于版本化数据并提供API来管理它的不完整的想法。你有没有做过这样的事情,想告诉我你的想法?你有其他想法运作良好吗?您能否就如何实施此API向我提出任何建议?虽然我理论上知道如何做,但我认为这是很多工作,我无法估计它的效果如何。

4 个答案:

答案 0 :(得分:2)

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

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

revision history

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

答案 1 :(得分:1)

我在Oracle产品中使用SQL(数据库11g)。我们有庞大的项目,版本控制是其中不可或缺的一部分。你提到的两种方法都很有用 如果您的数据库支持触发并且您可以使用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(无效)A2(无效)。

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

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

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

答案 2 :(得分:1)

我知道这是一篇过时的文章,但是我不仅想提供解决方案,而且还想与您交流我的想法,并讨论针对此重要版本控制问题的最有效解决方案。

我的想法是

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

  • 序列(递增编号)是真实标识符,用于连接
  • 创建记录时,
  • ID (自-外键)等于(序列)字段值
  • ValidFrom (记录变为活动状态的数据)
  • ValidTo (记录变为无效状态的数据)=>对于当前版本,该值为null
  • IsCurrent (标记表明该记录处于活动状态)

更新记录时

  • 更新该字段以将( ValidTo )设置为现在日期时间,并将( IsCurrent )设置为false

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

删除记录时

有效期将设置为现在时间 IsCurrent 设置为false

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

如果您对父表有FK,则可能要删除FK字段的值。

答案 3 :(得分:0)

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

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不能为null,则可以删除or条件。这也假设记录在ValidTo日期结束时有效。如果记录在ValidTo日期更改的日期开始时变为旧>&gt; =只是&gt;。

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

至于效率,我不是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