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]
触发器中的神秘错误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)为空,您就可以在没有行移动的情况下进行移动。
| 归档时间: |
|
| 查看次数: |
2814 次 |
| 最近记录: |