15

这个问题与可以在我的其他问题之一中找到的架构有关 基本上在我的数据库中,我存储用户、位置、传感器等。所有这些东西都可以由用户在系统中编辑,并且可以删除。

但是 - 当一个项目被编辑或删除时,我需要存储旧数据;我需要能够查看更改前的数据。

数据库中还有不可编辑的项目,例如“读数”。它们实际上更像是一个日志。读数是针对传感器记录的,因为它是特定传感器的读数。

如果我生成读数报告,我需要能够查看读数时位置或传感器的属性

基本上我应该能够重建任何时间点的数据。

现在,我之前已经完成了这项工作,并通过在每个可编辑表中添加以下列来使其正常工作:

valid_from
valid_to
edited_by

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

但是,我对用于强制外键一致性的触发器并不满意。

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

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

我见过人们提到使用特殊的历史表,但我不太喜欢为几乎每 1 个表维护 2 个表的想法(尽管它仍然可能是可能的)。

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

有人对此有任何想法吗?

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

谢谢。

4

3 回答 3

32

2011 年 1 月 1 日修订

好的,所以我所在的位置(提供完全可审计的数据库;您的特定要求)与您所在的位置之间存在差距:基于您的问题和评论。我们可能会在评论中解决这个问题。这是一个开始的位置。

  • 为了提供这个要求,根本不需要:触发器;大量复制;完整性受损;等等

  • 这也不是经典的时间要求,因此不需要“周期”功能,但您可以

  • ValidFromValidTo 是 Normalization 错误:ValidTo 是容易导出的数据;任意一行的ValidTo重复,在下一行的ValidFrom中;您有更新异常(当您更新一行中的一列时,您还必须更新下一行中的另一列);您必须为“当前”使用虚拟值。

    • 所有不必要的,仅使用 ValidFrom,并保持数据库清洁和纯 5NF。

    • 需要注意的是,如果 PostgreSQL 不能在不落入堆(ala Oracle)的情况下执行子查询,那么很好,保留 ValidTo。

所有这些东西都可以由用户在系统中编辑,并且可以删除。

嗯,不。它是一个保存重要信息的数据库;具有参照完整性,而不是暂存器,因此用户不能只是走到它并“删除”某些东西。它将与维护历史数据的相同用户要求相矛盾(在阅读中;警报;确认;行动;下载)。

  • 不允许级联删除。这些功能是非数据库、MS Access 类型的复选框。对于真实的数据库,RI 约束阻止有孩子的父母被删除。

  • 不能(不应该)更改主键。例如。用户身份; 位置标识;NetworkSlaveCode 永不改变;请记住,它们是经过仔细考虑的Identifiers。PK 的一个特点是它们是稳定的。

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

基本上,如果它是可编辑的,那么它必须是历史的(因此不包括读数和警报)。

也不包括:下载;致谢;行动。

以及参考表:SensorType;警报类型;动作类型。

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

我发现 isObselete 标志的问题是。假设您更改位置,传感器外键现在将指向一个过时的记录,这意味着您必须复制每个传感器记录。随着层次结构变大,这个问题会呈指数级恶化。

  • 好的,所以现在你明白LocationId(FK) inSensor不会改变了吗?没有大量复制等?首先没有问题(在那本愚蠢的书中有!),第二次变得更糟。

  • IsObsolete不足以满足您的要求。(参考下文)

  • UpdatedDtm任何实际行 (Reading等) 中的 标识当时有效的父 (FK 到)Sensor历史行 (its AuditedDtm)。

  • 完整的关系能力;声明性引用完整性等

  • 维护IDEF1X,强标识符的关系概念......只有一个当前父行(例如位置)

  • 历史记录中的行是当前行的图像,在它被更改之前,在声明的AuditedDtm. 当前行(非历史记录)显示最后一个 UpdatedDtm,当行更改时。

  • 显示任何给定键AuditedDtm的整个系列UpdatedDtms;因此,我用它在时间意义上“划分”了真正的密钥。

所需要的只是每个可更改表的历史表。我为四个识别表提供了历史表:位置;传感器; 网络奴隶;和用户。

请阅读本文以了解会计意义上的 Auditable

数据模型

链接到具有历史记录的传感器数据模型(第 2 页包含历史记录表和上下文)。

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

回应评论

(1)我的第一个问题是历史数据的参照完整性,因为我不确定是否存在,如果存在我不确定它是如何工作的。例如,在 SensoryHistory 中,如果您明白我的意思,则可以添加一条记录,该记录具有 UpdatedDtm 指示位置本身存在之前的日期时间。这是否真的是一个我不确定的问题 - 执行这可能是最重要的。

(您在另一个问题中提出了类似的问题。)您所经历的数据库可能实际上没有参照完整性;关系线只是为了记录;RI 是“在应用程序代码中实现的”(这意味着没有 RI)。

这是一个 ISO/IEC/ANSI 标准 SQL 数据库。这允许声明性引用完整性。每个 Relation 行都实现为 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)
        REEFERENCES Location(LocationId)
    ...
CREATE TABLE SensorHistory
    ...
    CONSTRAINT UC_PK
        PRIMARY KEY (LocationId, SensorNo, UpdatedDtm))
    CONSTRAINT Sensor_SensorHistory_fk
        FOREIGN KEY (LocationId, SensorNo)
        REEFERENCES Sensor (LocationId, SensorNo)
    ...
那些声明的约束是由服务器强制执行的;不是通过触发器;不在应用程序代码中。这意味着:

  • 不能插入 带有不存在的SensoraLocationIdLocation
  • 不能删除 包含行的LocationIdinLocationSensor
  • 不能插入 带有不存在的SensorHistoryaLocationId+SensorNoSensor
  • 不能删除 包含行的in LocationId+SensorNoSensorSensorHistory

(1.1)所有列都应该有规则和检查约束来约束它们的值范围。除了所有 INSERT/UPDATE/DELETE 都是程序化的,在存储过程中,因此不会发生意外,人们不会走到数据库并针对它运行命令(SELECTS 除外)。

一般来说,我远离触发器。如果您使用的是存储过程和正常权限,那么:

在 SensoryHistory 中,如果您明白我的意思,可以添加一条记录,该记录具有 UpdatedDtm 指示位置本身存在之前的日期时间

被阻止。因此,在 Sensor 本身之前插入带有 UpdatedDtm 的 SensorHistory 也是如此。但 procs 不是声明性规则。但是,如果您想加倍确定(我的意思是加倍,因为 INSERTS 都是通过 proc,用户直接命令),那么当然,您必须使用触发器。对我来说,这已经超过了顶峰。

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

还不确定。例如。你是否接受当 aSensor被删除时,它是最终的......(是的,历史被维护)......然后当一个新Sensor的被添加到时Location,它会有一个新的SensorNo......逻辑上没有Sensor被替换新的,有或没有时间间隔?

从最终用户的角度来看,他们应该能够通过软件不受限制地随意添加、编辑和删除传感器。但是,是的,一旦删除,它就会被删除并且无法取消删除。尽管使用完全相同的参数,但没有什么可以阻止他们稍后重新添加传感器。

和“删除” Locations, NetworkSlavesUsers以及。

行。那么新Sensor的具有相同参数的,是真正的新的,它有一个新的SensorNo,并且独立于任何以前的逻辑Sensor。我们可以IsObsolete在四个标识表中添加一个 BOOLEAN;现在确定它是足够的。删除现在是软删除。

(2.1) 对于NetworkSensorLoggerSensor,它们实际上依赖于两个父节点:如果它们的父节点中的任何一个已经过时,那么它们就已经过时了。所以给他们一个IsObsolete列是没有意义的,它具有双重含义,可以从适用的父级派生。

(2.2) 明确一点,用户不能从任何 Transaction 和 History 表中删除任何行,对吗?

(3)更新表时,在历史表中插入新行并更新主表最好用什么方法?可能只是事务中的普通 SQL 语句?

是的。这是事务的经典用法,根据 ACID 属性,它是原子的;它要么在 toto 中成功,要么在 toto 中失败(待问题解决后重试)。

(4) 参考书

权威和开创性的文本是Temporal Data and the Relational Model CJ Date, H Darwen, NA Lorentzos。就像我们这些接受 RM 的人一样,他们熟悉扩展,以及 RM 的继任者需要什么;而不是其他方法。

参考书很可怕,而且免费。PDF 不是 PDF(没有搜索;没有索引)。打开我的 MS 和 Oracle 说明;一些很好的东西藏在很多绒毛中。很多误传。不值得详细回答(如果您想要适当的评论,请打开一个新问题)。

(4.1)ValidTo除了ValidFrom。本书所犯的严重错误(如我的答案顶部所述);然后费力地解决。首先不要犯错误,其次你没有什么可解决的。据我了解,这将消除您的触发器。

(4.2) 简单的规则,同时考虑规范化和时间要求。首先,您需要深入了解 (a) 时间要求和 (b) 数据类型、正确用法和限制。始终存储:

  • 即时作为 DATETIME,例如。更新Dtm

  • 间隔为 INTEGER,明确标识列名中的 Unit,例如。间隔秒

  • 时期。取决于连接或分离。

    • 对于此要求的结合,(4.1) 适用:使用一个 DATETIME;周期的结束可以从下一行的周期的开始推导出来。
    • 对于不相干的时期,是的,您需要 2 x DATETIMEs,例如,RentedFrom以及RentedTo中间有间隙的 a。

(4.3) 他们弄乱了“临时主键”,这使代码复杂化(除了需要触发器来控制更新异常)。我已经交付了一个干净的(经过试验和测试的)临时主键。

(4.4) 他们混淆了虚拟值、非真实值和“现在”的空值。我不允许在数据库中使用这样的东西。由于我没有存储重复的ValidTo,所以我没有问题,没有什么可解决的。

(4.5) 人们不得不想知道为什么一本 528 页的“教科书”可以在网上免费获得,而且 PDF 格式很差。

(5)例如,我 [an User] 可以安静地删除所有 LocationHistory 行(仅在 Location 表中保留当前版本)——即使可能存在一个 SensorHistory 行在概念上“属于”之前版本的位置,如果这有意义的话。

这对我来说没有意义,我们必须关闭的沟通仍然存在差距。请继续互动,直到它关闭。

  • 在真实的(标准 ISO/IEC/ANSI SQL)数据库中,我们不会向用户授予 INSERT/UPDATE/DELETE 权限。我们仅授予SELECT 和 REFERENCES (对选定的用户)所有 INSERT/UPDATE/DELETE 都在 Transactions 中编码,这意味着存储过程。然后我们将每个存储过程上的 EXEC 授予选定的用户(使用 ROLES 来减少管理)。

    • 因此,没有人可以在不执行 proc 的情况下从任何表中删除。

    • 不要编写要从任何历史记录表中删除的 proc。不应删除这些行。在这种情况下,代码的不允许和不存在就是约束。

    • 从技术上讲,所有历史记录行都是有效的,没有需要关注的时期。最旧的 LocationHistory 行包含原始 Location 行在更改之前的前图像。最年轻的 LocationHistory 行是当前 Location 行的前图像。因此,中间的每个 LocationHistory 行都是有效的,并适用于中间的期间。

    • 无需“修剪”或查找可以删除的一些 LocationHistory 行,因为它们适用于未使用的 Period :它们都已使用。(明确地说,不需要检查 Location 子代到任何 LocationHistory 行的任何映射来证明这一点。)

    • 底线:用户不能从任何历史(或交易)表中删除。

    • 还是您的意思又有所不同?

    • 注意我在上面添加了(1.1)。

(6) 更正了 DM 中的一处错误。AnAlert是 的表达式Reading,不是Sensor

(7) 更正了其他问题/答案中的业务规则以反映这一点;以及这个问题中暴露的新规则。

(8) 您是否理解/欣赏,因为我们有一个完全符合 IDEF1X 的模型,所以重新标识符

  • 标识符贯穿整个数据库,保留其权力。例如。上市时Acknowledgements,可直接与Locationand连接Sensor;不必读取中间的表(如果Id使用键,则必须读取)。这就是为什么实际上在关系数据库中需要更少的连接(而在非规范化数据库中需要更多的连接)。

  • 当特定上下文相关时才需要导航子类型等。

于 2011-01-01T03:51:48.480 回答
1

我以前也遇到过这种情况。根据您尝试跟踪的数据量,这可能会令人生畏。历史表工作得很好,有时易于使用,因为您可以对历史表中的记录进行“快照”,然后根据需要在生产表中进行更改。实现起来非常简单,但是根据您拥有的数据量和更改频率,您最终可能会得到非常大的历史表。

另一种选择是记录所有允许某人“重播”发生的事情并跟踪它的更改。每个更改都会记录到一个表或一个字段(取决于您的需要)中,以跟踪谁、何时以及更改了什么,即在 2010 年 12 月 31 日,Bob 将状态从“打开”更改为“关闭”。

您要使用哪个系统通常取决于您以后需要如何保存/查看/使用数据。自动报告,由人审查,两者的某种组合等。

于 2010-12-20T16:23:24.620 回答
0

根据您的预算和/或环境,您可能需要考虑使用 Oracle 的闪回存档功能。

您可以打开表中行的自动“归档”,然后使用类似的东西在基表上运行语句

选择 *
来自重要数据
截至时间戳(SYSTIMESTAMP - 间隔“5”天)

Oracle 负责在单独的(影子)表中维护历史记录。您可以对任何表执行此操作,以便您也可以使用连接进行查询。

于 2010-12-21T13:10:34.770 回答