历史/可审计数据库

16
这个问题涉及到我之前在另一个问题中提到的架构。可以在这里找到。我的数据库中存储了用户、位置、传感器和其他一些东西。所有这些都可以通过用户在系统中进行编辑和删除。但是,当一个项目被编辑或删除时,我需要存储旧数据。我需要能够查看更改之前的数据。
数据库中还有一些不可编辑的项目,比如“读数”。它们更像是日志记录。读数记录在传感器上,因为它是特定传感器的读数。
如果我生成读数报告,我需要能够看到读数时位置或传感器的属性。 基本上,我应该能够重建任何时间点的数据。
现在,我已经做过类似的事情,并通过向每个可编辑表添加以下列使其运作良好:
valid_from
valid_to
edited_by

如果valid_to = 9999-12-31 23:59:59,则这是当前记录。如果valid_to等于valid_from,则该记录已被删除。

然而,我对需要使用的触发器以确保外键一致性从未满意。

我可以通过使用“PostgreSQL”数据库的扩展来避免触发器。这提供了一种称为“period”的列类型,它允许您存储两个日期之间的时间段,然后允许您执行CHECK约束以防止时间段重叠。那可能是一个答案。

不过,我想知道是否还有其他方法。

我看到人们提到使用特殊历史表,但我不太喜欢维护几乎每一个表都要用两个表的想法(尽管这仍然可能是一种可能性)。

也许我可以简化我的初始实现,不必检查那些不是“当前”记录的记录的一致性 - 即只在valid_to是9999-12-31 23:59:59的记录上检查约束。毕竟,使用历史表的人似乎没有对这些表进行约束检查(出于同样的原因,您需要触发器)。

有人对此有什么想法吗?

PS-标题还提到可审计数据库。在我之前提到的系统中,总是有一个edited_by字段。这允许跟踪所有更改,因此我们始终可以看到谁更改了记录。不确定这会有多大差异。

谢谢。

3个回答

33

修订于 01 Jan 11

好的,基于你的问题和评论,我所在的位置(提供完全可审计的数据库)与你的位置存在差距。 我们可能会在评论中解决这个问题。 这里有一个可以开始的立场。

  • 为了满足此要求,根本不需要:触发器;大规模复制;破坏完整性等。

  • 这也不是一个典型的时间需求,因此没有“期间”功能的需要,但是您可以使用它。

  • ValidFrom ValidTo 是标准化错误:ValidTo是可以轻松推导出的数据;在任何行中,ValidTo都重复出现在下一行的ValidFrom中;您有一个更新异常(当您在一行中更新一列时,还必须在下一行中更新另一列);您必须为“当前”使用虚拟值。

    • 全部都是不必要的,只需使用ValidFrom,使数据库保持干净和纯净的5NF。

    • 但是,如果PostgreSQL不能执行子查询而不崩溃(类似于Oracle),那么请使用ValidTo。

系统中所有这些内容都可以由用户编辑和删除。

不是的。这是一个保存重要信息的数据库,具有参照完整性,而不是一个草稿本,因此用户无法仅仅走到它面前并“删除”某些内容。这将违反同一用户维护历史数据(在Reading;Alert;Ack;Action;Download中)的要求。

  • 不允许级联删除。这些功能对于非数据库、MS Access类型的复选框来说是不允许的。对于真正的数据库,参照完整性约束会阻止删除具有子级的父级。

  • 主键不能(不应该)更改。例如 UserId; LocationId; NetworkSlaveCode 从不更改;请记住,它们是经过仔细考虑的标识符之一。主键的一个特征是它们稳定。

  • 您可以添加新用户;您可以更改当前用户的名称;但是您不能删除已经在下载、确认或操作中有条目的用户。

基本上,如果可以编辑,则必须是历史记录(因此排除了reading和alert)。

也排除了:下载、确认和操作。

还有参考表:SensorType; AlertType; ActionType.

还有新的History表:它们被插入,但不能被更新或删除。

使用isObsolete标志存在问题,如果更改位置,则传感器外键将指向已过时的记录,这意味着您必须复制每个传感器记录。随着层次结构的变大,这个问题会呈指数级恶化。
  • 好吧,那么现在你明白了,在Sensor表中的LocationId(FK)不会更改;没有大规模复制等等问题吗? 首先根本不存在问题(该愚蠢的书有问题!),其次也不存在成倍恶化的问题。
  • isObsolete对您的要求不足够。(参见下文)
  • 任何真实行的UpdatedDtm(例如Reading等)都可以确定其父行(FK到Sensor的)历史记录的AuditedDtm。
  • 完整的关系能力;声明性引用完整性等。
  • 保持强标识符的IDEF1X、关系概念...只有一个当前父行(例如位置)。
  • 历史中的行是当前行的图像,在更改之前以指定的AuditedDtm显示。当前行(非历史记录)显示最后一次更新日期时间,当行发生更改时。
  • AuditedDtm显示了给定密钥的整个系列UpdatedDtms;因此,我已将其用于在时间上“分区”真实密钥。

只需要为每个可更改表提供一个历史记录表。我已经为四个标识表提供了Hiistory表:Location; Sensor; NetworkSlave; and User。

请阅读关于会计意义上的审计以便理解。

数据模型

链接到带有历史记录的Sensor数据模型(第2页包含历史记录表和上下文)

不熟悉关系建模标准的读者可能会发现 IDEF1X符号有用。

回应评论

(1) 我的首要问题是关于历史数据的引用完整性,我不确定是否存在引用完整性,如果存在,我也不确定它是如何工作的。例如,在SensoryHistory中,可能会添加一个记录,其中UpdatedDtm指示的日期时间早于位置本身存在的时间,如果你明白我的意思的话。不确定这是否真的是一个问题——强制执行可能有些过头。

(您在其他问题中提出了类似的问题。)可能是您遇到的数据库实际上没有实现引用完整性;关系线仅用于文档;引用完整性是“在应用程序代码中实现的”(这意味着没有引用完整性)。

这是一个ISO/IEC/ANSI标准SQL数据库。这允许使用声明性引用完整性。每个关系行都作为PK::FK引用实现,是一个实际的约束声明。例如:

CREATE TABLE Location
    ...
    CONSTRAINT UC_PK
        PRIMARY KEY (LocationId)
    ...
CREATE TABLE Sensor
    ...
    CONSTRAINT UC_PK
        PRIMARY KEY (LocationId, SensorNo)
    CONSTRAINT Location_Sensor_fk
        FOREIGN KEY (LocationId)
        REFERENCES Location(LocationId)
    ...
CREATE TABLE SensorHistory
    ...
    CONSTRAINT UC_PK
        PRIMARY KEY (LocationId, SensorNo, UpdatedDtm))
    CONSTRAINT Sensor_SensorHistory_fk
        FOREIGN KEY (LocationId, SensorNo)
        REFERENCES Sensor(LocationId, SensorNo)
    ...
这些声明的约束由服务器强制实施,而不是通过触发器或应用程序代码。这意味着:

  • 不能插入具有不存在于Location中的LocationIdSensor
  • Sensor中具有行的LocationId不能被删除
  • 不能插入一个在Sensor中不存在的LocationId+SensorNo的记录至SensorHistory
  • SensorHistory中具有LocationId+SensorNo的行不能被删除

(1.1) 所有列都应该有RULEs和CHECK约束来限制它们的值域。此外,所有的INSERT/UPDATE/DELETE都是通过存储过程编程完成的,因此不会发生意外事故,人们也不会直接访问数据库并运行命令(除了SELECTS)。

通常我避免使用触发器。如果您正在使用存储过程和正常权限,则此问题:

在SensoryHistory中,可能会添加一个记录,其中UpdatedDtm指示的日期时间早于位置本身存在的时间,如果你明白我的意思的话

避免操作。这也适用于在传感器本身之前插入具有更新时间戳的传感器历史记录。但存储过程并不是声明性规则。然而,如果您想要双重确认(我指的是双重确认,因为所有INSERTS都通过存储过程进行,由用户直接命令),那么您必须使用触发器。对我来说,这有点过头了。

(2)如何表示删除?我想我可以向非历史版本的表中添加标志。

还不确定。例如,当删除传感器时,您是否认为它已经被彻底删除...(是的,历史记录会被保留)...然后当将新的传感器添加到位置时,它将具有新的传感器编号...没有传感器被逻辑上替换成新的传感器,无论是否存在时间间隔?

从终端用户的角度来看,他们应该能够自由地添加、编辑和删除传感器,没有任何限制。但是是的,一旦删除就不能恢复。不过,他们以后可以重新添加完全相同参数的传感器。

并且“删除”位置、网络从设备和用户。

好的。然后,在这四个识别表中添加一个IsObsolete BOOLEAN;现在被认为是足够的。删除现在是软删除。

(2.1) 对于实际上依赖于两个父项的NetworkSensorLoggerSensor,如果其中任何一个父项过时,则它们也将过时。因此,给它们一个具有双重含义、可以从适用的父项中派生的IsObsolete列是没有意义的。

(2.2) 只是为了明确起见,用户无法从任何事务和历史记录表中删除任何行,对吗?

(3)在更新表时,插入新行到历史表并更新主表的最佳方法是什么?只需要在事务内正常使用SQL语句?

是的。这是ACID属性的经典用法,它是原子性的;它要么完全成功,要么完全失败(在问题得到解决后稍后重试)。

(4)参考书

权威而开创性的文本是C J Date、H Darwen、N A Lorentzos的《时间数据与关系模型》。就像我们这些拥抱RM的人熟悉扩展和在RM的后继产品中所需的内容一样,而不是其他方法。

这本书很糟糕,但是免费的。PDF文档不是真正的PDF(没有搜索和索引功能)。阅读这本书时,微软和甲骨文的开放是有意义的;有一些好东西淹没在很多废话中。内容有许多误导。如果您想要一份适当的评论,请另开一个问题。
  • (4.1) 除了ValidFrom之外,还需要ValidTo。这是该书所犯的严重错误(如本回答开头所指出的),然后又费力地解决。如果你一开始就不犯这个错误,那么你就不需要在第二个地方解决它。据我所知,这将消除触发器。
  • (4.2) 简单的规则,同时考虑到规范化和时间要求。首先,您需要深入了解(a)时间要求和(b)数据类型、正确用法和限制。始终存储:
    • 瞬间作为DATETIME,例如UpdatedDtm

    • 间隔作为整数,清晰地标识单位在列名中,例如IntervalSec

    • 周期。取决于并联或离散。对于并联,应用(4.1):使用一个DATETIME;下一行的周期开始时间可以从前一行的结束时间中派生。对于离散周期,是的,您需要2个DATETIMEs,例如RentedFromRentedTo之间有间隔。

  • (4.3) 他们搞乱了“时间主键”,这使得代码变得复杂(还需要触发器来控制更新异常)。我已经提供了一个干净(经过测试)的时间主键。
  • (4.4) 他们搞乱了虚拟值、非真实值和“现在”的空值。我不允许在数据库中使用这样的东西。由于我没有存储重复的ValidTo,所以我没有这个问题,也没有什么需要解决的。
  • (4.5) 人们会想为什么一本528页的“教科书”在网络上免费提供,而且还是质量很差的PDF格式。
  • (5) [一个用户]可以完全删除所有LocationHistory行(只留下Location表中的当前版本),即使可能存在一个概念上“属于”之前版本位置的SensorHistory行,如果这有意义的话。
  • 这对我来说没有意义,我们需要消除交流中的障碍。请继续交互,直到解决问题。
  • 在真正的(标准ISO / IEC / ANSI SQL)数据库中,我们不会向用户授予插入/更新/删除权限。我们只授予SELECT和REFERENCES权限(给选定的用户)。所有插入/更新/删除都是在事务中编码的,这意味着存储过程。然后我们向选定的用户授予每个存储过程的EXEC权限(使用角色来减少管理工作)。

    • 因此,没有人可以在没有执行存储过程的情况下从任何表中删除数据。

    • 不要编写用于从任何历史记录表中删除数据的存储过程。这些行不应被删除。在这种情况下,没有权限和不存在的代码是限制条件。

    • 从技术上讲,所有历史记录行都是有效的,不存在需要关注的时间段。最早的LocationHistory行包含了原始Location行在更改之前的图像。最年轻的LocationHistory行是当前Location行的图像。介于两者之间的每个LocationHistory行都是有效的,并适用于期间内的情况。

    • 无需“修剪”或查找一些可以根据未使用的期间删除的LocationHistory行:它们都已使用。(明确地,无需检查任何Location子级与任何LocationHistory行的映射即可证明。)

    • 最重要的是,用户不能从任何历史记录(或事务)表中删除数据。

    • 或者你又有其他意思吗?

    • 请注意我已添加上述(1.1)。

    (6) 在DM中更正了一个错误。警报是Reading的表达式,而不是Sensor。

    (7) 更正了另一个问题/答案中的业务规则,以反映在此问题中公开的新规则。

    (8) 您是否理解并欣赏,由于我们拥有完全符合IDEF1X标准的模型,因此关于标识符

    • 标识符贯穿整个数据库,保留其功能。例如,在列出Acknowledgements时,它们可以直接与Location和Sensor连接;中间的表不必读取(如果使用Id键,则必须读取)。这就是为什么在规范化的关系型数据库中需要更少的连接(在非规范化的数据库中需要更多的连接)。

    • 只有在特定的上下文相关时才需要导航子类型等。


    @Mark(“取消”是我描述的词)。这是你的系统,你需要告诉我。根据你的观点,它可以通过(a)确认(b)任何操作(确认是第一个操作)(c)特定操作(d)良好的读数来“取消”。在其他一些情况下(例如传感器故障,没有进一步的读数),条件是永久性的,无法“取消”。除非你确定了一个新的要求,否则不需要另一个表格。数据库只是事实;处理是派生的,暂时的。同时简单和完整可能会让你感到不安。 - PerformanceDBA
    @Mark。1)你说的有道理,但是它变得比必要的复杂。警报不是传感器的子类(如果是这样,当传感器在范围内时会允许警报)。警报是读数的子类,特别是超出范围的读数。读数继续在一个路径上记录;确认和操作在另一个路径上记录;它们各自独立。 - PerformanceDBA
    @Mark。1)由于历史问题已经关闭,我已经完成了该页面的最终版本。2)关于警报的讨论澄清了动词短语。3)开关异或值需要更改DM,这不再适合一页。4)因此,我将它们按主题区域排列,并提供了ERD以进行完整的概述和导航(请尝试一下)。 - PerformanceDBA
    @Mark。是的。Location 中的UpdatedDtm被设置为当前日期和时间。 LocationHistory是Location纯粹的before-image,包括其未更改的UpdatedDtm。这与时间要求一致。 - PerformanceDBA
    @Mark. is_obsolete: 否。以上行中的列有一个PRIMARY KEY索引,每个Alternate Key“AK”都有一个UNIQUE索引。没有部分索引。如果location_name“白金汉宫”已被软删除,则不能重新使用该名称。如果您想要重新使用,请考虑其对子代和历史的影响,并反转is_obsolete。 - PerformanceDBA
    显示剩余32条评论

    1
    我之前也遇到过这种情况。根据你要跟踪的数据量,它可能会让人望而生畏。历史表有时很容易使用,因为你可以在历史表中拍摄记录的“快照”,然后在生产表中根据需要进行更改。实现起来非常简单,但是取决于你有多少数据以及它们变化的频率,你可能会得到非常大的历史表。
    另一个选择是记录所有允许某人“重播”发生的更改并跟踪它。每个更改都被记录在一个表或字段(根据你的需求)中,该表或字段跟踪谁、何时以及将什么更改为什么,例如:2010年12月31日,Bob将状态从“打开”更改为“关闭”。
    你想使用哪个系统通常取决于你以后需要如何保留/审查/使用数据。自动报告、人员审核、两者的组合等。

    表格的行数大小并不是问题所在。使用当前方法,只使用一个表格也可能会遇到同样的问题。 - Mark
    这就需要在每个表中拥有多少列和每次更改时是否添加日志/审计条目之间进行平衡。如果表格只有少数列,那么复制记录不太困难。但是如果拥有30个大型列,则可以通过将每个更改作为单独的记录记录来节省空间。 - Thyamine

    0
    根据您的预算和/或环境,您可能希望考虑使用Oracle的闪回归档功能。
    您可以在表中启用自动“归档”行,并使用类似以下语句在基表上运行:
    SELECT * FROM important_data AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '5' DAY)
    Oracle会负责在一个单独的(影子)表中维护历史记录。您可以对任何表执行此操作,以便进行带有连接的查询。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接