改訂のためのデータベース設計?


125

プロジェクトでは、エンティティのすべてのリビジョン(変更履歴)をデータベースに保存する必要があります。現在、このために設計された2つの提案があります。

例:「従業員」エンティティ

デザイン1:

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- Holds the Employee Revisions in Xml. The RevisionXML will contain
-- all data of that particular EmployeeId
"EmployeeHistories (EmployeeId, DateModified, RevisionXML)"

デザイン2:

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- In this approach we have basically duplicated all the fields on Employees 
-- in the EmployeeHistories and storing the revision data.
"EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName, 
      LastName, DepartmentId, .., ..)"

これを行う他の方法はありますか?

「デザイン1」の問題は、データにアクセスする必要があるたびにXMLを解析する必要があることです。これによりプロセスが遅くなり、リビジョンデータフィールドに結合を追加できないなどの制限が追加されます。

また、「デザイン2」の問題は、すべてのエンティティのすべてのフィールドを複製する必要があることです(リビジョンを維持したいエンティティが70〜80個あります)。



1
参考までに:それが.Sqlサーバー2008以降に役立つ可能性がある場合に備えて、table..visitsimple - talk.com / sql / learn - sql -server / の変更履歴を表示するテクノロジーを備えており、DBの詳細を確認できます。 Oracleのようなものもこのようなものになります。
Durai Amuthan.H 2013

一部の列はXMLまたはJSON自体を格納できることに注意してください。そうでない場合は、将来発生する可能性があります。そのようなデータを互いにネストする必要がないことを確認してください。
ジャクビゾン

回答:


38
  1. すべてをIsCurrent識別子属性を持つ1つのテーブルに配置しないでください。これは将来問題を引き起こすだけであり、代理キーやその他のあらゆる種類の問題が必要です。
  2. デザイン2では、スキーマの変更に問題があります。Employeesテーブルを変更する場合は、EmployeeHistoriesテーブルとそれに関連するすべての関連sprocを変更する必要があります。スキーマ変更の労力が2倍になる可能性があります。
  3. デザイン1は適切に機能し、適切に実行してもパフォーマンスへの影響はそれほど大きくありません。可能性のあるパフォーマンスの問題を回避するために、xmlスキーマとインデックスさえ使用できます。xmlの解析に関するコメントは有効ですが、xqueryを使用してビューを簡単に作成できます。これは、クエリに含めて結合できます。このようなもの...
CREATE VIEW EmployeeHistory
AS
, FirstName, , DepartmentId

SELECT EmployeeId, RevisionXML.value('(/employee/FirstName)[1]', 'varchar(50)') AS FirstName,

  RevisionXML.value('(/employee/LastName)[1]', 'varchar(100)') AS LastName,

  RevisionXML.value('(/employee/DepartmentId)[1]', 'integer') AS DepartmentId,

FROM EmployeeHistories 

25
IsCurrentトリガーを使用して、すべてを1つのテーブルに格納しないと言うのはなぜですか。これが問題になるいくつかの例を教えてください。
ネイサンW

@Simon Munro主キーまたはクラスター化キーはどうですか?検索を速くするために、デザイン1の履歴テーブルに追加できるキーはどれですか。
gotqn 2012

全テーブルスキャンSELECT * FROM EmployeeHistory WHERE LastName = 'Doe'での単純な結果を想定しています。アプリケーションをスケーリングするための最良のアイデアではありません。
魁夷

54

ここで重要な質問は、「誰が/歴史を何に使うのか」だと思います。

主にレポート/人間が読める履歴を対​​象とする場合、過去にこのスキームを実装しました...

「AuditTrail」というテーブル、または次のフィールドを持つものを作成します...

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[OldValue] [varchar](5000) NULL,
[NewValue] [varchar](5000) NULL

次に、「LastUpdatedByUserID」列をすべてのテーブルに追加できます。これは、テーブルで更新/挿入を行うたびに設定する必要があります。

次に、すべてのテーブルにトリガーを追加して、発生する挿入/更新をキャッチし、変更されたフィールドごとにこのテーブルにエントリを作成します。テーブルには更新/挿入ごとに「LastUpdateByUserID」も提供されるため、トリガーでこの値にアクセスして、監査テーブルに追加するときに使用できます。

RecordIDフィールドを使用して、更新されるテーブルのキーフィールドの値を格納します。結合キーの場合は、フィールド間に「〜」を付けて文字列を連結します。

このシステムには欠点があると思います-頻繁に更新されるデータベースの場合、パフォーマンスが低下する可能性がありますが、私のWebアプリの場合、書き込みよりも読み取りの方がはるかに多く、パフォーマンスはかなり良いようです。テーブル定義に基づいてトリガーを自動的に書き込むための小さなVB.NETユーティリティも作成しました。

ちょっとした考え!


5
NewValueは監査対象のテーブルに保存されるため、保存する必要はありません。
Petrus Theron 2010

17
厳密に言えば、それは本当です。ただし、同じフィールドに一定の期間に多数の変更があった場合、新しい値を保存すると、「ブライアンが行ったすべての変更を表示」などのクエリが1つの更新に関するすべての情報が保持されるため、はるかに簡単になります。 1つのレコード。ちょっとした考え!
クリスロバーツ

1
私が思うにsysname、テーブル名とカラム名のためのより適切なデータ型であってもよいです。
2013

2
sysnameを使用した@Samは値を追加しません。それも混乱するかもしれません... stackoverflow.com/questions/5720212/...
Jowen

19

Database ProgrammerブログのHistory Tables記事は役に立つかもしれません-ここで提起されたいくつかのポイントをカバーし、デルタのストレージについて議論します。

編集する

著者(Kenneth Downs)は、履歴テーブルエッセイで、少なくとも7列の履歴テーブルを維持することを推奨しています。

  1. 変更のタイムスタンプ、
  2. 変更を加えたユーザー
  3. 変更されたレコードを識別するトークン(履歴は現在の状態とは別に維持されます)、
  4. 変更が挿入、更新、または削除であったかどうか
  5. 古い値、
  6. 新しい価値、
  7. デルタ(数値の変更用)。

決して変化しない列、または履歴が不要な列は、肥大化を避けるために履歴テーブルで追跡しないでください。数値のデルタを保存すると、古い値と新しい値から派生させることができますが、後続のクエリが容易になります。

履歴テーブルは安全である必要があり、システム以外のユーザーは行の挿入、更新、削除を行えません。全体的なサイズを削減するために、定期的なパージのみをサポートする必要があります(ユースケースで許可されている場合)。


14

私たちは、Chris Robertsが提案するソリューションと非常によく似たソリューションを実装しました。

唯一の違いは、新しい値のみを保存することです。古い値は結局前の履歴行に保存されています

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[NewValue] [varchar](5000) NULL

20列のテーブルがあるとします。この方法では、行全体を格納する必要はなく、変更された正確な列のみを格納する必要があります。


14

デザイン1は避けてください。たとえば、管理者コンソールを使用して、自動または「手動」でレコードの古いバージョンにロールバックする必要がある場合などは、あまり便利ではありません。

デザイン2の欠点は本当にわかりません。2番目の履歴テーブルには、最初のレコードテーブルに存在するすべての列を含める必要があります。たとえばmysqlでは、別のテーブルと同じ構造のテーブルを簡単に作成できます(create table X like Y)。また、ライブデータベースのRecordsテーブルの構造を変更する場合は、alter tableとにかくコマンドを使用する必要があります。また、Historyテーブルに対してもこれらのコマンドを実行するのに大きな労力は必要ありません。

ノート

  • レコードテーブルには最新のリビジョンのみが含まれます。
  • 履歴テーブルには、レコードテーブル内のレコードの以前のリビジョンがすべて含まれています。
  • 履歴テーブルの主キーは、RevisionId列が追加されたレコードテーブルの主キーです。
  • 次のような追加の補助フィールドについて考えてみてくださいModifiedBy-特定のリビジョンを作成したユーザー。また、DeletedBy特定のリビジョンを誰が削除したかを追跡するフィールドが必要になる場合もあります。
  • DateModified意味が何であるかを考えてください。これは、この特定のリビジョンが作成された場所を意味するか、この特定のリビジョンが別のリビジョンに置き換えられたときを意味します。前者はフィールドがRecordsテーブルにあることを要求し、一見するとより直感的に見えるようです。ただし、2番目の解決策は、削除されたレコード(この特定のリビジョンが削除された日付)の場合により実用的であるようです。最初のソリューションに進む場合は、おそらく2番目のフィールドDateDeletedが必要になります(もちろん、必要な場合のみ)。あなたとあなたが実際に記録したいものに依存します。

デザイン2の操作は非常に簡単です。

修正
  • レコードをレコードテーブルから履歴テーブルにコピーし、新しいRevisionIdを付与します(レコードテーブルにまだ存在しない場合)。DateModifiedを処理します(解釈方法によって異なります。上記の注を参照してください)。
  • Recordsテーブルのレコードを通常どおり更新します
削除する
  • 変更操作の最初のステップとまったく同じようにします。選択した解釈に応じて、DateModified / DateDeletedを適切に処理します。
元に戻す(またはロールバック)
  • 履歴テーブルから最新の(または特定の?)リビジョンを取得し、それをレコードテーブルにコピーする
特定のレコードの変更履歴を一覧表示する
  • 履歴テーブルとレコードテーブルから選択
  • この操作からあなたが正確に何を期待するか考えてください。おそらく、DateModified / DateDeletedフィールドから必要な情報を決定します(上記の注を参照)。

デザイン2に進む場合、それを行うために必要なすべてのSQLコマンドは、メンテナンスと同様に非常に簡単です。おそらく、レコードテーブルでも補助列(RevisionIdDateModified)を使用すると、両方のテーブルをまったく同じ構造(一意のキーを除く)に維持する方がはるかに簡単になります。これにより、単純なSQLコマンドが可能になり、データ構造の変更に耐性があります。

insert into EmployeeHistory select * from Employe where ID = XX

トランザクションを使用することを忘れないでください!

スケーリングに関しては、このソリューションは非常に効率的です。XMLからデータを前後に変換せず、テーブルの行全体をコピーするだけです。非常に単純なクエリで、インデックスを使用して非常に効率的です。


12

履歴を保存する必要がある場合は、追跡しているテーブルと同じスキーマと「改訂日」および「改訂タイプ」列を使用してシャドウテーブルを作成します(「削除」、「更新」など)。監査テーブルに入力する一連のトリガーを作成(または生成-以下を参照)します。

テーブルのシステムデータディクショナリを読み取り、シャドウテーブルとそれを設定する一連のトリガーを作成するスクリプトを生成するツールを作成するのは、かなり簡単です。

このためにXMLを使用しないでください。XMLストレージは、このタイプのトリガーが使用するネイティブデータベーステーブルストレージよりも効率がはるかに低くなります。


3
単純化のために+1!後の変更を恐れて過剰設計する人もいますが、ほとんどの場合、実際には変更は発生しません。さらに、履歴を1つのテーブルで管理し、実際のレコードを別のテーブルで管理する方が、すべてを1つのテーブル(悪夢)にフラグやステータスを付けて管理するよりもはるかに簡単です。これは「KISS」と呼ばれ、通常は長期的に見れば報酬が与えられます。
Jeach

+1完全に同意、正確に私が私の答えで言うこと!シンプルでパワフル!
TMS

8

Ramesh、私は最初のアプローチに基づくシステムの開発に携わっていました。
リビジョンをXMLとして保存すると、データベースが非常に大きくなり、処理速度が大幅に低下することがわかりました。
私のアプローチは、エンティティごとに1つのテーブルを持つことです。

Employee (Id, Name, ... , IsActive)  

ここで、IsActiveは最新バージョンのサインです

追加情報をリビジョンに関連付ける場合は、その情報を含む個別のテーブルを作成し、PK \ FKリレーションを使用してそれをエンティティテーブルにリンクできます。

これにより、すべてのバージョンの従業員を1つのテーブルに格納できます。このアプローチの長所:

  • シンプルなデータベース構造
  • テーブルが追加専用になるため、競合なし
  • IsActiveフラグを変更するだけで前のバージョンにロールバックできます
  • オブジェクト履歴を取得するために結合する必要はありません

主キーが一意でないことを許可する必要があることに注意してください。


6
IsActiveの代わりに、またはIsActiveに加えて、「RevisionNumber」または「RevisionDate」列を使用すると、すべてのリビジョンを順番に確認できます。
Sklivvz 2008

「parentRowId」を使用すると、以前のバージョンに簡単にアクセスできるだけでなく、ベースとエンドの両方をすばやく見つけることができます。
chacham15 2013年

6

私がこれを過去に行ったことを見た方法は

Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );

このテーブルで「更新」することはなく(isCurrentの有効性を変更する場合を除く)、新しい行を挿入するだけです。特定のEmployeeIdについて、isCurrent == 1を持つことができるのは1行のみです。

これを維持することの複雑さは、ビューとトリガーの代わりに(オラクルでは、他のRDBMSと似ていると思います)、テーブルが大きすぎてインデックスで処理できない場合はマテリアライズドビューに移動することによっても隠すことができます) 。

この方法で問題ありませんが、複雑なクエリが発生する可能性があります。

個人的に、私はあなたのデザイン2のやり方がかなり好きです。これは、私が過去に行った方法でもあります。理解しやすく、実装も保守も簡単です。

また、特に読み取りクエリを実行するときに、データベースとアプリケーションのオーバーヘッドがほとんど発生しません。これは、99%の時間で実行される可能性があります。

履歴テーブルとトリガーの作成を自動で維持するのも非常に簡単です(トリガーを介して行われる場合)。


4

データの改訂は、時間データベースの「有効時間」の概念の一側面です。これについては多くの研究が行われており、多くのパターンとガイドラインが浮かび上がってきました。私は興味のある人のためにこの質問への参照の束を含む長い返信を書きました。


4

私はあなたと私のデザインを共有します。それは、エンティティタイプごとに1つのテーブルを必要とするという点で、両方のデザインとは異なります。データベース設計を説明する最良の方法はERDを使用することですが、これは私のものです。

ここに画像の説明を入力してください

この例では、employeeという名前のエンティティがあります。userテーブルはユーザーのレコードを保持し、entityentity_revisionは2つのテーブルで、システムにあるすべてのエンティティタイプの変更履歴を保持します。このデザインの仕組みは次のとおりです。

entity_idrevision_idの2つのフィールド

システム内の各エンティティには、固有のエンティティIDがあります。エンティティは改訂される可能性がありますが、entity_idは同じままです。このエンティティIDを(外部キーとして)従業員テーブルに保持する必要があります。エンティティのタイプをエンティティテーブルに格納する必要もあります(例:「従業員」)。現在、revision_idについては、その名前が示すように、エンティティのリビジョンを追跡しています。私がこれを見つけた最良の方法は、revision_idとしてemployee_idを使用することです。つまり、さまざまな種類のエンティティのリビジョンIDが重複することになりますが、これは私には扱いません(あなたのケースについてはわかりません)。唯一重要な注意事項は、entity_idとrevision_idの組み合わせは一意でなければならないということです。

entity_revisionテーブル内には、改訂の状態を示す状態フィールドもあります。これは3つの状態のいずれかを持つことができます、または(改訂の日付に依存しないことは、あなたのクエリを後押しする大いに役立ちます)。latestobsoletedeleted

revision_idに関する最後の注意点として、employee_idとrevision_idを接続する外部キーを作成しませんでした。将来追加する可能性のある各エンティティタイプのentity_revisionテーブルを変更したくないからです。

挿入

データベースに挿入する従業員ごとに、エンティティentity_revisionにもレコードを追加します。最後の2つのレコードは、誰がいつレコードをデータベースに挿入したかを追跡するのに役立ちます。

更新

既存の従業員レコードの各更新は、2つの挿入として実装されます。1つは従業員テーブルに、もう1つはentity_revisionに挿入されます。2つ目は、誰がいつレコードを更新したかを知るのに役立ちます。

削除

従業員を削除する場合、削除を示すレコードがentity_revisionに挿入されて完了します。

このデザインでわかるように、データが変更されたりデータベースから削除されたりすることはありません。さらに重要なことに、各エンティティタイプに必要なテーブルは1つだけです。個人的には、このデザインは本当に柔軟で扱いやすいと思います。しかし、ニーズが異なる可能性があるため、私はあなたについてはわかりません。

[更新]

新しいMySQLバージョンでパーティションをサポートしたので、私のデザインにも最高のパフォーマンスの1つが付属していると思います。フィールドを使用してパーティションを作成しながら、フィールドを使用してentityテーブルをパーティション化できます。これにより、デザインがシンプルでクリーンなまま、クエリが大幅に向上します。typeentity_revisionstateSELECT


3

本当に監査証跡が必要な場合は、私は監査テーブルソリューションに頼ります(他のテーブルの重要な列の非正規化されたコピーで完了しますUserName)。ただし、その苦い経験は、単一の監査テーブルが将来の大きなボトルネックになることを示していることに注意してください。監査対象のすべてのテーブルに対して個別の監査テーブルを作成するのは、おそらく努力する価値があります。

実際の履歴(および/または将来の)バージョンを追跡する必要がある場合、標準的なソリューションは、開始、終了、および期間の値のいくつかの組み合わせを使用して、複数の行で同じエンティティを追跡することです。ビューを使用すると、現在の値に簡単にアクセスできます。これがアプローチの場合、バージョン管理されたデータが変更可能だがバージョン管理されていないデータを参照していると、問題が発生する可能性があります。


3

最初の方法を実行する場合は、EmployeesテーブルにもXMLを使用できます。最近のほとんどのデータベースでは、XMLフィールドにクエリを実行できるため、これが常に問題になるとは限りません。そして、それが最新バージョンであるか以前のバージョンであるかに関係なく、従業員データにアクセスする1つの方法がある方が簡単かもしれません。

私は2番目のアプローチを試みます。DateModifiedフィールドを持つEmployeesテーブルを1つだけ持つことで、これを簡略化できます。EmployeeId + DateModifiedが主キーとなり、行を追加するだけで新しいリビジョンを保存できます。このようにすると、古いバージョンのアーカイブとアーカイブからのバージョンの復元も簡単になります。

これを行う別の方法は、Dan Linstedtによるdatavaultモデルです。私はこのモデルを使用するオランダ統計局のためのプロジェクトを行いましたが、それは非常にうまく機能します。しかし、私はそれが日常のデータベースの使用に直接役立つとは思いません。あなたは彼の論文を読むことでいくつかのアイデアを得るかもしれません。


2

どうですか:

  • 従業員ID
  • 日付が変更されました
    • 追跡する方法に応じて、リビジョン番号
  • ModifiedByUSerId
    • 追跡したいその他の情報
  • 従業員フィールド

主キー(EmployeeId、DateModified)を作成し、「現在の」レコードを取得するには、employeeidごとにMAX(DateModified)を選択するだけです。IsCurrentを保存することは非常に悪い考えです。何よりもまず、IsCurrentを計算できるためです。次に、データの同期が取れなくなるのは非常に簡単です。

また、最新のレコードのみを一覧表示するビューを作成し、ほとんどの場合、アプリでの作業中にそれを使用することもできます。このアプローチの良い点は、データの重複がなく、すべての履歴やロールバックなどを取得するために2つの異なる場所(現在はEmployeesにあり、アーカイブはEmployeesHistoryにある)からデータを収集する必要がないことです。 。


このアプローチの欠点は、2つのテーブルを使用する場合よりもテーブルの成長が速くなることです。
cdmckay

2

(レポート上の理由で)履歴データに依存したい場合は、次のような構造を使用する必要があります。

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds the Employee revisions in rows.
"EmployeeHistories (HistoryId, EmployeeId, DateModified, OldValue, NewValue, FieldName)"

またはアプリケーションのグローバルソリューション:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, OldValue, NewValue, FieldName)"

改訂をXMLで保存することもできます。そうすると、1つの改訂に対して1つのレコードのみが存在します。これは次のようになります。

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, XMLChanges)"

1
より良い:イベントソーシングを使用してください:)
dariol 2016

1

私たちにも同様の要件があり、ユーザーが変更内容を確認するだけで、変更をロールバックする必要がない場合が多いことがわかりました。

私はあなたのユースケースが何であるかはわかりませんが、私たちが行ったことは、外部キー参照と列挙のフレンドリ名を含む、ビジネスエンティティへの変更で自動的に更新される監査テーブルを作成することでした。

ユーザーが変更を保存するたびに、古いオブジェクトをリロードし、比較を実行して変更を記録し、エンティティを保存します(問題が発生した場合に備えて、すべて単一のデータベーストランザクションで実行されます)。

これは、ユーザーにとっては非常にうまく機能しているようで、ビジネスエンティティと同じフィールドを持つ完全に独立した監査テーブルを作成するという頭痛を軽減します。


0

特定のエンティティへの変更を経時的に追跡したいようです。たとえば、ID 3、「bob」、「123 main street」、次に別のID 3、「bob」、「234 elm st」など、本質的には「ボブ」があったすべてのアドレスを示す改訂履歴を吐き出す。

これを行う最良の方法は、各レコードに「is current」フィールドを設定し、(おそらく)日付/時刻テーブルへのタイムスタンプまたはFKを設定することです。

次に、挿入は「is current」を設定し、前の「is current」レコードの「is current」を設定解除する必要があります。すべての履歴が必要でない限り、クエリは「最新」を指定する必要があります。

テーブルが非常に大きい場合、または多数のリビジョンが予想される場合は、さらに微調整を加えますが、これはかなり標準的なアプローチです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.