利用触发器动态改变分区函数

Pic*_*llo 1 database-design sql-server partitioning sql-server-2016

我想利用基于 a 的分区[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
Run Code Online (Sandbox Code Playgroud)

接下来,我将[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
Run Code Online (Sandbox Code Playgroud)

我在创建触发器之前播种第一个值。

INSERT INTO [Auth].[Tenant]
VALUES (1,'Partition Trigger Test A');
Run Code Online (Sandbox Code Playgroud)

我针对 [Tenant] 表创建触发器。

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
Run Code Online (Sandbox Code Playgroud)

我随后尝试插入第二个[Tenant]值。

INSERT INTO [Auth].[Tenant]
VALUES (1,'Partition Trigger Test B');
Run Code Online (Sandbox Code Playgroud)

此时就会出现上述错误。根据错误本身以及阅读Technet 的论据,我了解了使用中的问题AFTER INSERT。由于事务的分区操作依赖于利用分区函数内的范围值,因此失败ALTER PARTITION SCHEME,因此整个事务也失败。

AFTER 指定仅当触发 SQL 语句中指定的所有操作均已成功执行时才触发 DML 触发器。在此触发器触发之前,所有引用级联操作和约束检查也必须成功。

我已经研究过INSTEAD OF INSERT但没有取得任何成功。触发器触发一次并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
Run Code Online (Sandbox Code Playgroud)

[Tenant]由于尝试输入 0 (NULL),后续行插入会产生额外的错误。

分区函数边界值列表中不允许出现重复的范围边界值。所添加的边界值已存在于边界值列表的序号 1 处。

我该如何解决这个问题?我是否需要显式设置与IDENTITY的值?新的插入将是相当零星和最少的,但将成为其他表的约束键。这就是为什么我决定研究这种实现方法,以便动态改变配分函数。[TenantId]INSTEAD OF INSERT[Tenant][TenantId]

Dan*_*man 5

触发器中的神秘错误AFTER是由于对触发器目标表执行 DDL 造成的。使用INSTEAD OF触发器,您需要执行INSERT来获取分配的IDENTITY值,然后拆分分区函数。但是,您可能不想在这里使用 IDENTITY,因为这些间隙有时可能很大,并导致分区边界列表不整齐。

下面是一个放弃 IDENTITY 并使用 RANGE RIGHT 函数的示例,我认为这对于增量分区边界更自然。此版本仅验证插入的一行,但如果需要,可以扩展以处理多行插入。据我了解,您的用例表明只有很少的单例插入。

--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
Run Code Online (Sandbox Code Playgroud)

编辑:

我看到了您的符号,但考虑到查询将从 [Tenant] 读取,那么实际上会导致死锁的情况是否会发生相反的情况?

租户表上的粗粒度 X 锁将等待针对该表的其他并发活动(被阻止)完成,并且一旦授予,就会阻止针对该表的其他活动。此阻塞将防止触发器事务内的 DDL 操作期间租户表上的死锁。SPLIT 本身的持续时间会很快,因为行不会在分区之间移动。授予初始块 X 锁之前的阻塞持续时间将取决于其他查询运行的时间。

在多个表的情况下(即基于相同功能的方案划分的相关表),如果触发器中的锁定顺序与其他活动的锁定顺序不同,仍然可能发生死锁。触发器中对这些表的独占锁也只能减轻这种情况下死锁的可能性。例如,如果您有一个连接 Tenant 和 TenantDetails 的 SELECT 查询,两者的分区方式类似,则当查询以与触发器相反的顺序获取这些表的锁时,可能会发生死锁。

另外,据我了解,对于分区方案,您通常希望在左侧和右侧边界上留下“空”分区,以便进行正确的切换。

SPLIT空分区是和 的考虑因素,MERGE但不是SWITCH。使用SPLITin 触发器时,分割分区始终为空,因此不需要昂贵的数据移动即可符合新的边界规范。

一般的最佳实践是MERGE当两个相邻分区都为空时确定边界。也就是说,MERGE只要包含边界的分区(右侧带有函数的分区RANGE RIGHT)为空,您就可以在没有行移动的情况下进行移动。