レコードの変更履歴を追跡するMySQLオプション/機能はありますか?


122

MySQLデータベースのレコードへの変更を追跡できるかどうか尋ねられました。したがって、フィールドが変更された場合、古いものと新しいものが利用可能であり、これが行われた日付です。これを行う機能または一般的な技術はありますか?

もしそうなら、私はこのようなことをすることを考えていました。というテーブルを作成しますchanges。これには、マスターテーブルと同じフィールドが含まれますが、プレフィックスとしてoldおよびnewが付けられますが、実際に変更されたフィールドとそのフィールドのみが対象TIMESTAMPです。それはID。このように、SELECTレポートを実行して、各レコードの履歴を表示できます。これは良い方法ですか?ありがとう!

回答:


83

微妙です。

ビジネス要件が「データの変更を監査したい-誰がいつ何をしたか?」である場合、通常は監査テーブルを使用できます(トリガーの例Keethanjanが投稿したとおり)。私はトリガーの大ファンではありませんが、実装するのが比較的簡単であるという大きな利点があります。既存のコードはトリガーや監査に関することを知る必要はありません。

ビジネス要件が「過去の特定の日付におけるデータの状態を表示する」である場合、それは時間の経過に伴う変化の側面がソリューションに入ったことを意味します。監査テーブルを見ただけでデータベースの状態を再構築することはできますが、それは難しく、エラーが発生しやすく、複雑なデータベースロジックでは扱いにくくなります。たとえば、企業が「毎月1日に未払いの未払いの請求書があった顧客に送信する必要がある手紙の住所を見つけたい」場合、半ダースの監査テーブルをトロールする必要がある可能性があります。

代わりに、時間の経過に伴う変更の概念をスキーマ設計に組み込むことができます(これは、Keethanjanが提案する2番目のオプションです)。これはアプリケーションの変更であり、間違いなくビジネスロジックと永続性のレベルであるため、簡単ではありません。

たとえば、次のようなテーブルがあるとします。

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(現在)とnullのVALID_UNTILを使用して新しいレコードを挿入します。「CUSTOMER_USER」ステータスを現在のユーザーのログインIDに設定します(保持する必要がある場合)。顧客を削除する必要がある場合は、CUSTOMER_STATUSフラグを使用してこれを示します。このテーブルからレコードを削除することはできません。

このようにして、特定の日付の顧客テーブルのステータスを常に見つけることができます-住所は何でしたか?彼らは名前を変えましたか?同様のvalid_fromおよびvalid_until日付を持つ他のテーブルに結合することにより、全体像を歴史的に再構築できます。現在のステータスを確認するには、VALID_UNTILの日付がnullのレコードを検索します。

これは扱いにくいです(厳密に言えば、valid_fromは必要ありませんが、クエリが少し簡単になります)。これは、設計とデータベースアクセスを複雑にします。しかし、それは世界を再構築することをずっと簡単にします。


しかし、更新されていないフィールドに重複データが追加されますか?それを管理する方法?
itzmukeshy7 2015年

2番目のアプローチでは、顧客レコードが時間の経過とともに編集された場合、レポート生成で問題が発生します。特定のエントリが同じ顧客に属しているか異なる顧客に属しているかを認識するのは困難です。
Akshay Joshi 2015

この問題について私がこれまでに見た中で最も良い提案
Worthy7

ああ、そしてコメントへの応答として、変更されなかった他のすべてのためにnullを格納するのはどうですか?したがって、最新バージョンはすべて最新のデータになりますが、名前が5日前に「Bob」であった場合は、1行のみ、name = bobがあり、5日前まで有効です。
Worthy7

2
customer_idと日付の組み合わせが主キーであるため、一意であることが保証されます。
Neville Kuyt 2017年

186

これを行う簡単な方法を次に示します。

最初に、追跡するデータテーブルごとに履歴テーブルを作成します(以下のクエリ例)。このテーブルには、データテーブルの各行で実行される挿入、更新、削除の各クエリのエントリがあります。

履歴テーブルの構造は、3つの追加の列を除いて、それが追跡するデータテーブルと同じになります。発生した操作を格納する列(「アクション」と呼びましょう)、操作の日時、および列シーケンス番号( 'リビジョン')を格納します。これは、操作ごとに増分され、データテーブルの主キー列によってグループ化されます。

このシーケンス動作を実行するために、2つの列(複合)インデックスが主キー列とリビジョン列に作成されます。履歴テーブルで使用されるエンジンが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
私はこのソリューションが本当に好きです。ただし、メインテーブルに主キーがない場合、または主キーが何であるかわからない場合は、少し注意が必要です。
Benjamin Eckstein 2014年

1
元のテーブルのすべてのインデックスが履歴テーブルにコピーされる方法(CREATE TABLE ... LIKE ....が機能するため)のため、このソリューションをプロジェクトに使用するときに問題が発生しました。履歴テーブルに一意のインデックスがあると、AFTER UPDATEトリガーのINSERTクエリが無効になる可能性があるため、それらを削除する必要があります。私が持っているphpスクリプトでこれを行うには、新しく作成された履歴テーブルの一意のインデックスをクエリし( "SHOW INDEX FROM data_table WHERE Key_name!= 'PRIMARY' and Non_unique = 0"を使用)、それらを削除します。
一時閉鎖

3
ここでは、毎回バックアップテーブルに繰り返しデータが挿入されています。テーブルに10個のフィールドがあり、2個を更新した場合、残りの8個のフィールドに繰り返しデータを追加します。それを克服する方法は?
itzmukeshy7 2015年

6
あなたは偶然にcreate table文を変更することによって、様々な指標の上に運ぶ避けることができますCREATE TABLE MyDB.data_history as select * from MyDB.data limit 0;
エリック・ヘイズ

4
@transientclosure元のクエリに含まれていない他のフィールドを履歴に含めることをどのように提案しますか?例えば、誰がそれらの変更を行ったかを追跡したいです。挿入の場合はすでにownerフィールドがあり、更新の場合はupdatedbyフィールドを追加できますが、削除の場合はトリガーを使用してそれを行う方法がわかりません。でdata_historyユーザーIDを使用して行を更新すると、汚れた感じになります:P
Horse

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;
$$

別の解決策は、リビジョンフィールドを保持し、保存時にこのフィールドを更新することです。maxが最新のリビジョンであるか、0が最新の行であるかを決定できます。それはあなた次第です。


9

解決方法は次のとおりです

Usersテーブルは次のようになりました

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のUserDataを検索します1

特定の期間のユーザーのアドレスを取得するには、created_on bewteen(date1、date 2)を使用できます。


これは私が欲しいソリューションですが、知りたいのですが、トリガーを使用してこのテーブルにid_userをどのように挿入できますか?
ケース

1
何が起こりましたrevision=1id_user=1?最初私はあなたのカウントがそうであると思ったが0,2,3,...、それから私id_user=2はリビジョンのカウントがそれであるのを見た0,1, ...
Pathros

1
列id`(ユーザーID)とは必要idありません。id_user. Just use a group ID of revision
Gajus 2017

6

MariaDBは10.3以降のシステムバージョニングをサポートしています。これは、必要な機能を正確に実行する標準SQL機能SELECTです。テーブルレコードの履歴を保存し、クエリを介してアクセスできるようにします。MariaDBは、MySQLのオープン開発フォークです。あなたはこのリンクを介してそのシステムバージョニングの詳細を見つけることができます:

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


上記のリンクから次の点に注意してください。「mysqldumpはバージョン対応テーブルから履歴行を読み取らないため、履歴データはバックアップされません。また、タイムスタンプはinsert /で定義できないため、復元できません。ユーザー。"
ダニエル

4

なぜ単にビンログファイルを使用しないのですか?Mysqlサーバーでレプリケーションが設定され、binlogファイル形式がROWに設定されている場合、すべての変更をキャプチャできます。

noplayと呼ばれる優れたPythonライブラリを使用できます。詳細はこちら


2
Binlogは、レプリケーションが必要ない場合でも使用できます。Binlogには多くの有益なユースケースがあります。レプリケーションはおそらく最も一般的な使用例ですが、ここで説明したように、バックアップや監査履歴にも利用できます。
webaholik 2017年

3

ちょうど私の2セント。何が変化したかを正確に記録するソリューションを作成します。これは一時的なソリューションと非常によく似ています。

私の変更テーブルは簡単です:

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

1)メインテーブルの行全体が変更されると、多くのエントリがこのテーブルに入りますが、これはほとんどあり得ないため、大きな問題ではありません(通常、人々は1つのものだけを変更しています)2)OldVaue(および欲しい)それはあらゆるデータである可能性があるので、ある種の叙事詩「anytype」である必要があります。RAWタイプでこれを行う方法、またはJSON文字列を使用して変換する方法があるかもしれません。

最小限のデータ使用量、必要なすべてを保存し、一度にすべてのテーブルに使用できます。今は自分で調べていますが、結局これが私のやり方かもしれません。

作成と削除の場合、行IDのみで、フィールドは必要ありません。削除時にメインテーブルのフラグ(アクティブ?)が良いでしょう。


0

これを行う直接的な方法は、テーブルにトリガーを作成することです。いくつかの条件またはマッピング方法を設定します。更新または削除が発生すると、「変更」テーブルに自動的に挿入されます。

しかし、最大の部分は、たくさんの列とたくさんのテーブルがある場合です。すべてのテーブルのすべての列の名前を入力する必要があります。明らかに、それは時間の無駄です。

これをより豪華に処理するために、列の名前を取得するためのいくつかのプロシージャまたは関数を作成できます。

これを行うために、サードパーティのツールを使用することもできます。ここでは、Javaプログラム Mysql Trackerを作成します


Mysql Trackerはどのように使用できますか?
webchun 2017年

1
1.各テーブルの主キーとしてid列があることを確認します。2. javaファイルをローカル(またはIDE)にコピーします。3. libsをインポートし、データベースの構成と構造に従って9〜15行目の静的変数を編集します。4. Javaファイルを解析して実行する5.コンソールログをコピーして
Mysql

create table like tableすべての列を簡単に複製できると思う
ジョナサン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.