是否有 MySQL 选项/功能来跟踪记录更改的历史记录?

Posted

技术标签:

【中文标题】是否有 MySQL 选项/功能来跟踪记录更改的历史记录?【英文标题】:Is there a MySQL option/feature to track history of changes to records? 【发布时间】:2012-09-15 20:01:48 【问题描述】:

有人问我是否可以跟踪 mysql 数据库中记录的更改。因此,当一个字段被更改时,旧的与新的可用以及发生的日期。是否有这样做的功能或常用技术?

如果是这样,我正在考虑做这样的事情。创建一个名为changes 的表。它将包含与 ma​​ster 表相同的字段,但以 old 和 new 为前缀,但仅适用于实际更改的字段和 TIMESTAMP 。它将使用ID 进行索引。这样,可以运行SELECT 报告来显示每条记录的历史记录。这是一个好方法吗?谢谢!

【问题讨论】:

【参考方案1】:

您可以创建触发器来解决此问题。 Here is a tutorial to do so(存档链接)。

在数据库中设置约束和规则比写更好 处理相同任务的特殊代码,因为它会阻止另一个 开发人员免于编写绕过所有 特殊代码,可能会使您的数据库的数据完整性较差。

长期以来,我一直在使用脚本将信息复制到另一个表 因为当时 MySQL 不支持触​​发器。我现在找到了这个 触发以更有效地跟踪所有内容。

如果更改了旧值,此触发器会将旧值复制到历史表中 当有人编辑一行时。 Editor IDlast mod 存储在 每次有人编辑该行时的原始表;时间对应 到何时更改为当前形式。

DROP TRIGGER IF EXISTS history_trigger $$

CREATE TRIGGER history_trigger
BEFORE UPDATE ON clients
    FOR EACH ROW
    BEGIN
        IF OLD.first_name != NEW.first_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'first_name',
                        NEW.first_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

        IF OLD.last_name != NEW.last_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'last_name',
                        NEW.last_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

    END;
$$

另一种解决方案是保留修订字段并在保存时更新此字段。您可以决定 max 是最新的版本,或者 0 是最新的行。这取决于你。

【讨论】:

【参考方案2】:

很微妙。

如果业务需求是“我想审计对数据的更改 - 谁在什么时候做了什么?”,您通常可以使用审计表(根据 Keethanjan 发布的触发器示例)。我不是触发器的忠实拥护者,但它具有实现起来相对轻松的巨大好处 - 您现有的代码不需要了解触发器和审计内容。

如果业务需求是“告诉我数据在过去给定日期的状态”,则意味着随时间变化的方面已进入您的解决方案。虽然您几乎可以通过查看审计表来重建数据库的状态,但这很困难且容易出错,而且对于任何复杂的数据库逻辑,它都会变得笨拙。例如,如果企业想知道“找到我们应该发送给在当月第一天有未付发票的客户的信件的地址”,您可能需要搜索六张审计表。

相反,您可以将随时间变化的概念融入您的架构设计中(这是 Keethanjan 建议的第二个选项)。这是对您的应用程序的更改,肯定是在业务逻辑和持久性级别上,所以它不是微不足道的。

例如,如果您有这样的表:

CUSTOMER
---------
CUSTOMER_ID PK
CUSTOMER_NAME
CUSTOMER_ADDRESS

并且您想随着时间的推移进行跟踪,您可以将其修改如下:

CUSTOMER
------------
CUSTOMER_ID            PK
CUSTOMER_VALID_FROM    PK
CUSTOMER_VALID_UNTIL   PK
CUSTOMER_STATUS
CUSTOMER_USER
CUSTOMER_NAME
CUSTOMER_ADDRESS

每次您想要更改客户记录时,您无需更新记录,而是将当前记录上的 VALID_UNTIL 设置为 NOW(),并插入一条带有 VALID_FROM(现在)和空 VALID_UNTIL 的新记录。您将“CUSTOMER_USER”状态设置为当前用户的登录 ID(如果需要保留)。如果需要删除客户,您可以使用 CUSTOMER_STATUS 标志来指示 - 您可能永远不会从该表中删除记录。

这样,您始终可以找到给定日期客户表的状态 - 地址是什么?他们改名了吗?通过加入其他具有相似 valid_from 和 valid_until 日期的表,您可以从历史上重建整个图片。要查找当前状态,请搜索具有空 VALID_UNTIL 日期的记录。

它很笨拙(严格来说,您不需要valid_from,但它使查询更容易一些)。它使您的设计和数据库访问变得复杂。但它使重建世界变得容易得多。

【讨论】:

但它会为那些未更新的字段添加重复数据?如何管理? 使用第二种方法时,如果在一段时间内编辑客户记录,则很难识别特定条目是属于同一客户还是不同客户。 我所见过的关于这个问题的最佳建议 哦,作为对 cme​​ts 的回应,为其他没有变化的东西存储 null 怎么样?所以最新版本将是所有最新数据,但如果名称在 5 天前曾经是“Bob”,那么只有一行,名称 = bob 并且有效期至 5 天前。 customer_id和日期组合为主键,保证唯一。【参考方案3】:

这是一个简单的方法:

首先,为您要跟踪的每个数据表创建一个历史记录表(下面的示例查询)。该表将为对数据表中的每一行执行的每个插入、更新和删除查询都有一个条目。

历史表的结构将与它跟踪的数据表相同,除了三个附加列:存储发生的操作的列(我们称之为“操作”)、操作的日期和时间、以及用于存储序列号('revision')的列,该序列号每次操作递增,并按数据表的主键列分组。

要执行此排序行为,会在主键列和修订列上创建两列(复合)索引。请注意,如果历史表使用的引擎是 MyISAM(See 'MyISAM Notes' on this page)

历史表很容易创建。在下方的 ALTER TABLE 查询中(以及下方的触发器查询中),将“primary_key_column”替换为数据表中该列的实际名称。

CREATE TABLE MyDB.data_history LIKE MyDB.data;

ALTER TABLE MyDB.data_history MODIFY COLUMN primary_key_column int(11) NOT NULL, 
   DROP PRIMARY KEY, ENGINE = MyISAM, ADD action VARCHAR(8) DEFAULT 'insert' FIRST, 
   ADD revision INT(6) NOT NULL AUTO_INCREMENT AFTER action,
   ADD dt_datetime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER revision,
   ADD PRIMARY KEY (primary_key_column, revision);

然后你创建触发器:

DROP TRIGGER IF EXISTS MyDB.data__ai;
DROP TRIGGER IF EXISTS MyDB.data__au;
DROP TRIGGER IF EXISTS MyDB.data__bd;

CREATE TRIGGER MyDB.data__ai AFTER INSERT ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'insert', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__au AFTER UPDATE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'update', NULL, NOW(), d.*
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__bd BEFORE DELETE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'delete', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = OLD.primary_key_column;

你就完成了。现在,“MyDb.data”中的所有插入、更新和删除都将记录在“MyDb.data_history”中,为您提供这样的历史记录表(减去人为的“data_columns”列)

ID    revision   action    data columns..
1     1         'insert'   ....          initial entry for row where ID = 1
1     2         'update'   ....          changes made to row where ID = 1
2     1         'insert'   ....          initial entry, ID = 2
3     1         'insert'   ....          initial entry, ID = 3 
1     3         'update'   ....          more changes made to row where ID = 1
3     2         'update'   ....          changes made to row where ID = 3
2     2         'delete'   ....          deletion of row where ID = 2 

要显示给定列从更新到更新的更改,您需要在主键和序列列上将历史表连接到自身。您可以为此创建一个视图,例如:

CREATE VIEW data_history_changes AS 
   SELECT t2.dt_datetime, t2.action, t1.primary_key_column as 'row id', 
   IF(t1.a_column = t2.a_column, t1.a_column, CONCAT(t1.a_column, " to ", t2.a_column)) as a_column
   FROM MyDB.data_history as t1 INNER join MyDB.data_history as t2 on t1.primary_key_column = t2.primary_key_column 
   WHERE (t1.revision = 1 AND t2.revision = 1) OR t2.revision = t1.revision+1
   ORDER BY t1.primary_key_column ASC, t2.revision ASC

编辑: 哦,哇,人们喜欢我 6 年前的历史表:P

我想我的实现仍然在嗡嗡作响,变得越来越大,越来越笨拙,我想。我写了视图和漂亮的 UI 来查看这个数据库中的历史,但我认为它没有被太多使用。就这样吧。

不按特定顺序处理某些 cmets:

我在 php 中进行了自己的实现,涉及更多一点,并且避免了 cmets 中描述的一些问题(索引转移,非常重要。如果将唯一索引转移到历史表,事情会打破。在 cmets 中有解决方案)。根据您的数据库建立的程度,认真阅读这篇文章可能是一次冒险。

如果主键和修订列之间的关系似乎不正确,这通常意味着复合键以某种方式失效。在极少数情况下,我会发生这种情况并且不知所措。

我发现这个解决方案非常高效,因为它使用触发器。此外,MyISAM 的插入速度很快,这是所有触发器所做的。您可以通过智能索引(或缺少...)进一步改进这一点。实际上,除非您在其他地方遇到重大问题,否则将单行插入具有主键的 MyISAM 表不应该是您需要优化的操作。在我运行 MySQL 数据库的整个过程中,这个历史表实现一直在运行,它从来都不是出现任何(许多)性能问题的原因。

如果您收到重复插入,请检查您的软件层是否有 INSERT IGNORE 类型查询。嗯,现在不记得了,但我认为这个方案和事务在运行多个 DML 操作后最终会失败。至少要注意一些事情。

历史表和数据表中的字段匹配很重要。或者,更确切地说,您的数据表没有比历史表更多的列。否则,当对历史表的​​插入将不存在的列放入查询中时(由于触发器查询中的 d.*),对数据表的插入/更新/删除查询将失败,并且触发器失败。如果 MySQL 有类似模式触发器之类的东西,那就太棒了,如果将列添加到数据表中,您可以在其中更改历史表。 MySQL现在有吗?这些天我做 React :P

【讨论】:

我真的很喜欢这个解决方案。但是,如果您的主表没有主键或者您不知道主表是什么,那就有点棘手了。 我最近在一个项目中使用这个解决方案时遇到了一个问题,因为原始表中的所有索引都被复制到历史表中(由于 CREATE TABLE ... LIKE ....作品)。在历史表上拥有唯一索引可能会导致 AFTER UPDATE 触发器中的 INSERT 查询出错,因此需要删除它们。在我拥有的 php 脚本中,我查询新创建的历史表上的任何唯一索引(使用“SHOW INDEX FROM data_table WHERE Key_name != 'PRIMARY' and Non_unique = 0”),然后删除它们。跨度> 这里我们每次都在备份表中插入重复的数据。如果我们在一个表中有 10 个字段并且我们更新了 2 个字段,那么我们将为其余 8 个字段添加重复数据。如何克服它? 您可以通过将create table语句更改为CREATE TABLE MyDB.data_history as select * from MyDB.data limit 0;来避免意外携带各种索引 @transientclosure 您如何建议将不属于原始查询的其他字段添加到历史记录中?例如,我想跟踪谁进行了这些更改。对于插入,它已经有一个owner 字段,对于更新我可以添加一个updatedby 字段,但对于删除我不确定如何通过触发器来做到这一点。用用户 ID 更新 data_history 行感觉很脏:P【参考方案4】:

我们是这样解决的

Users 表看起来像这样

Users
-------------------------------------------------
id | name | address | phone | email | created_on | updated_on

业务需求发生了变化,我们需要检查用户以前拥有的所有地址和电话号码。 新架构如下所示

Users (the data that won't change over time)
-------------
id | name

UserData (the data that can change over time and needs to be tracked)
-------------------------------------------------
id | id_user | revision | city | address | phone | email | created_on
 1 |   1     |    0     | NY   | lake st | 9809  | @long | 2015-10-24 10:24:20
 2 |   1     |    2     | Tokyo| lake st | 9809  | @long | 2015-10-24 10:24:20
 3 |   1     |    3     | Sdny | lake st | 9809  | @long | 2015-10-24 10:24:20
 4 |   2     |    0     | Ankr | lake st | 9809  | @long | 2015-10-24 10:24:20
 5 |   2     |    1     | Lond | lake st | 9809  | @long | 2015-10-24 10:24:20

要查找任何用户的当前地址,我们使用修订版 DESC 和 LIMIT 1 搜索 UserData

获取某个时间段内用户的地址 我们可以使用 created_on bewteen (date1 , date 2)

【讨论】:

这是我想要的解决方案,但我想知道如何使用触发器在此表中插入 id_user ? id_user=1revision=1 怎么了?首先我以为你的计数是0,2,3,...,但后来我看到id_user=2的修订计数是0,1, ... 你不需要idid_user. Just use a group ID of id`(用户ID)和revision【参考方案5】:

这样做的直接方法是在表上创建触发器。设置一些条件或映射方法。当更新或删除发生时,它会自动插入到“更改”表中。

但是最重要的部分是如果我们有很多列和很多表怎么办。我们必须输入每个表的每一列的名称。显然,这是浪费时间。

为了更华丽地处理这个问题,我们可以创建一些过程或函数来检索列的名称。

我们也可以简单地使用第 3 部分工具来执行此操作。在这里,我写了一个java程序 Mysql Tracker

【讨论】:

我怎样才能使用你的Mysql Tracker? 1.确保在每个表中都有一个 id 列作为主键。 2. 将 java 文件复制到本地(或 IDE) 3. 导入库并根据您的数据库配置和结构从第 9-15 行编辑静态变量。 4.解析并运行java文件 5.复制控制台日志并作为Mysql命令执行 create table like table 我认为可以轻松复制所有列【参考方案6】:

只有我的 2 美分。我会创建一个解决方案来准确记录更改的内容,这与瞬态的解决方案非常相似。

我的 ChangesTable 很简单:

DateTime | WhoChanged | TableName | Action | ID |FieldName | OldValue

1) 当主表中的一整行被改变时,很多条目会进入这个表,但是这不太可能,所以不是一个大问题(人们通常只改变一件事) 2)OldVaue(如果你愿意,还有 NewValue)必须是某种史诗般的“任何类型”,因为它可以是任何数据,可能有一种方法可以使用 RAW 类型或仅使用 JSON 字符串来转换输入和输出。

最少的数据使用,存储您需要的所有内容,并且可以一次用于所有表格。我现在正在自己研究这个,但这可能最终是我要走的路。

对于创建和删除,只需行 ID,不需要任何字段。删除主表上的标志(活动?)会很好。

【讨论】:

【参考方案7】:

为什么不简单地使用 bin 日志文件?如果在 Mysql 服务器上设置了复制,并且 binlog 文件格式设置为 ROW,则可以捕获所有更改。

可以使用一个很好的名为 noplay 的 Python 库。更多信息here。

【讨论】:

即使您没有/不需要复制,也可以使用 Binlog。 Binlog 有许多有益的用例。复制可能是最常见的用例,但它也可以用于备份和审计历史记录,如此处所述。【参考方案8】:

MariaDB 从 10.3 开始支持系统版本控制,这是标准的 SQL 功能,完全符合您的要求:它存储表记录的历史记录并通过SELECT 查询提供对其的访问。 MariaDB 是 MySQL 的一个开放式开发分支。您可以通过此链接找到有关其系统版本控制的更多信息:

https://mariadb.com/kb/en/library/system-versioned-tables/

【讨论】:

请注意上面链接中的以下内容:“mysqldump 不会从版本化表中读取历史行,因此不会备份历史数据。此外,无法恢复时间戳,因为它们不能由插入/用户定义。” @Daniel 目前计划在 10.8 中使用:jira.mariadb.org/browse/MDEV-16029【参考方案9】:

在 MariaDB 10.5+ 中,这和设置一样容易

CREATE TABLE t (x INT) WITH SYSTEM VERSIONING 
  PARTITION BY SYSTEM_TIME;

然后可以通过做查询过​​去的历史记录

SELECT * FROM t FOR SYSTEM_TIME AS OF TIMESTAMP '2016-10-09 08:07:06';

目前在 MySQL 中没有对应的。

请参阅the documentation 了解更多信息。如果您使用的是较旧版本的 MariaDB,则该文档具有自 MariaDB 10.3.4 以来可用的替代语法。

【讨论】:

以上是关于是否有 MySQL 选项/功能来跟踪记录更改的历史记录?的主要内容,如果未能解决你的问题,请参考以下文章

Github桌面窗口同步按钮消失,历史记录选项卡不起作用

Dropbox Api,获取用户历史记录

操纵SVN历史记录

我可以使用 Libre Office 的哪种输出格式来跟踪我的文件历史记录?

为啥idea按9,就直接变成看本地更改的文件了

Git - 如何查看方法/函数的更改历史记录?