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


121

有人问我是否可以跟踪MySQL数据库中记录的更改。因此,更改字段后,可以使用旧的还是新的,以及发生的日期。有没有功能或通用技术可以做到这一点?

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

Answers:


83

真微妙

如果业务要求是“我想审核数据的更改-谁做了什么以及何时做?”,则通常可以使用审核表(按照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_UNTIL的记录。

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


但这会为那些未更新的字段添加重复数据吗?如何管理?
itzmukeshy7 2015年

对于第二种方法,如果在一段时间内编辑了客户记录,则会产生报表生成问题,很难识别特定条目是属于同一客户还是属于不同客户。
Akshay Joshi 2015年

最好的建议,我已经看到过这个问题
Worthy7 '16

哦,对这些评论做出回应,如何将所有未更改的所有内容都存储为null呢?因此,最新版本将是所有最新数据,但是如果该名称在5天前曾经是“ Bob”,则只需一行,name = bob并在5天前有效。
Worthy7年

2
customer_id和日期的组合是主键,因此将确保它们是唯一的。
内维尔·库伊特

186

这是一种简单的方法:

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

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

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

历史记录表很容易创建。在下面的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来查看此数据库中的历史记录,但是我认为它用的很少。这样吧。

要以不特定的顺序处理一些评论:

  • 我用PHP进行了自己的实现,但涉及更多,并且避免了注释中描述的一些问题(将索引转移到了显着位置。如果将唯一索引转移到历史表中,事情将会中断。有解决方案在评论中)。在这封信之后,这取决于您数据库的建立方式,可能是一次冒险。

  • 如果主键和修订列之间的关系似乎不存在,则通常意味着复合键因某种原因被打扰。在极少数情况下,我会发生这种情况,而对此事一无所知。

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

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

  • 历史记录表和数据表中的字段必须匹配,这一点很重要。或者,更确切地说,您的数据表没有比历史表更多的列。否则,当对历史记录表的插入将查询中不存在的列放入数据库中时(由于触发器查询中的d。*),数据表上的插入/更新/删除查询将失败,并且触发器将失败。如果MySQL具有诸如模式触发之类的功能,那将真是棒极了;如果将列添加到数据表中,则可以在其中更改历史表。MySQL现在有吗?这些天我反应:P


3
我真的很喜欢这个解决方案。但是,如果您的主表没有主键,或者您不知道主键是什么,那就有点棘手了。
本杰明·埃克斯坦

1
最近我在使用此解决方案进行项目时遇到了一个问题,因为原始表中的所有索引如何复制到历史表中(由于CREATE TABLE ... LIKE ....是如何工作的)。在历史记录表上具有唯一索引会导致AFTER UPDATE触发器中的INSERT查询变为barf,因此需要将其删除。在php脚本中,我执行此操作,然后查询新创建的历史记录表上的所有唯一索引(“在data_table中显示索引,其中key_name!='PRIMARY'和Non_unique = 0”),然后将其删除。
暂时关闭

3
在这里,我们每次都在备份表中插入重复的数据。让我们假设表中有10个字段,并且更新了2个,那么我们将为其余8个字段添加重复数据。如何克服呢?
itzmukeshy7 2015年

6
您可以通过将create table语句更改为CREATE TABLE MyDB.data_history as select * from MyDB.data limit 0;
Eric Hayes'Aug

4
@transientclosure您如何建议将不属于原始查询的其他字段添加到历史记录中?例如,我想跟踪谁进行了这些更改。对于插入,它已经有一个owner字段,对于更新,我可以添加一个updatedby字段,但是对于删除,我不确定如何通过触发器来做到这一点。data_history用用户ID 更新行感觉很脏:P

16

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

在数据库中设置约束和规则比编写特殊代码来处理同一任务要好,因为这将阻止其他开发人员编写绕过所有特殊代码的查询,从而使数据库的数据完整性很差。

很长时间以来,我一直在使用脚本将信息复制到另一个表中,因为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是最新的行。随你(由你决定。


8

这是我们解决的方法

用户表如下所示

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

要获取特定时间段内的用户地址,我们可以使用bewteen(date1,date 2)


这是我想要的解决方案,但我想知道如何使用触发器在此表中插入id_user?
死刑

1
发生revision=1id_user=1什么?首先我以为您的计数是,0,2,3,...但后来我看到id_user=2修订计数是0,1, ...
Pathros

1
您不需要idid_user. Just use a group ID of ID(用户ID)和revision
Gajus,2017年

6

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

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


请注意上面链接中的以下内容:“ mysqldump不会从版本表中读取历史行,因此不会备份历史数据。此外,由于无法通过insert /定义时间戳,因此无法恢复时间戳用户。”
Daniel

4

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

可以使用一个名为noplay的优秀python库。更多信息在这里


2
即使您不需要复制,也可以使用Binlog。Binlog有许多有益的用例。复制可能是最常见的用例,但也可以将其用于备份和审核历史记录,如此处所述。
webaholik '17

3

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

我的ChangesTable很简单:

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

1)当主表中的整个行都发生更改时,该表中将有很多条目,但是这种情况不太可能出现,所以这不是一个大问题(人们通常只更改一件事)2)OldVaue(如果您要更改,则为NewValue)想要)必须是某种史诗般的“任何类型”,因为它可以是任何数据,也许可以使用RAW类型或仅使用JSON字符串进行转换。

数据使用量最少,可存储您需要的所有内容,并可一次用于所有表。我现在正在自己研究这个问题,但这可能最终成为我的发展方向。

对于“创建和删除”,只需行ID,无需任何字段。在删除时,在主表上标记(活动?)会很好。


0

直接执行此操作的方法是在表上创建触发器。设置一些条件或映射方法。当发生更新或删除时,它将自动插入“更改”表中。

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

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

我们也可以简单地使用第三部分工具来做到这一点。在这里,我编写了一个Java程序 Mysql Tracker


如何使用您的Mysql Tracker?
webchun

1
1.确保每个表中都有一个id列作为主键。2.将Java文件复制到本地(或IDE)。3.根据数据库配置和结构,从9-15行导入库并编辑静态变量。4.解析并运行Java文件。5.复制控制台日志并作为Mysql命令执行
goforu

create table like table我认为可以轻松复制所有专栏
Jonathan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.