Aqui está uma maneira direta de fazer isso:
Primeiro, crie uma tabela de histórico para cada tabela de dados que você deseja acompanhar (exemplo de consulta abaixo). Essa 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 ela 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 de chave primária e na coluna de revisão. Observe que você só pode fazer o sequenciamento dessa maneira se o mecanismo usado pela tabela de histórico for MyISAM (Veja 'Notas do 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), 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 você está feito. Agora, todas as inserções, atualizações e exclusões em 'MyDb.data' serão registradas em 'MyDb.data_history', dando-lhe uma tabela de histórico como esta (menos a coluna 'data_columns' artificial)
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á unir a tabela de histórico a si mesma na chave primária e nas colunas de sequência. Você pode criar uma visualização para essa finalidade, 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 uau, as pessoas gostam da minha mesa de história de 6 anos atrás :P
Minha implementação ainda está zumbindo, ficando maior e mais difícil de manejar, eu presumo. Eu escrevi visualizações e uma interface de usuário muito boa para ver o histórico neste banco de dados, mas acho que nunca foi muito usado. Assim vai.
Para abordar alguns comentários em nenhuma ordem específica:
-
Eu fiz minha própria implementação em PHP que foi um pouco mais complicada, e evitei alguns dos problemas descritos nos comentários (ter índices transferidos, signifcativamente. Se você transferir índices únicos 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 quão estabelecido é o seu banco de dados.
-
Se a relação entre a chave primária e a coluna de revisão parecer incorreta, geralmente significa que a chave composta está quebrada de alguma forma. Em algumas raras ocasiões, isso aconteceu e fiquei sem saber a causa.
-
Achei essa solução bastante eficiente, usando gatilhos como ela faz. Além disso, o MyISAM é rápido em inserções, que é tudo o que os gatilhos 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, a menos que você tenha problemas significativos acontecendo em outro lugar. Durante todo o tempo em que eu estava executando o banco de dados MySQL, essa implementação da tabela de histórico estava ativada, nunca foi a causa de nenhum dos (muitos) problemas de desempenho que surgiram.
-
se você estiver recebendo inserções repetidas, verifique sua camada de software para consultas do tipo INSERT IGNORE. 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 para estar ciente, pelo menos.
-
É importante que os campos na tabela de histórico e na tabela de dados correspondam. Ou melhor, que sua tabela de dados não tem MAIS colunas do que a tabela de histórico. Caso contrário, as consultas insert/update/del na tabela de dados falharão, 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 as colunas fossem adicionadas à tabela de dados. O MySQL tem isso agora? Eu reajo esses dias :P