【问题标题】:Maintaining Referential Integrity - Good or Bad?保持参照完整性——好还是坏?
【发布时间】:2018-10-23 14:04:24
【问题描述】:

我们计划在我们的数据库中引入简单的审计跟踪,使用触发器和单独的历史表为每个需要审计的表。

例如考虑表 StudentScore,它有很少的外键(例如 StudentID、CourseID)将其链接到相应的父表(学生和课程)。

Table StudentScore (
    StudentScoreID, -- PK
    StudentID ref Student(StudentID),  -- FK to Student
    CourseID ref Course(CourseID),   -- FK to Course
)

如果 StudentScore 需要审计,我们计划创建审计表 StudentScoreHistory -

Table StudentScoreHistory (
    StudentScoreHistoryID, -- PK
    StudentScoreID,
    StudentID,
    CourseID,
    AuditActionCode,
    AuditDateTime,
    AuditActionUserID
)

如果 StudentScore 中的任何行被修改,我们会将旧行移至 StudentScoreHistory。

在设计讨论期间提出的一个观点是将 StudentHistory 表中的 StudentID 和 CourseID 设为 FK,以保持参照完整性。支持这一点的论点是因为我们 always 主要执行软(逻辑布尔标志)删除而不是硬删除,这有助于保持引用完整性以确保我们在审计表中没有任何孤立 ID .

Table StudentScoreHistory (
    StudentScoreHistoryID, -- PK
    StudentScoreID,
    StudentID ref Student(StudentID), -- FK to Student
    CourseID ref Course(CourseID), -- FK to Course
    AuditActionCode,
    AuditDateTime,
    AuditActionUserID
)

这对我来说似乎有点奇怪。我同意@Jonathan Leffler's comment 的观点,即审计记录不应停止删除父数据。相反,如果需要,应该通过主表中的外键而不是审计表中的外键来处理。我想听听您的意见,以确保我不会错过将外键扩展到审计表的某些价值。

现在我的问题是: 在历史表中包含这些外键是一个好的设计吗?

任何关于关键论点的细节(例如性能、最佳实践、设计灵活性等)都将受到高度赞赏。

为了任何寻求特定目的和我们环境的人的利益:

目的:

  1. 维护关键数据历史记录
  2. 允许审核用户活动并支持重新创建场景
  3. 在有限的范围内允许用户活动回滚

环境:

  • 事务性数据库
  • 并非每个表都需要审核
  • 尽可能使用软删除,特别是针对静态/参考数据
  • 很少有高度事务性的表使用硬删除

【问题讨论】:

    标签: sql database-design referential-integrity audit-trail


    【解决方案1】:

    在讨论审计时,我会回到它背后的目的。它不是真正的备份,而是过去的历史。例如,对于StudentScore,您要确保不要忘记学生最初拥有 65% 而现在拥有 95% 的事实。此审计跟踪将允许您回顾更改以查看发生了什么以及是谁做的。由此,您可以确定特定用户滥用系统的行为。在某些方面,这可能是一种备份,因为您可以将这些更改回滚到以前的状态,而无需回滚整个表。

    考虑到这一点(如果我对您使用它的假设是正确的),您唯一需要 FK/PK 关系的地方是历史表与其“实时”对应项之间。您的审计(历史)表不应引用任何其他表,因为它不再是该系统的一部分。相反,它只是一张表中发生的事情的记录。时期。您可能要考虑的唯一引用完整性是在历史表和实时表之间(因此可能存在 FK/PK 关系)。如果您允许从活动表中删除记录,请不要在历史表中包含 FK。然后历史表可以包含已删除的记录(如果您允许删除,这就是您想要的)。

    不要将主数据库中的关系完整性与此历史表混淆。历史表都是独立的。它们仅用作一个表(而不是一组表)的历史记录。

    可以将两个历史表关联在一起,并且实时表和历史表之间的关系甚至更高级(例如,同时具有实时和历史的学生和课程),因此您甚至可以处理学生被删除的可能性(不寒而栗)因为该记录仍会在历史记录表中。这里唯一的问题是如果您不保留特定表的历史记录,在这种情况下您选择丢失该数据(如果您允许删除)。

    【讨论】:

    • 是的 - 这个问题忽略了关系数据和日志文件之间的区别。应用 RDB 结构和要求没有意义 - 但日志文件有自己的结构和要求。
    • 同意您的观点,除了“可以关联两个历史表” - 看不出我们如何实现它。 CourseHistory 表中可能有多个 CourseID:15 - 我们将再次维护 FK。此外,如果我们确实维护 StudentHistory 和 Course 表之间的 FK,任何可能存在的潜在缺点(性能等)的建议。感谢您的宝贵时间!
    【解决方案2】:

    我建议不要将外键扩展到审计表。我的建议是将审计中的数据扩展到外键值。

    CourseID 不会存储为“1”,而是“HTML4”。这样,如果外键值被删除,审计表仍然有效。如果将来任何时候外键值从“HTML4”更改为“HTML5”,这也将成立。如果您只存储外键,那么您将告诉审核员以前的学生使用“HTML5”,这是不正确的。

    另一个巨大的好处是能够将审计跟踪发送到另一台服务器进行数据挖掘,而不会出现任何问题。

    我已经使用上述设置一段时间了,它对我有用。

    【讨论】:

    • 课程不仅仅是名称,它可能还有其他相关字段,包括其他表的 FK(InstructorID)。不确定扩展所有数据是否可行。我将编辑我的问题以包含此内容。
    • @YetAnotherUser 如果您需要能够返回参考表以获取其他信息,那么您必须将外键存储在审计表中。要完成这项工作,您必须实施软删除并确保不更改参考数据以使审核表无效,即为“HTML5”课程创建新记录,而不是编辑“HTML4”课程。
    • 我会将“CourseID”存储在审核表中,不确定我是否理解为什么我必须维护 FK。如果我需要回溯某个操作,我可以随时返回并查看相关表审核。
    • @YetAnotherUser 如果想保持引用完整性,那么我建议将外键约束添加到审计表中。您最终可能会在相关表上进行查找,并且数据不再存在。
    • 我喜欢审计所有表的想法,这样我们就可以始终引用 ID 指向的整个记录​​,而不仅仅是它的名称或描述。我可能存储的数据比我需要的多得多,但安全总比抱歉好,而且磁盘空间毕竟便宜。我在这里提出了一个基于触发器的审计模型:codeproject.com/Articles/1112660/…
    【解决方案3】:

    如果您需要重新创建场景,那么我会说是的,您需要 FK,并且我认为拥有它们会更容易跟踪相关的相关详细记录。但是,这会导致删除以及主键表中可能更改的信息成为问题。在这种情况下,我会说您想要删除在其他表中具有 FK 的记录,而是像您已经指出的那样使用软删除。

    至于 PK 表中的信息发生变化,请注意购买者。设置 FK 是获得一些回溯能力的简单方法,但它并不完美。有取舍。为了获得绝对完美的历史记录,您基本上需要创建所有相关记录的备份副本,只要审计候选记录发生了什么事。您需要确定合适的粒度级别并加以配合,因为完美的事件记录设置起来可能很复杂,并且在此过程中会占用大量空间。

    此外,这可能是也可能不是您的选择,但我强烈考虑使用 ApexSQL Audit + ApexSQL Log 等工具的组合,而不是自主开发的审计解决方案。根据您的需要,这两个工具结合定期归档您的事务日志将涵盖您需要做的事情。审计工具可以将数据存储在同一个db或其他地方,日志工具可以选择性地恢复数据。只是一个想法。

    【讨论】:

      【解决方案4】:

      您的里程显然会随情况而变化,但根据我的经验,请保持原始表的主键的引用完整性,仅此而已。这样可以避免历史记录中的孤立 ID,同时允许与相关表进行流畅的交互。

      假设,例如,你有这样的事情:

      table scores (
       score_id,
       student_id ref students (student_id),
       course_id ref courses (course_id),
       score_date,
       score,
       pkey (score_id)
      )
      

      在这种情况下,在 score_logs 上有一个 on delete 级联 fkey 引用 score(score_id) 是有意义的。这是对象;如果它被硬删除,还不如丢弃历史。

      相比之下,student_id 和 course_id 上的外键在我的经验中意义不大。它们意味着您不能对学生和课程进行(硬)删除——即使不存在引用它们的实时行。这可能是您想要实现的,在这种情况下忽略提示。就我而言,我发现自己需要修剪用户、cmets、产品、订单等;历史日志中的外键使这很不方便。

      另外,请注意在某些情况下 fkeys 对您不利。如果您在订单上有一个订单行,并且该订单行被删除,您仍然需要该订单行上的历史记录。在这种情况下使用的正确 pkey 是 order_id,而不是 order_line_id。

      最后一点,如果您最终选择保留 fkey:请考虑它们应该指向什么。对于解耦的数据(例如学生和课程),可以合理地假设实时行很好。但是,对于强耦合的数据(例如产品和促销),您真正想要的是同时引用 fkey 及其版本。

      关于前两点,您可能会发现这个相关的线程和答案很有趣:

      How do you create an audit trail for aggregate roots?

      【讨论】:

        【解决方案5】:

        如果您的系统真的专注于事务处理,那么我的回答可能不适用于您,但在数据仓库/BI 世界中,此问题通常通过使用“星型模式”来解决。在这种方法中,您将对链接表中的重要指示信息以及审计记录进行非规范化。这可能包括父表的 PK 值(即审计表上的 FK 值)。但是,您不会保留实际的参照完整性约束本身。

        因此,对于您的示例,您的 StudentScoreHistory 表可以保留其 StudentID 列,而无需 FK 约束,也可能保留 StudentName(或您认为可能需要 Student 的任何内容)。通过这种方式,您可以返回您的审计跟踪,将发生的事情和时间拼凑起来,而不必担心您是硬删除还是软删除父记录。这具有进一步的优势(或劣势,取决于您的观点),即在最初记录子记录时跟踪可更改的父表属性。例如,知道学生 123456(现在是 Mrs. Marriedlady)在获得生物学学位时曾经是单身女郎小姐,这可能很有用。

        【讨论】:

        • 是的——在我的例子中,它是事务数据库,但方法很有趣。我将对此进行更多探索。在您回过头来拼凑发生的事情时,在原始示例中也应该很简单,直到我们保留所有关键表/实体的审计跟踪为止。我们可以回到 StudentHistory 表并查找在给定时间点 Marriedlady 夫人被称为什么。
        【解决方案6】:

        您的实时架构强制执行关系完整性,因此您不需要历史架构中的外键。或者换一种说法:在 History 模式中的表之间强制执行外键的唯一原因是,是否有某种机制可以针对 History 模式执行 DML,而不是从实时模式中的更改中填充它。在这种情况下,您的 History 模式作为审计跟踪毫无用处。

        您提出了软删除的问题,这使问题变得混乱。仅当您考虑在两个模式之间使用外键时,这才是相关的,例如StudentScoreHistory 引用 StudentScore。这可能是一个有效的设计,但同样,它表明您不信任您的审计机制。就个人而言,我更喜欢在实时表中进行硬删除,并在历史表中记录删除的事实。软删除只是另一个悲伤的来源。

        但无论如何,这是一个不同的问题。在每个表的实时版本和历史版本之间存在外键是完全可能的,例如StudentScoreHistory -> StudentScore 也没有在历史模式中强制关系完整性,例如 StudentScoreHistory -> StudentHistory

        【讨论】:

        • 我同意我的实时模式应该强制关系完整性。我更担心拥有 FK StudentScoreHistory -> StudentStudentScoreHistory -> Course 的效用 - 这(SSH -> S)有点不干净的设计气味。
        【解决方案7】:

        作为第一次实施非常相似的审计系统,我目前面临同样的问题。我的观点与 BiggsTRC 的观点相呼应——您的“实时”表维护与课程记录的 FK 关系,而您的历史表仅维护与其“实时”对应项 (StudentScore) 的关系。我认为,这实现了审计表中没有孤儿。

        现在,还有一些我在答案中没有看到的东西:在我们当前的项目中,我们看到了在历史表中维护一个 FK 到 CourseHistory 表的价值,这样我们就知道什么是“状态” StudentScoreHistory 审核条目时的课程记录。当然,这对您来说可能重要也可能不重要,具体取决于您的系统逻辑。

        我们对您的担忧(在您对 BiggsTRC 的回答中)的解决方案是,您可能多次使用相同的 CourseId,而不是引用实际的 CourseId,而是引用 CourseHistory 表的 PK 列。我们仍然没有确定如何实现这一点 - 即使没有更改,我们是否要创建课程记录的审核条目,或者尝试引入一些逻辑来查找与相关课程匹配的 CourseHistory 记录StudentScoreHistory 条目时的状态。

        【讨论】:

          【解决方案8】:

          如果您只打算按照您的描述进行软删除,那么我认为您没有理由不使用外键。

          【讨论】:

          • 这不是让设计更加僵化而没有明显的好处吗?此外,从概念上讲,审计不应干扰系统的其余部分。如果没有审计表中的外键,我们将对系统做出更少的假设(软删除)。
          • 不要认为您的审计/历史与系统是分开的:它是系统的一部分。如果您想保持准确的历史记录,那么历史记录表中的 FK 很有用,就像实时数据表中的 FK 很有用一样。
          【解决方案9】:

          我不会为“已审核”行创建第二组表,只需将您的审核功能集成到您现有的生产模式中即可。听起来您的目的不是在给定日期/灾难时进行备份和恢复,而是跟踪每个用户或学生的更改历史记录,这是您的应用程序的功能。我认为您的附加字段很好,它们只是不需要添加到另一组表中。

          备份和恢复过程的一个问题是架构更改。架构往往会随着时间而改变,这意味着您可能无法直接从备份中恢复。如果您将审计功能内置到生产模式中,则无需担心在需要支持其他功能时会破坏任何内容。

          【讨论】:

          • 审计是否是应用程序的一项功能,或者只是让 DBA/安全管理员更加安心,真的取决于应用程序......听起来这只是额外的数据库端任何前端应用程序组件都看不到的活动。综上所述,值得考虑它是否应该是应用程序的功能!
          • @Tao,前端应用程序对审计表的可见性有限 - 以审计跟踪报告等形式。虽然,它绝对不是应用程序的功能(如果你想查看审计作为一个单独的独立模块)。
          • @Beth - 我们会将审计表保存在与主表相同的数据库中。此外,对主表的任何模式更改也将复制到历史表。您是否介意详细说明“将您的审计功能集成到您现有的生产模式中”——您的意思是将生产数据和历史数据保存在同一个表中吗?您如何维护正确的索引和 pks?这不会是主应用程序的开销吗?
          • 如果您期望频繁备份,这可能会增加主应用程序的开销。是的,我们的想法是将审计详细信息保留在与带有标记字段的主表相同的表中以区分它们,就像您对软删除所做的那样。索引和 PK 应该没问题,除非您想在审核后重用 PK?在这种情况下,您的审计标记需要成为 PK 的一部分。
          • 如果您需要恢复已审核的数据,就好像它从未被删除一样,将它们放在一起会更容易。如果可以让 IT 为恢复执行一些魔法,或者如果审计量明显影响性能,那么无论如何都要将其分开。你可以在用户不注意的时候做任何你想做的事情。
          猜你喜欢
          • 2010-09-26
          • 1970-01-01
          • 1970-01-01
          • 2013-06-17
          • 2010-12-29
          • 2017-12-30
          • 2011-09-17
          • 1970-01-01
          相关资源
          最近更新 更多