提问人:Tyler Furman 提问时间:10/7/2023 最后编辑:Aaron BertrandTyler Furman 更新时间:10/9/2023 访问量:86
强制执行要首先检查的特定约束?
Enforce a specific constraint to be checked first?
问:
我有以下SQL Server表:
create table Account(
-- column names with their data types and properties
Id int not null identity primary key,
User varchar(50) not null,
Job varchar(200) not null,
-- table limitations
constraint UniqueUserPerAccount unique (User),
constraint UniqueJobPerAccount unique (Job),
)
对于这两个约束,我首先列出了更重要的约束,希望如果违反了这两个约束,第一个约束将被触发并显示给用户。但是,约束似乎是按列名的字母顺序声明的,因此,如果两个约束都失败,则表示违反了 UniqueJobPerAccount 错误,并且仅此错误。
我通过两种方式对此进行了测试:
尝试加载到具有重复用户和作业的 Account 表时,错误消息中会显示 UniqueJobPerAccount 错误,并且仅显示此错误。
使用 SQL Server 管理并右键单击 Account 表以“将表脚本化为”>“CREATE to”时,它按列的字母顺序提供约束,如下所示,而不是我在上面的 --table 限制中提供的约束:
CREATE TABLE [dbo].[Account] ( [Id] [int] IDENTITY(1,1) NOT NULL, [User] [varchar](50) NOT NULL, [Job] [varchar](200) NOT NULL, PRIMARY KEY CLUSTERED ( [Id] ASC ), CONSTRAINT [UniqueJobPerAccount] UNIQUE NONCLUSTERED ( [Job] ASC ), CONSTRAINT [UniqueUserPerAccount] UNIQUE NONCLUSTERED ( [User] ASC ) );
答:
我不相信您可以专门对约束进行排序。老实说,如果插入到此表中相对不常见(即:不是经常完成或每次页面加载等),我会修改您的逻辑,在插入之前先检查重复项,然后您可以根据发现的重复项返回自定义错误。当然,它需要 2 次往返数据库,而不仅仅是 1 次插入,但它可以在 99% >的情况下实现您的目标(在 和 之间会有一个小的竞争条件窗口,仍然可以像现在一样触发另一个约束)。SELECT
SELECT
INSERT
评论
假设插入大部分没有错误,您可以尝试以永远不会出错的方式插入:
insert into account(user, job)
select ?, ?
where not exists (
select *
from account
where user = ?
or job = ?
)
然后检查受其执行影响的行数。如果为 1,则插入有效。如果为 0,则可以查询数据库以找出存在哪些重复项。
在一般情况下,只对数据库进行 1 次调用。只有当出现问题时,您才会拨打第二个电话,这可能并不常见。
评论
from
正如 Aaron 在评论中指出的那样,SQL Server 不使用定义顺序来确定约束的执行顺序(好吧,有点......亚伦也有点不对劲......由于需要完全成功或完全失败,因此数据库引擎可能会按照对 SQL Server 来说似乎是最佳的任何顺序进行检查。INSERT
让我们使用您的表定义,并在表中设置两行的种子:
对于读者来说,一个重要的细节是,唯一约束是由唯一索引强制执行的。它们具有不同的元数据,但其他方面,唯一索引和唯一约束可以被认为是相同的。
CREATE TABLE dbo.Account(
-- column names with their data types and properties
Id int not null IDENTITY PRIMARY KEY,
[User] varchar(50) not null,
Job varchar(200) not null,
-- table limitations
CONSTRAINT UniqueUserPerAccount UNIQUE ([User]),
CONSTRAINT UniqueJobPerAccount UNIQUE (Job),
);
INSERT INTO dbo.Account([User],[Job])
VALUES ('Andy','MyJob'),
('Aaron','HisJob');
让我们导致错误:
此插入尝试插入 3 行 - 第一行违反了两个唯一约束,而其他两行则不会:
INSERT INTO dbo.Account([User],[Job])
VALUES ('Aaron','MyJob'),
('Billy','ThirdJob'),
('Bob','FourthJob');
正如预期的那样,我们只收到一个唯一约束冲突的错误,即 SQL Server 首先尝试执行的冲突。像你一样,我遇到了约束的违规行为:UniqueJobPerAccount
Msg 2627, Level 14, State 1, Line 33
Violation of UNIQUE KEY constraint 'UniqueJobPerAccount'. Cannot insert duplicate key in object 'dbo.Account'. The duplicate key value is (MyJob).
那么这是怎么回事呢?
如果我们看一下插入的图形估计执行计划,它并没有向我们显示太多有用的东西:
但是,如果我们查看执行计划 XML(通过右键单击图形式计划并选择“显示执行计划 XML”),我们可以找到与“聚簇索引插入”相对应的 XML:
<RelOp AvgRowSize="9" EstimateCPU="3E-06" EstimateIO="0.03" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="1" LogicalOp="Insert" NodeId="0" Parallel="false" PhysicalOp="Clustered Index Insert" EstimatedTotalSubtreeCost="0.0300044">
<OutputList />
<Update DMLRequestSort="false">
<Object Database="[/dev/null/]" Schema="[dbo]" Table="[Account]" Index="[PK__Account__3214EC076797E3BE]" IndexKind="Clustered" Storage="RowStore" />
<Object Database="[/dev/null/]" Schema="[dbo]" Table="[Account]" Index="[UniqueJobPerAccount]" IndexKind="NonClustered" Storage="RowStore" />
<Object Database="[/dev/null/]" Schema="[dbo]" Table="[Account]" Index="[UniqueUserPerAccount]" IndexKind="NonClustered" Storage="RowStore" />
<Snip ✂ />
<Snip ✂ />
<Snip ✂ />
</Update>
</RelOp>
XML 节点与“聚集索引插入”运算符相关,其下的 XML 节点定义将由该插入修改的物理索引结构。果然,我们发现它们按顺序列出:<RelOp>
<Object>
PK__Account__3214EC076797E3BE
UniqueJobPerAccount
UniqueUserPerAccount
在实践中,由于在执行计划的这一部分中排在第一位,因此首先查找重复项的唯一索引将是唯一的索引,这将是失败时错误消息报告的冲突。UniqueJobPerAccount
但是,为什么索引会按这个顺序列出呢?因为 .当您在单个原子语句中创建表和约束时,SQL Server 的唯一目标是完成您要求的操作,并且它可以方便地按照它喜欢的任何顺序创建索引。index_id
CREATE TABLE
你可以尝试用你的创建顺序来玩游戏,以强制 s 按照你想要的顺序进行——但这不是我建议的。索引可以被删除和重新创建,并且不可能保证索引始终按照应用程序代码预期的方式排序。index_id
index_id
举例来说:
如果我们删除并重新创建表,首先没有唯一约束,然后以特定顺序将它们添加为单独的语句,我们可以影响两个唯一索引的 s,从而影响执行计划,并最终影响冲突引发的错误消息。index_id
DROP TABLE IF EXISTS dbo.Account;
CREATE TABLE dbo.Account(
-- column names with their data types and properties
Id int not null IDENTITY PRIMARY KEY,
[User] varchar(50) not null,
Job varchar(200) not null
);
-- table limitations
ALTER TABLE dbo.Account
ADD CONSTRAINT UniqueUserPerAccount UNIQUE ([User]);
ALTER TABLE dbo.Account
ADD CONSTRAINT UniqueJobPerAccount UNIQUE (Job);
INSERT INTO dbo.Account([User],[Job])
VALUES ('Andy','MyJob'),
('Aaron','HisJob');
INSERT INTO dbo.Account([User],[Job])
VALUES ('Aaron','MyJob'),
('Billy','ThirdJob'),
('Bob','FourthJob');
这将返回以下错误:
Msg 2627, Level 14, State 1, Line 29
Violation of UNIQUE KEY constraint 'UniqueUserPerAccount'. Cannot insert duplicate key in object 'dbo.Account'. The duplicate key value is (Aaron).
我们可以看到执行计划XML以不同的顺序排列这些节点:<Update>
<RelOp AvgRowSize="9" EstimateCPU="3E-06" EstimateIO="0.03" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="1" LogicalOp="Insert" NodeId="0" Parallel="false" PhysicalOp="Clustered Index Insert" EstimatedTotalSubtreeCost="0.0300044">
<OutputList />
<Update DMLRequestSort="false">
<Object Database="[/dev/null/]" Schema="[dbo]" Table="[Account]" Index="[PK__Account__3214EC076797E3BE]" IndexKind="Clustered" Storage="RowStore" />
<Object Database="[/dev/null/]" Schema="[dbo]" Table="[Account]" Index="[UniqueUserPerAccount]" IndexKind="NonClustered" Storage="RowStore" />
<Object Database="[/dev/null/]" Schema="[dbo]" Table="[Account]" Index="[UniqueJobPerAccount]" IndexKind="NonClustered" Storage="RowStore" />
<Snip ✂ />
<Snip ✂ />
<Snip ✂ />
</Update>
</RelOp>
但是你真正想要什么行为?
在我的示例中,我尝试插入 3 行,但由于唯一约束冲突,所有三行都失败了。
另外,我说过,你不应该依赖现实生活中代码的内部值。那么你应该怎么做呢?index_id
如果你正在执行单行插入,你可以做这样的事情,它使用事务和锁定语义来消除竞争条件,并为你提供机会,以任意顺序抛出特定错误,从而保持一致的行为,而不考虑内部值:index_id
DECLARE @User varchar(50) = 'Aaron';
DECLARE @Job varchar(200) = 'MyJob';
BEGIN TRANSACTION
IF EXISTS (SELECT 1 FROM dbo.Account (HOLDLOCK) WHERE [User] = @User)
BEGIN
ROLLBACK;
THROW 60000, 'Duplicate value for the [User] value when inserting into dbo.Account',1;
END;
IF EXISTS (SELECT 1 FROM dbo.Account (HOLDLOCK) WHERE Job = @Job)
BEGIN
ROLLBACK;
THROW 60001, 'Duplicate value for the [Job] value when inserting into dbo.Account',1;
END;
INSERT INTO dbo.Account([User],[Job])
SELECT @User, @Job
COMMIT;
如果你正在做基于集合的插入,事情可能会变得有点棘手,你需要考虑你真正想要的行为。
- 如果要插入 3 行,但只有 1 行违反了唯一约束,那么是否仍应插入其他 2 行?
- 如果要插入 3 行,其中 2 行相互冲突(但不是表中已有的数据),是否要插入另外 1 行?
- 如果要插入 3 行,其中 2 行相互冲突(但不是表中已有的数据),是否要“统一”插入并选择要插入的两行中的一行?
- 如果要插入一个大型集合,则某些行可能与插入中的其他行冲突,某些行可能与现有数据冲突,某些行可能具有 的重复项,而其他行可能具有 的重复项。
User
Job
处理所有这些情况可能会变得复杂,尤其是在自定义错误报告时。因此,我将在这里停下来,而不是深入到弄清楚所有可能的排列的兔子洞中,因为这可能超出了你的问题范围 - 但值得一提和思考。
评论
INSTEAD OF
index_id
index_id