Esta é uma maneira simples de fazer isso:
Primeiro, crie uma tabela de histórico para cada tabela de dados que deseja rastrear (consulta de exemplo abaixo). Esta tabela terá uma entrada para cada consulta de inserção, atualização e exclusão realizada em cada linha da tabela de dados.
A estrutura da tabela de histórico será a mesma da tabela de dados que rastreia, exceto por três colunas adicionais: uma coluna para armazenar a operação que ocorreu (vamos chamá-la de 'ação'), a data e hora da operação e uma coluna para armazenar um número de sequência ('revisão'), que aumenta por operação e é agrupado pela coluna de chave primária da tabela de dados.
Para fazer esse comportamento de sequenciamento, um índice de duas colunas (composto) é criado na coluna da chave primária e na coluna de revisão. Note que você só pode fazer o sequenciamento desta maneira se o motor usado pela tabela de histórico for MyISAM ( Veja 'Notas MyISAM' nesta página)
A tabela de histórico é bastante fácil de criar. Na consulta ALTER TABLE abaixo (e nas consultas de gatilho abaixo dela), substitua 'primary_key_column' pelo nome real dessa coluna em sua tabela de dados.
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);
E então você cria os gatilhos:
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;
E pronto. Agora, todas as inserções, atualizações e exclusões em 'MyDb.data' serão registradas em 'MyDb.data_history', dando a você uma tabela de histórico como esta (menos a coluna 'data_columns' inventada)
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
Para exibir as alterações para uma determinada coluna ou colunas de atualização para atualização, você precisará juntar a tabela de histórico a si mesma na chave primária e colunas de sequência. Você pode criar uma vista para este propósito, por exemplo:
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
Edit: Oh wow, as pessoas gostam da minha história na tabela de 6 anos atrás: P
Minha implementação ainda está zumbindo, ficando maior e mais difícil de manejar, eu diria. Eu escrevi visualizações e uma interface de usuário muito boa para olhar o histórico neste banco de dados, mas não acho que tenha sido muito usado. Assim vai.
Para abordar alguns comentários sem ordem específica:
Fiz minha própria implementação em PHP que era um pouco mais envolvente e evitei alguns dos problemas descritos nos comentários (transferência de índices, significativamente. Se você transferir índices exclusivos para a tabela de histórico, as coisas vão quebrar. Existem soluções para isso nos comentários). Seguir este post ao pé da letra pode ser uma aventura, dependendo de como está estabelecido seu banco de dados.
Se o relacionamento entre a chave primária e a coluna de revisão parecer incorreto, geralmente significa que a chave composta está danificada de alguma forma. Em algumas raras ocasiões, isso aconteceu e não sabia a causa.
Achei que essa solução tinha um ótimo desempenho, usando gatilhos como faz. Além disso, MyISAM é rápido em inserções, que é tudo o que os triggers fazem. Você pode melhorar ainda mais com a indexação inteligente (ou a falta de ...). Inserir uma única linha em uma tabela MyISAM com uma chave primária não deve ser uma operação que você precise otimizar, realmente, a menos que você tenha problemas significativos acontecendo em outro lugar. Durante todo o tempo em que estive executando o banco de dados MySQL, essa implementação da tabela de histórico nunca foi a causa de nenhum dos (muitos) problemas de desempenho que surgiram.
se você estiver obtendo inserções repetidas, verifique se há consultas do tipo INSERT IGNORE em sua camada de software. Hrmm, não me lembro agora, mas acho que há problemas com esse esquema e transações que falham depois de executar várias ações DML. Algo a ter em conta, pelo menos.
É importante que os campos da tabela de histórico e da tabela de dados correspondam. Ou melhor, que sua tabela de dados não tenha MAIS colunas do que a tabela de histórico. Caso contrário, as consultas de inserção / atualização / del na tabela de dados irão falhar, quando as inserções nas tabelas de histórico colocarem colunas na consulta que não existem (devido a d. * Nas consultas do gatilho), e o gatilho falhar. Seria incrível se o MySQL tivesse algo como gatilhos de esquema, onde você pudesse alterar a tabela de histórico se colunas fossem adicionadas à tabela de dados. O MySQL tem isso agora? Eu reajo hoje em dia: P