这是一个简单的方法:
首先,为您要跟踪的每个数据表创建一个历史记录表(下面的示例查询)。该表将为对数据表中的每一行执行的每个插入、更新和删除查询都有一个条目。
历史表的结构将与它跟踪的数据表相同,除了三个附加列:存储发生的操作的列(我们称之为“操作”)、操作的日期和时间、以及用于存储序列号('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