sql >> データベース >  >> RDS >> Database

スキーマスイッチ-A-Roo:パート2

    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

    live.t1の結果

    id int_column udf_column computer_column
    1000

    2 20 3
    1001

    2 20 3

    prep.t1の結果

    そしてメッセージペインで:

    live.trig
    live.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

    live.t1の結果

    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.trig
    prep.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

    メタデータduck-duck-goose

    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回実行することです。 テーブルのバージョン。これは、そもそもスキーマスワップの目的を大きく損なうものです。つまり、ユーザーがテーブルにアクセスできない時間を短縮し、最小限の中断で更新されたデータを提供することです。


    1. MS-Accessレコードセットおよびクラスモジュール

    2. MySQLまたはMariaDBデータベースをSQLインジェクションから保護する方法:パート1

    3. MariaDBでのLOCATE()のしくみ

    4. SQL Server 2008データベースを使用してvb.netまたはC#デスクトップアプリケーションにPower BIレポートとダッシュボードを埋め込む方法はありますか?