【问题标题】:Database Design for Revisions?用于修订的数据库设计?
【发布时间】:2010-09-07 13:05:41
【问题描述】:

我们在项目中要求将实体的所有修订(更改历史)存储在数据库中。目前我们为此设计了 2 个提案:

例如对于“员工”实体

设计 1:

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- Holds the Employee Revisions in Xml. The RevisionXML will contain
-- all data of that particular EmployeeId
"EmployeeHistories (EmployeeId, DateModified, RevisionXML)"

设计 2:

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- In this approach we have basically duplicated all the fields on Employees 
-- in the EmployeeHistories and storing the revision data.
"EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName, 
      LastName, DepartmentId, .., ..)"

还有其他方法可以做这件事吗?

“设计 1”的问题在于,每次需要访问数据时,我们都必须解析 XML。这会减慢进程并增加一些限制,例如我们无法在修订数据字段上添加连接。

“设计 2”的问题在于,我们必须在所有实体上复制每个字段(我们有大约 70-80 个实体需要维护修订)。

【问题讨论】:

标签: sql database database-design versioning


【解决方案1】:

我认为这里要问的关键问题是“谁/什么将使用历史记录”?

如果它主要用于报告/人类可读的历史记录,我们过去已经实施了这个方案......

创建一个名为“AuditTrail”的表或具有以下字段的表...

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[OldValue] [varchar](5000) NULL,
[NewValue] [varchar](5000) NULL

然后,您可以将“LastUpdatedByUserID”列添加到所有表中,每次对表执行更新/插入时都应设置该列。

然后,您可以向每个表添加一个触发器,以捕获发生的任何插入/更新,并在此表中为每个更改的字段创建一个条目。因为该表还为每个更新/插入提供了“LastUpdateByUserID”,所以您可以在触发器中访问此值并在添加到审计表时使用它。

我们使用 RecordID 字段来存储正在更新的表的键字段的值。如果是组合键,我们只需在字段之间使用“~”进行字符串连接。

我确信这个系统可能有缺点 - 对于大量更新的数据库,性能可能会受到影响,但对于我的 web 应用程序,我们得到的读取比写入多得多,而且它的性能似乎相当不错。我们甚至编写了一个小的 VB.NET 实用程序来根据表定义自动编写触发器。

只是一个想法!

【讨论】:

  • 不需要存储NewValue,因为它存储在审计表中。
  • 严格来说,确实如此。但是 - 当在一段时间内对同一字段进行多次更改时,存储新值会使诸如“向我显示 Brian 所做的所有更改”之类的查询变得更加容易,因为有关一次更新的所有信息都保存在一个记录。只是一个想法!
  • 我认为sysname 可能是更适合表名和列名的数据类型。
  • @Sam 使用 sysname 不会添加任何值;它甚至可能令人困惑...stackoverflow.com/questions/5720212/…
【解决方案2】:
  1. 不要不要将它们全部放在一个带有 IsCurrent 鉴别器属性的表中。这只会导致问题,需要代理键和各种其他问题。
  2. Design 2 在架构更改方面确实存在问题。如果您更改Employees 表,您必须更改EmployeeHistories 表和所有相关的sprocs。可能会使您的架构更改工作量翻倍。
  3. 设计 1 运行良好,如果做得好,在性能损失方面不会花费太多。您可以使用 xml 架构甚至索引来克服可能的性能问题。您关于解析 xml 的评论是有效的,但您可以使用 xquery 轻松创建视图 - 您可以将其包含在查询中并加入。像这样...
CREATE VIEW EmployeeHistory
AS
, FirstName, , DepartmentId

SELECT EmployeeId, RevisionXML.value('(/employee/FirstName)[1]', 'varchar(50)') AS FirstName,

  RevisionXML.value('(/employee/LastName)[1]', 'varchar(100)') AS LastName,

  RevisionXML.value('(/employee/DepartmentId)[1]', 'integer') AS DepartmentId,

FROM EmployeeHistories 

【讨论】:

  • 你为什么说不要用 IsCurrent 触发器将它全部存储在一个表中。您能否指出一些可能会出现问题的示例。
  • @Simon Munro 主键或集群键怎么样?我们可以在 Design 1 历史表中添加什么键以加快搜索速度?
  • 我假设一个简单的SELECT * FROM EmployeeHistory WHERE LastName = 'Doe' 会导致全表扫描。不是扩展应用程序的最佳主意。
【解决方案3】:

Database Programmer 博客中的 History Tables 文章可能很有用 - 涵盖了此处提出的一些要点并讨论了增量的存储。

编辑

History Tables 的文章中,作者 (Kenneth Downs) 建议维护至少七列的历史表:

  1. 更改的时间戳,
  2. 进行更改的用户,
  3. 用于标识已更改记录的标记(其中历史记录与当前状态分开维护),
  4. 更改是插入、更新还是删除,
  5. 旧值,
  6. 新值,
  7. 增量(用于数值更改)。

不应该在历史记录表中跟踪从不更改或不需要其历史记录的列,以避免膨胀。存储数值的增量可以使后续查询更容易,即使它可以从旧值和新值派生。

历史记录表必须是安全的,防止非系统用户插入、更新或删除行。仅应支持定期清除以减小整体大小(如果用例允许)。

【讨论】:

    【解决方案4】:

    我们实施的解决方案与 Chris Roberts 建议的解决方案非常相似,而且对我们来说效果很好。

    唯一的区别是我们只存储新值。旧值毕竟存储在上一个历史记录行中

    [ID] [int] IDENTITY(1,1) NOT NULL,
    [UserID] [int] NULL,
    [EventDate] [datetime] NOT NULL,
    [TableName] [varchar](50) NOT NULL,
    [RecordID] [varchar](20) NOT NULL,
    [FieldName] [varchar](50) NULL,
    [NewValue] [varchar](5000) NULL
    

    假设您有一个包含 20 列的表。这样,您只需存储已更改的确切列,而不必存储整行。

    【讨论】:

      【解决方案5】:

      避免设计 1;一旦您需要例如回滚到旧版本的记录,它就不是很方便 - 自动或使用管理员控制台“手动”。

      我并没有真正看到设计 2 的缺点。我认为第二个历史表应该包含第一个记录表中存在的所有列。例如。在 mysql 中,您可以轻松地创建与另一个表 (create table X like Y) 具有相同结构的表。而且,当您要更改实时数据库中 Records 表的结构时,无论如何您都必须使用 alter table 命令 - 也无需为您的 History 表运行这些命令。

      注意事项

      • 记录表仅包含最新修订;
      • 历史记录表包含记录表中记录的所有以前的修订;
      • 历史表的主键是添加了RevisionId列的Records表的主键;
      • 考虑其他辅助字段,例如ModifiedBy - 创建特定修订的用户。您可能还希望有一个字段 DeletedBy 来跟踪谁删除了特定的修订。
      • 想一想DateModified 应该是什么意思 - 要么表示此特定修订版的创建位置,要么意味着此特定修订版何时被另一个修订版替换。前者要求字段在Records表中,乍一看似乎更直观;然而,第二种解决方案似乎更适用于已删除的记录(删除此特定修订的日期)。如果您选择第一个解决方案,您可能需要第二个字段DateDeleted(当然只有当您需要它时)。取决于您以及您实际想要录制的内容。

      设计 2 中的操作非常简单:

      修改
      • 将记录从 Records 表复制到 History 表,为其赋予新的 RevisionId(如果 Records 表中不存在),处理 DateModified(取决于您如何解释它,请参阅上面的注释)
      • 继续正常更新记录表中的记录
      删除
      • 与修改操作的第一步完全相同。根据您选择的解释,相应地处理 DateModified/DateDeleted。
      取消删除(或回滚)
      • 从历史表中获取最高(或某些特定?)修订并将其复制到记录表中
      列出特定记录的修订历史
      • 从历史表和记录表中选择
      • 想一想您对该操作的期望;它可能会从 DateModified/DateDeleted 字段中确定您需要哪些信息(请参阅上面的注释)

      如果您选择设计 2,那么执行此操作所需的所有 SQL 命令都将非常简单,而且维护也非常简单!也许,如果您在 Records 表中也使用辅助列(RevisionIdDateModified)会容易得多 - 使两个表保持完全相同的结构(唯一键除外)!这将允许简单的 SQL 命令,这将容忍任何数据结构的变化:

      insert into EmployeeHistory select * from Employe where ID = XX
      

      别忘了使用交易!

      至于缩放,这个解决方案非常高效,因为您不需要从 XML 来回转换任何数据,只需复制整个表行 - 非常简单的查询,使用索引 - 非常高效!

      【讨论】:

        【解决方案6】:

        如果您必须存储历史记录,请使用与您正在跟踪的表相同的架构以及“修订日期”和“修订类型”列(例如“删除”、“更新”)创建一个影子表。编写(或生成 - 见下文)一组触发器来填充审计表。

        制作一个工具来读取表的系统数据字典并生成一个脚本来创建影子表和一组触发器来填充它是相当简单的。

        不要尝试为此使用 XML,XML 存储的效率远低于此类触发器使用的本机数据库表存储。

        【讨论】:

        • +1 为简单起见!有些人会因为害怕以后的变化而过度设计,而大多数时候实际上并没有发生任何变化!此外,管理一张表中的历史记录和另一张表中的实际记录比将它们全部放在一个带有某些标志或状态的表中(噩梦)要容易得多。它被称为“KISS”,通常会长期奖励您。
        【解决方案7】:

        Ramesh,我参与了基于第一种方法的系统开发。
        事实证明,将修订存储为 XML 会导致数据库的巨大增长并显着减慢速度。
        我的方法是每个实体有一个表:

        Employee (Id, Name, ... , IsActive)  
        

        其中 IsActive 是最新版本的标志

        如果您想将一些附加信息与修订相关联,您可以创建单独的表格 包含该信息并使用 PK\FK 关系将其与实体表链接。

        这样您可以将所有版本的员工存储在一个表中。 这种方法的优点:

        • 简单的数据库结构
        • 由于表变为仅追加,因此没有冲突
        • 您只需更改 IsActive 标志即可回滚到以前的版本
        • 无需连接即可获取对象历史记录

        请注意,您应该允许主键是非唯一的。

        【讨论】:

        • 我会使用“RevisionNumber”或“RevisionDate”列来代替 IsActive,这样您就可以按顺序查看所有修订。
        • 我会使用“parentRowId”,因为这样可以让您轻松访问以前的版本,并能够快速找到基数和终点。
        【解决方案8】:

        我过去看到的这种做法是有

        Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );
        

        您永远不会在此表上“更新”(除了更改 isCurrent 的有效性),只需插入新行。对于任何给定的 EmployeeId,只有 1 行的 isCurrent == 1。

        维护这一点的复杂性可以通过视图和“而不是”触发器来隐藏(在 oracle 中,我认为其他 RDBMS 类似),如果表太大且无法处理,您甚至可以转到物化视图按索引)。

        这个方法没问题,但是你可能会遇到一些复杂的查询。

        就我个人而言,我非常喜欢您的设计 2 方式,这也是我过去的方式。它易于理解、易于实现和易于维护。

        它还为数据库和应用程序创建了非常少的开销,尤其是在执行读取查询时,这很可能是您 99% 的时间要做的事情。

        自动创建历史表和触发器来维护也很容易(假设它是通过触发器完成的)。

        【讨论】:

          【解决方案9】:

          数据修订是时态数据库“valid-time”概念的一个方面。对此进行了大量研究,并出现了许多模式和指南。我写了一个很长的回复,其中包含大量对this 问题的引用,供有兴趣的人参考。

          【讨论】:

            【解决方案10】:

            我将与您分享我的设计,它与您的两种设计的不同之处在于,每种实体类型都需要一个表。我发现描述任何数据库设计的最佳方式是通过 ERD,这是我的:

            在本例中,我们有一个名为 employee 的实体。 user 表保存您的用户记录,entityentity_revision 是两个表,其中保存了您将拥有的所有实体类型的修订历史记录系统。以下是此设计的工作原理:

            entity_idrevision_id两个字段

            您系统中的每个实体都有自己的唯一实体 ID。您的实体可能会经历修订,但其 entity_id 将保持不变。您需要将此实体 ID 保留在您的员工表中(作为外键)。您还应该将实体的类型存储在 entity 表中(例如“员工”)。现在至于 revision_id,正如其名称所示,它会跟踪您的实体修订。我为此找到的最佳方法是使用 employee_id 作为您的 revision_id。这意味着对于不同类型的实体,您将拥有重复的修订 ID,但这对我来说不是一种享受(我不确定您的情况)。唯一需要注意的是 entity_id 和 revision_id 的组合应该是唯一的。

            entity_revision 表中还有一个 state 字段,用于指示修订状态。它可以具有以下三种状态之一:latestobsoletedeleted(不依赖于修订日期可以极大地提高查询速度)。

            关于revision_id 的最后一点说明,我没有创建将employee_id 连接到revision_id 的外键,因为我们不想为将来可能添加的每种实体类型更改entity_revision 表。

            插入

            对于您要插入数据库的每个 employee,您还将向 entityentity_revision 添加一条记录。最后两条记录将帮助您跟踪记录由谁以及何时插入数据库。

            更新

            现有员工记录的每次更新都将通过两次插入来实现,一次在员工表中,一次在 entity_revision 中。第二个将帮助您了解记录的更新者和时间。

            删除

            为了删除员工,将一条记录插入到 entity_revision 中,说明删除并完成。

            正如您在此设计中看到的那样,不会从数据库中更改或删除任何数据,更重要的是,每种实体类型只需要一个表。就我个人而言,我发现这种设计非常灵活且易于使用。但我不确定您的情况,因为您的需求可能会有所不同。

            [更新]

            在新的 MySQL 版本中支持分区,我相信我的设计也具有最好的性能之一。可以使用type 字段对entity 表进行分区,而使用state 字段对entity_revision 进行分区。这将大大提升SELECT 查询的数量,同时保持设计简洁明了。

            【讨论】:

              【解决方案11】:

              如果确实只需要审计跟踪,我会倾向于审计表解决方案(包括其他表上重要列的非规范化副本,例如,UserName)。但请记住,这种痛苦的经历表明,单个审计表将成为未来的巨大瓶颈。为所有被审计的表创建单独的审计表可能是值得的。

              如果您需要跟踪实际的历史(和/或未来)版本,那么标准解决方案是使用开始、结束和持续时间值的某种组合来跟踪具有多行的同一实体。您可以使用视图来方便地访问当前值。如果这是您采用的方法,如果您的版本化数据引用了可变但未版本化的数据,您可能会遇到问题。

              【讨论】:

                【解决方案12】:

                如果您想做第一个,您可能还想对Employees 表使用XML。大多数较新的数据库允许您查询 XML 字段,因此这并不总是一个问题。无论是最新版本还是早期版本,使用一种访问员工数据的方法可能会更简单。

                不过,我会尝试第二种方法。您可以通过只有一个带有 DateModified 字段的员工表来简化此操作。 EmployeeId + DateModified 将是主键,您只需添加一行即可存储新修订。这样存档旧版本和从存档中恢复版本也更容易。

                另一种方法是 Dan Linstedt 的 datavault model。我为荷兰统计局做了一个项目,使用了这个模型,效果很好。但我不认为它对日常数据库使用直接有用。不过,您可能会从阅读他的论文中获得一些想法。

                【讨论】:

                  【解决方案13】:

                  怎么样:

                  • 员工 ID
                  • 修改日期
                    • 和/或修订号,取决于您希望如何跟踪它
                  • 由用户 ID 修改
                    • 加上您想要跟踪的任何其他信息
                  • 员工字段

                  您创建主键 (EmployeeId, DateModified),并获取“当前”记录,您只需为每个员工 ID 选择 MAX(DateModified)。存储 IsCurrent 是一个非常糟糕的主意,因为首先它可以计算,其次,数据太容易不同步。

                  您还可以创建一个仅列出最新记录的视图,并在您的应用程序中工作时主要使用它。这种方法的好处是您没有重复的数据,并且您不必从两个不同的地方(当前在员工中,并在员工历史中存档)收集数据来获取所有历史记录或回滚等) .

                  【讨论】:

                  • 这种方法的一个缺点是,与使用两个表相比,表的增长速度会更快。
                  【解决方案14】:

                  如果您想依赖历史数据(出于报告原因),您应该使用如下结构:

                  // Holds Employee Entity
                  "Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
                  
                  // Holds the Employee revisions in rows.
                  "EmployeeHistories (HistoryId, EmployeeId, DateModified, OldValue, NewValue, FieldName)"
                  

                  或全球应用解决方案:

                  // Holds Employee Entity
                  "Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
                  
                  // Holds all entities revisions in rows.
                  "EntityChanges (EntityName, EntityId, DateModified, OldValue, NewValue, FieldName)"
                  

                  您也可以将您的修订保存在 XML 中,这样您就只有一个记录对应一个修订。这看起来像:

                  // Holds Employee Entity
                  "Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
                  
                  // Holds all entities revisions in rows.
                  "EntityChanges (EntityName, EntityId, DateModified, XMLChanges)"
                  

                  【讨论】:

                  • 更好:使用事件溯源:)
                  【解决方案15】:

                  我们也有类似的需求,但我们发现用户通常只想查看已更改的内容,而不一定要回滚任何更改。

                  我不确定您的用例是什么,但我们所做的是创建和审核表,该表会随着业务实体的更改自动更新,包括任何外键引用和枚举的友好名称。

                  每当用户保存他们的更改时,我们都会重新加载旧对象、运行比较、记录更改并保存实体(所有这些都在单个数据库事务中完成,以防出现任何问题)。

                  这似乎对我们的用户非常有效,并且让我们不必担心拥有一个与我们的业务实体具有相同字段的完全独立的审计表。

                  【讨论】:

                    【解决方案16】:

                    听起来您想跟踪特定实体随时间的变化,例如ID 3,“bob”,“123 main street”,然后是另一个 ID 3,“bob”“234 elm st”,等等,本质上能够吐出显示“bob”每个地址的修订历史.

                    做到这一点的最佳方法是在每条记录上都有一个“当前”字段,以及(可能)日期/时间表的时间戳或 FK。

                    然后插入必须设置“是当前的”并取消设置前一个“是当前的”记录上的“当前的”。查询必须指定“是当前的”,除非您想要所有历史记录。

                    如果它是一个非常大的表,或者需要进行大量修订,则可以对此进行进一步的调整,但这是一种相当标准的方法。

                    【讨论】:

                      猜你喜欢
                      • 2010-10-19
                      • 1970-01-01
                      • 2011-11-19
                      • 2011-02-10
                      • 2016-01-24
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多