8月に、火曜日にT-SQLのスキーマ交換方法に関する投稿を書きました。このアプローチでは、基本的に、バックグラウンドでテーブル(たとえば、ある種のルックアップテーブル)のコピーを遅延ロードして、ユーザーへの干渉を最小限に抑えることができます。バックグラウンドテーブルが最新になると、更新されたデータを配信するために必要なすべての処理が行われます。ユーザーにとっては、メタデータの変更をコミットするのに十分な長さの中断です。
その投稿で、私が長年にわたって擁護してきた方法論が現在対応していない2つの警告について言及しました:外部キーの制約 および統計 。この手法を妨げる可能性のある他の機能も多数あります。最近会話で登場したもの:トリガー 。その他にも、ID列があります。 、主キーの制約 、デフォルトの制約 、チェック制約 、UDFを参照する制約 、インデックス 、ビュー (インデックス付きビューを含む 、SCHEMABINDING
が必要です )、およびパーティション 。今日はこれらすべてに対処するつもりはありませんが、正確に何が起こるかを確認するためにいくつかテストしてみようと思いました。
私の元のソリューションは基本的に貧乏人のスナップショットであり、レプリケーション、ミラーリング、可用性グループなどのソリューションの煩わしさ、データベース全体、およびライセンス要件はすべてありませんでした。これらは、T-SQLとスキーマスワップ手法を使用して「ミラーリング」されていた本番環境からのテーブルの読み取り専用コピーでした。したがって、これらの派手なキー、制約、トリガー、その他の機能は必要ありませんでした。しかし、この手法はより多くのシナリオで役立つ可能性があり、それらのシナリオでは、上記の要因のいくつかが関係する可能性があることを私は理解しています。
それでは、これらのプロパティのいくつかを持つ単純なテーブルのペアを設定し、スキーマスワップを実行して、何が壊れているかを見てみましょう。 :-)
まず、スキーマ:
CREATE SCHEMA prep; GO CREATE SCHEMA live; GO CREATE SCHEMA holder; GO
これで、live
のテーブル トリガーとUDFを含むスキーマ:
CREATE FUNCTION dbo.udf() RETURNS INT AS BEGIN RETURN (SELECT 20); END GO CREATE TABLE live.t1 ( id INT IDENTITY(1,1), int_column INT NOT NULL DEFAULT 1, udf_column INT NOT NULL DEFAULT dbo.udf(), computed_column AS CONVERT(INT, int_column + 1), CONSTRAINT pk_live PRIMARY KEY(id), CONSTRAINT ck_live CHECK (int_column > 0) ); GO CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END GO
ここで、prep
のテーブルのコピーに対して同じことを繰り返します。 。 prep
でトリガーを作成できないため、トリガーの2番目のコピーも必要です。 live
のテーブルを参照するスキーマ 、またはその逆。意図的にIDをより高いシードに設定し、int_column
のデフォルト値を変更します。 (複数のスキーマスワップの後で実際に処理しているテーブルのコピーをより正確に追跡できるようにするため):
CREATE TABLE prep.t1 ( id INT IDENTITY(1000,1), int_column INT NOT NULL DEFAULT 2, udf_column INT NOT NULL DEFAULT dbo.udf(), computed_column AS CONVERT(INT, int_column + 1), CONSTRAINT pk_prep PRIMARY KEY(id), CONSTRAINT ck_prep CHECK (int_column > 1) ); GO CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END GO
次に、各テーブルに2つの行を挿入して、出力を確認します。
SET NOCOUNT ON; INSERT live.t1 DEFAULT VALUES; INSERT live.t1 DEFAULT VALUES; INSERT prep.t1 DEFAULT VALUES; INSERT prep.t1 DEFAULT VALUES; SELECT * FROM live.t1; SELECT * FROM prep.t1;
結果:
id | int_column | udf_column | computer_column |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
id | int_column | udf_column | computer_column |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
prep.t1の結果
そしてメッセージペインで:
live.triglive.trig
prep.trig
prep.trig
それでは、簡単なスキーマスワップを実行してみましょう:
-- assume that you do background loading of prep.t1 here BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
そして、演習を繰り返します:
SET NOCOUNT ON; INSERT live.t1 DEFAULT VALUES; INSERT live.t1 DEFAULT VALUES; INSERT prep.t1 DEFAULT VALUES; INSERT prep.t1 DEFAULT VALUES; SELECT * FROM live.t1; SELECT * FROM prep.t1;
表の結果は問題ないようです:
id | int_column | udf_column | computer_column |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
3 | 1 | 20 | 2 |
4 | 1 | 20 | 2 |
id | int_column | udf_column | computer_column |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
1002 | 2 | 20 | 3 |
1003 | 2 | 20 | 3 |
prep.t1の結果
ただし、メッセージペインには、トリガー出力が間違った順序で表示されます。
prep.trigprep.trig
live.trig
live.trig
それでは、すべてのメタデータを掘り下げてみましょう。これは、関連するオブジェクトのスキーマ、名前、定義(およびのシード/最後の値)に焦点を当てて、これらのテーブルのすべてのID列、トリガー、主キー、デフォルト、およびチェック制約をすばやく検査するクエリです。 ID列):
SELECT [type] = 'Check', [schema] = OBJECT_SCHEMA_NAME(parent_object_id), name, [definition] FROM sys.check_constraints WHERE OBJECT_SCHEMA_NAME(parent_object_id) IN (N'live',N'prep') UNION ALL SELECT [type] = 'Default', [schema] = OBJECT_SCHEMA_NAME(parent_object_id), name, [definition] FROM sys.default_constraints WHERE OBJECT_SCHEMA_NAME(parent_object_id) IN (N'live',N'prep') UNION ALL SELECT [type] = 'Trigger', [schema] = OBJECT_SCHEMA_NAME(parent_id), name, [definition] = OBJECT_DEFINITION([object_id]) FROM sys.triggers WHERE OBJECT_SCHEMA_NAME(parent_id) IN (N'live',N'prep') UNION ALL SELECT [type] = 'Identity', [schema] = OBJECT_SCHEMA_NAME([object_id]), name = 'seed = ' + CONVERT(VARCHAR(12), seed_value), [definition] = 'last_value = ' + CONVERT(VARCHAR(12), last_value) FROM sys.identity_columns WHERE OBJECT_SCHEMA_NAME([object_id]) IN (N'live',N'prep') UNION ALL SELECT [type] = 'Primary Key', [schema] = OBJECT_SCHEMA_NAME([parent_object_id]), name, [definition] = '' FROM sys.key_constraints WHERE OBJECT_SCHEMA_NAME([object_id]) IN (N'live',N'prep');
結果は、メタデータがかなり混乱していることを示しています:
type | スキーマ | 名前 | 定義 |
---|---|---|---|
チェック | 準備 | ck_live | ([int_column]>(0)) |
チェック | ライブ | ck_prep | ([int_column]>(1)) |
デフォルト | 準備 | df_live1 | ((1)) |
デフォルト | 準備 | df_live2 | ([dbo]。[udf]()) |
デフォルト | ライブ | df_prep1 | ((2)) |
デフォルト | ライブ | df_prep2 | ([dbo]。[udf]()) |
トリガー | 準備 | trig_live | CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END |
トリガー | ライブ | trig_prep | CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END |
アイデンティティ | 準備 | シード=1 | last_value =4 |
アイデンティティ | ライブ | シード=1000 | last_value =1003 |
主キー | 準備 | pk_live | |
主キー | ライブ | pk_prep |
ID列と制約の問題は大きな問題ではないようです。カタログビューによると、オブジェクトは間違ったオブジェクトを指しているように見えますが、少なくとも基本的な挿入の機能は、メタデータを見たことがない場合に期待どおりに機能します。
大きな問題はトリガーにあります。私がこの例をどれほど些細なものにしたかを少し忘れてしまいました。現実の世界では、おそらくスキーマと名前でベーステーブルを参照しています。その場合、それが間違ったテーブルに取り付けられていると、物事がうまくいかない可能性があります…まあ、間違っています。元に戻しましょう:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
(メタデータクエリを再度実行して、すべてが正常に戻ったことを確認できます。)
それでは、live
で*のみ*トリガーを変更しましょう 実際に何か役に立つことをするためのバージョン(まあ、この実験の文脈では「役に立つ」):
ALTER TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN SELECT i.id, msg = 'live.trig' FROM inserted AS i INNER JOIN live.t1 AS t ON i.id = t.id; END GO
次に、行を挿入しましょう:
INSERT live.t1 DEFAULT VALUES;
結果:
id msg ---- ---------- 5 live.trig
次に、スワップを再度実行します:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
そして、別の行を挿入します:
INSERT live.t1 DEFAULT VALUES;
結果(メッセージペイン内):
prep.trig
ええとああ。このスキーマスワップを1時間に1回実行し、その後毎日12時間実行すると、テーブルの間違ったコピーに関連付けられているため、トリガーは期待どおりに実行されません。次に、トリガーの「準備」バージョンを変更しましょう。
ALTER TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN SELECT i.id, msg = 'prep.trig' FROM inserted AS i INNER JOIN prep.t1 AS t ON i.id = t.id; END GO
結果:
メッセージ208、レベル16、状態6、プロシージャtrig_prep、1行目無効なオブジェクト名'prep.trig_prep'。
まあ、それは間違いなく良くありません。メタデータがスワップされるフェーズにあるため、そのようなオブジェクトはありません。トリガーはlive.trig_prep
になりました およびprep.trig_live
。まだ混乱していますか?私も。では、これを試してみましょう:
EXEC sp_helptext 'live.trig_prep';
結果:
CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
まあ、それは面白くないですか?メタデータがそれ自体の定義に適切に反映されていない場合、このトリガーを変更するにはどうすればよいですか?これを試してみましょう:
ALTER TRIGGER live.trig_prep ON prep.t1 FOR INSERT AS BEGIN SELECT i.id, msg = 'prep.trig' FROM inserted AS i INNER JOIN prep.t1 AS t ON i.id = t.id; END GO
結果:
メッセージ2103、レベル15、状態1、プロシージャtrig_prep、1行目そのスキーマがターゲットテーブルまたはビューのスキーマと異なるため、トリガー'live.trig_prep'を変更できません。
もちろん、これも良くありません。オブジェクトを元のスキーマにスワップバックすることを伴わないこのシナリオを解決するための良い方法は実際にはないようです。このトリガーをlive.t1
に対して変更することができます :
ALTER TRIGGER live.trig_prep ON live.t1 FOR INSERT AS BEGIN SELECT i.id, msg = 'live.trig' FROM inserted AS i INNER JOIN live.t1 AS t ON i.id = t.id; END GO
しかし、今では、本文にlive.t1
に対して動作するという2つのトリガーがあります。 、しかし実際に実行されるのはこれだけです。はい、私の頭は回転しています(そして、このブログ投稿のMichael J. Swart(@MJSwart)も回転しています)。また、この混乱を解消するために、スキーマを再度スワップした後、元の名前でトリガーを削除できることに注意してください。
DROP TRIGGER live.trig_live; DROP TRIGGER prep.trig_prep;
DROP TRIGGER live.trig_prep;
を試してみると たとえば、オブジェクトが見つかりませんというエラーが発生します。
解決策?
トリガーの問題の回避策は、CREATE TRIGGER
を動的に生成することです。 スワップの一部として、コードを記述し、トリガーをドロップして再作成します。まず、live
の*current*テーブルにトリガーを戻しましょう。 (prep
でトリガーが必要かどうかは、シナリオで決定できます。 テーブルのバージョン):
CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN SELECT i.id, msg = 'live.trig' FROM inserted AS i INNER JOIN live.t1 AS t ON i.id = t.id; END GO
ここで、新しいスキーマスワップがどのように機能するかを簡単に示します(複数のトリガーがある場合は、各トリガーを処理するためにこれを調整し、prep
のスキーマに対してそれを繰り返す必要があります。 バージョン、そこでもトリガーを維持する必要がある場合。以下のコードは、簡潔にするために、live.t1
にトリガーが*1つ*しかないことを前提としていることに特に注意してください。 。
BEGIN TRANSACTION; DECLARE @sql1 NVARCHAR(MAX), @sql2 NVARCHAR(MAX); SELECT @sql1 = N'DROP TRIGGER live.' + QUOTENAME(name) + ';', @sql2 = OBJECT_DEFINITION([object_id]) FROM sys.triggers WHERE [parent_id] = OBJECT_ID(N'live.t1'); EXEC sp_executesql @sql1; -- drop the trigger before the transfer ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; EXEC sp_executesql @sql2; -- re-create it after the transfer COMMIT TRANSACTION;
別の(あまり望ましくない)回避策は、prep
に対して発生する操作を含め、スキーマスワップ操作全体を2回実行することです。 テーブルのバージョン。これは、そもそもスキーマスワップの目的を大きく損なうものです。つまり、ユーザーがテーブルにアクセスできない時間を短縮し、最小限の中断で更新されたデータを提供することです。