强制执行要首先检查的特定约束?

Enforce a specific constraint to be checked first?

提问人:Tyler Furman 提问时间:10/7/2023 最后编辑:Aaron BertrandTyler Furman 更新时间:10/9/2023 访问量:86

问:

我有以下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 错误,并且仅此错误。

我通过两种方式对此进行了测试:

  1. 尝试加载到具有重复用户和作业的 Account 表时,错误消息中会显示 UniqueJobPerAccount 错误,并且仅显示此错误。

  2. 使用 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
       )
     );
    
SQL-Server 约束声明

评论

0赞 Aaron Bertrand 10/7/2023
我不相信有什么办法可以做到这一点,不。如果您需要这种级别的控制,您可以在插入之前实现触发器并检查违规行为吗?(此外,Management Studio 编写对象脚本的方式并不一定意味着这是强制执行任何内容的顺序。INSTEAD OF
0赞 Tyler Furman 10/7/2023
感谢您的快速回复和建议!我现在会坚持这一点,因为这是一种“想要”而不是“需要”。我仍然很好奇为什么它首先触发该约束,但现在只是接受,直到另有说明,因为它们都是唯一的约束,它选择按列名的字母顺序进行检查。
1赞 Aaron Bertrand 10/7/2023
SQL Server 中的许多内容并不像我们通常理解的那样完全是确定性的。例如,当有两个相同的索引时,引擎将 (IIRC) 选择首先创建的索引(由 确定)。index_id
0赞 AMtwo 10/8/2023
@AaronBertrand -- 在所有条件相同的情况下,较低的 ,但假设它们由于页面拆分等原因不是相同的 B 树,那么通常会选择在磁盘上使用较少空间的 B 树。index_id

答:

0赞 Haney 10/7/2023 #1

我不相信您可以专门对约束进行排序。老实说,如果插入到此表中相对不常见(即:不是经常完成或每次页面加载等),我会修改您的逻辑,在插入之前先检查重复项,然后您可以根据发现的重复项返回自定义错误。当然,它需要 2 次往返数据库,而不仅仅是 1 次插入,但它可以在 99% >的情况下实现您的目标(在 和 之间会有一个小的竞争条件窗口,仍然可以像现在一样触发另一个约束)。SELECTSELECTINSERT

评论

2赞 Aaron Bertrand 10/7/2023
好吧,我们可以在一次旅行中完成,并且还可以避免竞争条件,我们只需要在事务中正确隔离它。:-)
0赞 Haney 10/7/2023
@AaronBertrand是的,我考虑过这一点,但不想让事务和条件选择和插入以及返回幻数错误代码或字符串以指示唯一违规等事情变得过于复杂;)。
0赞 Tyler Furman 10/7/2023
感谢您提供有效的解决方案!但是,这不太可能发生,因此不是我进行两次旅行的理由。我更简单地对底层数据库设计及其工作原理感到好奇。:)
2赞 AMtwo 10/8/2023
@Haney --- 在 Aaron 看来,事务对于保证支票在您尝试插入时仍然有效非常重要,否则无法保证插入会成功。无论您是否使用事务,出于性能原因,我肯定会在一次往返中执行此操作。
1赞 Bohemian 10/7/2023 #2

假设插入大部分没有错误,您可以尝试以永远不会出错的方式插入:

insert into account(user, job)
select ?, ?
where not exists (
  select *
  from account
  where user = ?
  or job = ?
)

然后检查受其执行影响的行数。如果为 1,则插入有效。如果为 0,则可以查询数据库以找出存在哪些重复项。

在一般情况下,只对数据库进行 1 次调用。只有当出现问题时,您才会拨打第二个电话,这可能并不常见。

评论

1赞 Haney 10/7/2023
我比我的更喜欢这个答案
1赞 Bohemian 10/7/2023
@DavidBrowne Microsoft对不起。在想神谕。SQL Server 允许省略 。查询已修改。from
3赞 AMtwo 10/9/2023 #3

正如 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).

那么这是怎么回事呢?

如果我们看一下插入的图形估计执行计划,它并没有向我们显示太多有用的东西:Picture of execution plan showing an insert

但是,如果我们查看执行计划 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_idCREATE TABLE

你可以尝试用你的创建顺序来玩游戏,以强制 s 按照你想要的顺序进行——但这不是我建议的。索引可以被删除和重新创建,并且不可能保证索引始终按照应用程序代码预期的方式排序。index_idindex_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 行相互冲突(但不是表中已有的数据),是否要“统一”插入并选择要插入的两行中的一行?
  • 如果要插入一个大型集合,则某些行可能与插入中的其他行冲突,某些行可能与现有数据冲突,某些行可能具有 的重复项,而其他行可能具有 的重复项。UserJob

处理所有这些情况可能会变得复杂,尤其是在自定义错误报告时。因此,我将在这里停下来,而不是深入到弄清楚所有可能的排列的兔子洞中,因为这可能超出了你的问题范围 - 但值得一提和思考。