web-dev-qa-db-ja.com

トリガーを利用してパーティション関数を動的に変更する

[TenantId](および後で日付範囲と組み合わせて)に基づくパーティション分割を利用したいと思っています。 PARTITION FUNCTION内に最新の値を手動で挿入する代わりに、TRIGGER AFTER INSERTを作成して[TenantId]の値をプルし、ALTER PARTITION FUNCTIONを作成してSPLIT RANGEに追加することを考えました。しかし、私は予期しないエラーに遭遇しています:

テーブルは現在実行中のトリガーのターゲットテーブルまたはカスケードアクションの一部であるため、テーブル 'テナント'で/それを使用してALTER PARTITION FUNCTIONを実行できません。

最初に、PARTITION FUNCTION [PF_Tenant_Isolation]でのパーティション分割のためにPARTITION SCHEME [PS_Tenant_Isolation]および[TenantId]を作成します。

CREATE PARTITION FUNCTION [PF_Tenant_Isolation] ([int])
    AS RANGE LEFT FOR VALUES (1);
GO

CREATE PARTITION SCHEME [PS_Tenant_Isolation]
    AS PARTITION [PF_Tenant_Isolation]
    ALL TO ([Auth]);
GO

これに続いて、新しく作成したパーティション構成に対して[Tenant]テーブルを作成しています。

IF OBJECT_ID('[Auth].[Tenant]', 'U') IS NULL
BEGIN
    CREATE TABLE [Auth].[Tenant] (
        [TenantId] [int] IDENTITY(1,1)
        ,[TenantActive] [bit] NOT NULL CONSTRAINT [DF_Tenant_TenantActive] DEFAULT 1
        ,[TenantName] [varchar](256) NOT NULL
        ,CONSTRAINT [PK_Tenant_TenantId] PRIMARY KEY CLUSTERED ([TenantId] ASC)
    ) ON [PS_Tenant_Isolation]([TenantId]);
END

トリガーを作成する前に、最初の値をシードします。

INSERT INTO [Auth].[Tenant]
VALUES (1,'Partition Trigger Test A');

[テナント]テーブルに対してトリガーを作成します。

CREATE TRIGGER [TR_Tenant_Isolation] ON [Auth].[Tenant]
AFTER INSERT
AS
BEGIN
    DECLARE @MaxInsertedId int
    SET @MaxInsertedId = (SELECT MAX([TenantId]) FROM inserted)

    ALTER PARTITION SCHEME [PS_Tenant_Isolation]
        NEXT USED [Auth];

    ALTER PARTITION FUNCTION [PF_Tenant_Isolation]()
        SPLIT RANGE (@MaxInsertedId);
END

これに続いて、2番目の[Tenant]値を挿入しようとします。

INSERT INTO [Auth].[Tenant]
VALUES (1,'Partition Trigger Test B');

これは、上記のエラーがポップアップするときです。エラー自体に基づいて Technet引数 を読み、AFTER INSERTを利用する際の問題を理解しています。トランザクションのパーティションアクションはパーティション関数内の範囲値の利用に依存しているため、ALTER PARTITION SCHEMEは失敗し、トランザクション全体も失敗します。

AFTERは、トリガーSQLステートメントで指定されたすべての操作が正常に実行された場合にのみDMLトリガーが起動されることを指定します。 このトリガーが起動する前に、すべての参照カスケードアクションと制約チェックも成功する必要があります。

私は INSTEAD OF INSERT を調べましたが、成功していません。トリガーは1回起動され、SPLIT RANGEを値0で更新します(暗黙的にNULLから変換されます)。これは、トランザクションの範囲でIDENTITYが適切にキャプチャされていないためと考えられます。

CREATE TRIGGER [TR_Tenant_Isolation] ON [Auth].[Tenant]
INSTEAD OF INSERT
AS
BEGIN
    DECLARE @MaxInsertedId int
    SET @MaxInsertedId =  (SELECT [TenantId] FROM inserted)

    ALTER PARTITION SCHEME [PS_Tenant_Isolation]
        NEXT USED [Auth];

    ALTER PARTITION FUNCTION [PF_Tenant_Isolation]()
        SPLIT RANGE (@MaxInsertedId);

    INSERT INTO [Auth].[Tenant] ([TenantActive], [TenantName])
    SELECT [TenantActive], [TenantName]
    FROM inserted;
END

[Tenant]への後続の行挿入は、0(NULL)を入力しようとするため、追加のエラーを生成します。

パーティション関数の境界値のリストでは、範囲の境界値の重複は許可されていません。追加される境界値は、境界値リストの序数1にすでに存在しています。

どうすればこれを回避できますか? [TenantId]とともにINSTEAD OF INSERTIDENTITY値を明示的に設定する必要がありますか? [Tenant]への新しい挿入は散発的で最小限になりますが、[TenantId]は他のテーブル間の制約キーになります。そのため、この実装方法を調査して、パーティション機能を動的に変更することにしました。

1
PicoDeGallo

AFTERトリガーの不可解なエラーは、トリガーターゲットテーブルに対してDDLを実行したことが原因です。 INSTEAD OFトリガーの場合、INSERTを実行して、割り当てられたIDENTITY値を取得し、パーティション関数を分割する必要があります。ただし、ギャップが大きくなり、パーティション境界リストが乱雑になる可能性があるため、ここではIDENTITYを使用しないでください。

以下は、IDENTITYを除外し、RANGE RIGHT関数を使用する例です。これは、インクリメンタルパーティション境界の方が自然と考えられます。このバージョンでは、挿入された行が1つだけ検証されますが、必要に応じて複数行の挿入を処理するように拡張することもできます。私が理解しているユースケースは、まれなシングルトン挿入のみを示唆しています。

--start with no partition boundaries
CREATE PARTITION FUNCTION [PF_Tenant_Isolation] ([int])
    AS RANGE RIGHT FOR VALUES ();
GO

CREATE PARTITION SCHEME [PS_Tenant_Isolation]
    AS PARTITION [PF_Tenant_Isolation]
    ALL TO ([Auth]);
GO

CREATE TABLE [Auth].[Tenant] (
     [TenantId] [int] NOT NULL
    ,[TenantActive] [bit] NOT NULL CONSTRAINT [DF_Tenant_TenantActive] DEFAULT 1
    ,[TenantName] [varchar](256) NOT NULL
    ,CONSTRAINT [PK_Tenant_TenantId] PRIMARY KEY CLUSTERED ([TenantId] ASC)
) ON [PS_Tenant_Isolation]([TenantId]);
GO

CREATE TRIGGER [TR_Tenant_Isolation] ON [Auth].[Tenant]
INSTEAD OF INSERT
AS
DECLARE @TenantId int;
BEGIN TRY

    --Get next TenantId and exclusively lock table to prevent deadlocking during DDL.
    --If other tables are partitoned via this function, add code to get exclusive locks on those too.
    SELECT TOP(1) @TenantId = COALESCE(MAX(TenantId),0) + 1 FROM [Auth].[Tenant] WITH(TABLOCKX);

    INSERT INTO [Auth].[Tenant] ([TenantId], [TenantActive], [TenantName])
        SELECT @TenantId, [TenantActive], [TenantName]
        FROM inserted;

    IF @@ROWCOUNT <> 1
    BEGIN
        RAISERROR('Exactly one row must be inserted into Auth.Tenant at a time',16,1);
    END;

    ALTER PARTITION SCHEME [PS_Tenant_Isolation]
        NEXT USED [Auth];

    ALTER PARTITION FUNCTION [PF_Tenant_Isolation]()
        SPLIT RANGE (@TenantId);

END TRY
BEGIN CATCH;
    THROW;
END CATCH;
GO

INSERT INTO [Auth].[Tenant]([TenantActive], [TenantName])
VALUES (1,'Partition Trigger Test A');
GO

編集:

私はあなたの記法を見ていますが、クエリが[テナント]から読み取ることを考えると、これが実際にデッドロックを引き起こすという反対のことが起こりませんか?

テナントテーブルの粗粒度のXロックは、テーブルに対する他の同時アクティビティが完了するのを(ブロックされる)待機し、許可されると、テーブルに対する他のアクティビティをブロックします。このブロッキングにより、トリガートランザクション内のDDL操作中にテナントテーブルのデッドロックが防止されます。行はパーティション間で移動されないため、SPLIT自体の継続時間は速くなります。最初のブロックXロックが許可される前のブロック期間は、他のクエリの実行時間によって異なります。

複数のテーブル(つまり、同じ関数に基づくスキームによってパーティション化された関連テーブル)の場合、トリガーのロック順序が他のアクティビティのロック順序と異なる場合でも、デッドロックが発生する可能性があります。トリガー内のこれらのテーブルに対する排他ロックも、その場合のデッドロックの可能性を軽減するだけです。たとえば、TenantとTenantDetailsを結合するSELECTクエリがあり、どちらも同様にパーティション分割されている場合、クエリがトリガーとしてこれらのテーブルを逆の順序でロックすると、デッドロックが発生する可能性があります。

また、パーティションスキームを使用して、適切な切り替えのために「空」のパーティションを左側と右側の境界に残したい場合も理解しています。

空のパーティションはSPLITおよびMERGEでは考慮されますが、SWITCHでは考慮されません。トリガーにSPLITを使用すると、分割パーティションは常に空になるため、新しい境界仕様に準拠するために高価なデータを移動する必要はありません。

一般的なベストプラクティスは、隣接する両方のパーティションが空の場合に境界をMERGEにすることです。つまり、境界を含むパーティション(RANGE RIGHT関数の右側)が空である限り、行を移動せずにMERGEを敷設できます。

3
Dan Guzman