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

Edw*_*ard 109 mysql database

我被问到是否可以跟踪MySQL数据库中记录的更改.所以当一个字段被更改时,旧的和​​新的可用并且发生了这个日期.有这样的功能或常用技术吗?

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

tra*_*ure 169

这是一种直接的方法:

首先,为要跟踪的每个数据表创建一个历史表(下面的示例查询).对于在数据表中的每一行上执行的每个插入,更新和删除查询,此表都有一个条目.

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

要执行此排序行为,将在主键列和修订列上创建两列(复合)索引.请注意,如果历史记录表使用的引擎是MyISAM,则只能以这种方式进行排序(请参阅本页的"MyISAM Notes")

历史表非常容易创建.在下面的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);
Run Code Online (Sandbox Code Playgroud)

然后你创建触发器:

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;
Run Code Online (Sandbox Code Playgroud)

而且你已经完成了.现在,'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 
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

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

我认为,我对它的实现仍在嗡嗡作响,越来越大,越来越笨拙.我写了一些视图和相当不错的用户界面来查看这个数据库中的历史记录,但我认为它并没有被大量使用.就这样吧.

为了解决一些评论,没有特别的顺序:

  • 我在PHP中做了我自己的实现,它涉及的更多,并且避免了注释中描述的一些问题(将索引转移过来,显着.如果你将唯一索引转移到历史表,事情就会破坏.有解决方案这在评论中).根据你的数据库建立的方式,这封信后面的信可能是冒险.

  • 如果主键和修订列之间的关系似乎关闭,则通常意味着复合键以某种方式被borked.在极少数情况下,我发生了这种情况并且不知道原因.

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

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

  • 重要的是历史表和数据表中的字段匹配.或者说,您的数据表没有比历史记录表更多的列.否则,插入/更新/删除查询的数据表将失败,当插入到历史表放置在查询不存在的列(由于d*在触发查询),并触发失败.如果MySQL有类似schema-triggers的东西,那么你可以改变历史表,如果列被添加到数据表中,那将是很棒的.MySQL现在有这个吗?我这些天做反应:P

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

Nev*_*uyt 74

这很微妙.

如果业务要求是"我想审核数据的更改 - 谁做了什么以及何时做什么?",您通常可以使用审计表(根据Keethanjan发布的触发器示例).我并不是触发器的忠实粉丝,但它具有实现相对轻松的巨大好处 - 您现有的代码不需要知道触发器和审计的东西.

如果业务要求是"向我显示过去在给定日期数据的状态",则表示随时间变化的方面已进入您的解决方案.虽然您可以通过查看审计表来重建数据库的状态,但它很难且容易出错,并且对于任何复杂的数据库逻辑,它变得难以处理.例如,如果企业想知道"找到我们本应该发送给在月份第一天有未付发票的未付发票的客户的信件地址",那么您可能需要搜索六个审计表.

相反,您可以将随时间变化的概念烘焙到您的模式设计中(这是Keethanjan建议的第二个选项).这是对应用程序的更改,绝对是在业务逻辑和持久性级别,所以这不是微不足道的.

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

CUSTOMER
---------
CUSTOMER_ID PK
CUSTOMER_NAME
CUSTOMER_ADDRESS
Run Code Online (Sandbox Code Playgroud)

并且你想跟踪时间,你会修改如下:

CUSTOMER
------------
CUSTOMER_ID            PK
CUSTOMER_VALID_FROM    PK
CUSTOMER_VALID_UNTIL   PK
CUSTOMER_STATUS
CUSTOMER_USER
CUSTOMER_NAME
CUSTOMER_ADDRESS
Run Code Online (Sandbox Code Playgroud)

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

这样,你总能找到客户表在给定日期的状态 - 地址是什么?他们改名了吗?通过连接具有类似valid_from和valid_until日期的其他表,您可以历史地重建整个图片.要查找当前状态,请搜索VALID_UNTIL为空的记录.

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

  • customer_id和日期的组合是主键,因此它们将保证唯一. (2认同)

小智 16

您可以创建触发器来解决此问题.这是一个这样做的教程(存档链接).

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

很长一段时间我都在使用脚本将信息复制到另一个表,因为MySQL当时不支持触发器.我现在发现这个触发器可以更有效地跟踪所有事情.

如果在某人编辑行时更改了旧值,则此触发器会将旧值复制到历史记录表.Editor ID并且last 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;
$$
Run Code Online (Sandbox Code Playgroud)

另一种解决方案是保留Revision字段并在保存时更新此字段.您可以确定max是最新版本,或者0是最近的行.随你(由你决定.


mid*_*nok 10

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

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

  • 请注意上面链接中的以下内容:“mysqldump 不会从版本化表中读取历史行,因此不会备份历史数据。此外,不可能恢复时间戳,因为它们无法通过插入/定义一个用户。” (3认同)

小智 6

这是我们解决的方法

用户表如下所示

Users
-------------------------------------------------
id | name | address | phone | email | created_on | updated_on
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

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

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


Our*_*ros 5

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

可以使用一个名为 noplay 的优秀 Python 库。更多信息请点击这里

  • 即使您没有/不需要复制,也可以使用 Binlog。Binlog 有许多有益的用例。复制可能是最常见的用例,但它也可以用于备份和审核历史记录,如此处所述。 (2认同)