与有条件的INSERT和SELECT相比,与OUTPUT进行MERGE更好吗?


12

我们经常遇到“如果不存在,请插入”的情况。丹·古兹曼(Dan Guzman)的博客对如何使此过程具有线程安全性进行了出色的研究。

我有一个基本表,可以简单地将一个字符串分类为一个整数SEQUENCE。在存储过程中,我需要获取该值的整数键(如果存在),或者INSERT获取它的值。dbo.NameLookup.ItemName列上有唯一性约束,因此数据完整性不会受到威胁,但是我不想遇到异常。

这不是一个,IDENTITY所以我无法获得SCOPE_IDENTITYNULL在某些情况下该值可能是。

在我的情况下,我只需要处理INSERT桌子上的安全性,因此我试图确定使用MERGE这种更好的做法:

SET NOCOUNT, XACT_ABORT ON;

DECLARE @vValueId INT 
DECLARE @inserted AS TABLE (Id INT NOT NULL)

MERGE 
    dbo.NameLookup WITH (HOLDLOCK) AS f 
USING 
    (SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
        ON f.ItemName= new_item.val
WHEN MATCHED THEN
    UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
    INSERT
      (ItemName)
    VALUES
      (@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s

我可以MERGE只在条件INSERT后面加上一个条件就可以做到这一点,SELECT 我认为第二种方法对读者来说更清晰,但是我不认为这是“更好”的做法

SET NOCOUNT, XACT_ABORT ON;

INSERT INTO 
    dbo.NameLookup (ItemName)
SELECT
    @vName
WHERE
    NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)

DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName

也许还有我没有考虑过的另一种更好的方法

我确实搜索并引用了其他问题。这是一个:https : //stackoverflow.com/questions/5288283/sql-server-insert-if-not-exists-best-practice是我能找到的最合适的方法,但似乎不适用于我的用例。IF NOT EXISTS() THEN我认为不可接受的其他方法问题。


您是否曾尝试过大于缓冲区的表,但我曾尝试过一旦表达到一定大小,合并性能就会下降。
pacreely

Answers:


8

因为您使用的是序列,所以可以使用相同的NEXT VALUE FOR函数(您已经在IdPrimary Key字段的Default Constraint中拥有该Id函数)提前生成新值。首先生成值意味着您不必担心没有它SCOPE_IDENTITY,这意味着您不需要OUTPUT子句或执行其他操作SELECT即可获得新值;在执行之前,您将拥有值INSERT,并且您甚至不需要弄乱SET IDENTITY INSERT ON / OFF:-)

这样就可以照顾到整体情况。另一部分是在完全相同的时间处理两个进程的并发问题,而不是为完全相同的字符串找到现有的行,然后继续执行INSERT。关注点在于避免发生唯一约束违规。

处理这些类型的并发问题的一种方法是强制此特定操作为单线程。做到这一点的方法是使用应用程序锁(跨会话工作)。虽然有效,但对于像这样的情况(碰撞频率可能很低),它们可能会显得有些笨拙。

处理冲突的另一种方法是接受它们有时会发生并处理它们,而不是避免它们。使用该TRY...CATCH构造,您可以有效地捕获特定错误(在这种情况下:“唯一约束违规”,消息2601)并重新执行SELECT以获取Id值,因为我们知道由于存在于CATCH具有该特定值的块中而导致该值现在存在错误。其他错误可以在典型的处理RAISERROR/ RETURNTHROW方式。

测试设置:序列,表和唯一索引

USE [tempdb];

CREATE SEQUENCE dbo.MagicNumber
  AS INT
  START WITH 1
  INCREMENT BY 1;

CREATE TABLE dbo.NameLookup
(
  [Id] INT NOT NULL
         CONSTRAINT [PK_NameLookup] PRIMARY KEY CLUSTERED
        CONSTRAINT [DF_NameLookup_Id] DEFAULT (NEXT VALUE FOR dbo.MagicNumber),
  [ItemName] NVARCHAR(50) NOT NULL         
);

CREATE UNIQUE NONCLUSTERED INDEX [UIX_NameLookup_ItemName]
  ON dbo.NameLookup ([ItemName]);
GO

测试设置:存储过程

CREATE PROCEDURE dbo.GetOrInsertName
(
  @SomeName NVARCHAR(50),
  @ID INT OUTPUT,
  @TestRaceCondition BIT = 0
)
AS
SET NOCOUNT ON;

BEGIN TRY
  SELECT @ID = nl.[Id]
  FROM   dbo.NameLookup nl
  WHERE  nl.[ItemName] = @SomeName
  AND    @TestRaceCondition = 0;

  IF (@ID IS NULL)
  BEGIN
    SET @ID = NEXT VALUE FOR dbo.MagicNumber;

    INSERT INTO dbo.NameLookup ([Id], [ItemName])
    VALUES (@ID, @SomeName);
  END;
END TRY
BEGIN CATCH
  IF (ERROR_NUMBER() = 2601) -- "Cannot insert duplicate key row in object"
  BEGIN
    SELECT @ID = nl.[Id]
    FROM   dbo.NameLookup nl
    WHERE  nl.[ItemName] = @SomeName;
  END;
  ELSE
  BEGIN
    ;THROW; -- SQL Server 2012 or newer
    /*
    DECLARE @ErrorNumber INT = ERROR_NUMBER(),
            @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();

    RAISERROR(N'Msg %d: %s', 16, 1, @ErrorNumber, @ErrorMessage);
    RETURN;
    */
  END;

END CATCH;
GO

考试

DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
  @SomeName = N'test1',
  @ID = @ItemID OUTPUT;
SELECT @ItemID AS [ItemID];
GO

DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
  @SomeName = N'test1',
  @ID = @ItemID OUTPUT,
  @TestRaceCondition = 1;
SELECT @ItemID AS [ItemID];
GO

OP的问题

为什么比这更好MERGE?没有TRY使用WHERE NOT EXISTS子句,我是否会获得相同的功能?

MERGE有各种“问题”(@SqlZim的答案中链接了多个引用,因此无需在此处重复该信息)。而且,此方法没有其他锁定(争用较少),因此并发性应该更好。在这种方法中,您将永远不会遇到唯一约束违规,而没有任何违规HOLDLOCK,等等。它几乎可以保证正常工作。

这种方法背后的原因是:

  1. 如果您有足够的执行此过程的时间,以便您需要担心冲突,那么您不想:
    1. 采取不必要的步骤
    2. 锁定任何资源的时间超出必要
  2. 由于冲突仅可能发生在新条目(恰好同时提交新条目)上,CATCH因此首先掉入该块的频率会非常低。优化将在99%的时间运行的代码而不是将在1%的时间运行的代码更有意义(除非对这两者进行优化没有成本,但是这里不是这种情况)。

@SqlZim的答案的注释(强调已添加)

我个人更喜欢尝试并制定解决方案,以尽可能避免这样做。在这种情况下,我不认为使用from锁serializable是徒劳的方法,并且我相信它可以很好地处理高并发性。

如果将第一句话修改为“和_谨慎”,我将同意。仅仅因为某种技术上可行,并不意味着这种情况(即预期的用例)将从中受益。

我看到的这种方法的问题是,它的锁定程度超出了建议的范围。重读有关“可序列化”的引用文档很重要,特别是以下内容(添加了重点):

  • 在当前事务完成之前,其他事务不能插入键值将落入当前事务中任何语句读取的键范围内的新行。

现在,这是示例代码中的注释:

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */

这里的操作词是“范围”。锁定不仅取决于in中的值@vName,而且更准确地说是新值应到达的位置(即新值适合的任一侧的现有键值之间),但值本身不行。这意味着,其他进程将被阻止插入新值,具体取决于当前正在查找的值。如果在范围的顶部进行查找,则将阻止插入任何可能占据相同位置的内容。例如,如果存在值“ a”,“ b”和“ d”,则如果一个进程在“ f”上执行SELECT,则将不可能插入值“ g”甚至“ e”(因为其中任何一个都将在“ d”之后立即出现)。但是,可以插入值“ c”,因为它不会放在“保留”范围内。

下面的示例应说明此行为:

(在查询标签(即会话)#1中)

INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'test5');

BEGIN TRAN;

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE  ItemName = N'test8';

--ROLLBACK;

(在查询标签(即会话)#2中)

EXEC dbo.NameLookup_getset_byName @vName = N'test4';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'test9';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

EXEC dbo.NameLookup_getset_byName @vName = N'test7';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

EXEC dbo.NameLookup_getset_byName @vName = N's';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'u';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

同样,如果存在值“ C”,并且已选择值“ A”(因此已锁定),则可以插入值“ D”,但不能插入值“ B”:

(在查询标签(即会话)#1中)

INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'testC');

BEGIN TRAN

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE  ItemName = N'testA';

--ROLLBACK;

(在查询标签(即会话)#2中)

EXEC dbo.NameLookup_getset_byName @vName = N'testD';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'testB';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

公平地说,在我建议的方法中,当出现异常时,在事务日志中将有4个条目不会在这种“可序列化事务”方法中发生。但是,如上所述,如果异常发生的时间为1%(甚至5%),则其影响要比初始SELECT临时阻止INSERT操作的可能性要大得多。

这种“可序列化的事务+ OUTPUT子句”方法的另一个(尽管较小)问题是该OUTPUT子句(按其当前用法)将数据作为结果集发送回。与简单OUTPUT参数相比,结果集需要更多的开销(可能在两端:在SQL Server中用于管理内部游标,在应用程序层中用于管理DataReader对象)。假设我们只处理单个标量值,并且假设执行频率很高,那么结果集的额外开销可能会加起来。

尽管OUTPUT可以以返回OUTPUT参数的方式使用该子句,但这将需要其他步骤来创建临时表或表变量,然后从该临时表/表变量中选择值作为OUTPUT参数。

进一步说明:对@SqlZim的响应(更新后的答案)的响应对我的有关并行性和性能的声明的@SqlZim的响应(在原始答案中);-)

很抱歉,如果这部分很长,但是在这一点上,我们只能考虑两种方法的细微差别。

我认为,信息的呈现方式可能导致错误的假设,即serializable在原始问题中提出的场景中使用锁时,可能会遇到的锁定量。

是的,尽管坦率地说,我承认我有偏见:

  1. 对于人类来说,至少在很小的程度上不存在偏见是不可能的,而我确实试图将其保持在最低水平,
  2. 给出的示例很简单,但这只是为了说明目的,目的是在不过度复杂化的情况下传达行为。虽然我确实理解我也没有明确声明其他含义,但这并不意味着暗示频率过高,这可以理解为暗示着比实际存在的问题更大的问题。我将在下面尝试澄清。
  3. 我还提供了一个锁定两个现有键(第二组“查询选项卡1”和“查询选项卡2”块)之间的范围的示例。
  4. 我确实发现(并自愿)我的方法的“隐性成本”,即每次INSERT由于唯一约束违规而失败时,都会有四个额外的交易日志条目。我还没有看到其他任何答案/帖子中提到的内容。

关于@gbn的“ JFDI”方法,Michael J. Swart的“ Ugly Pragmatism For The Win”帖子,以及Aaron Bertrand对Michael的帖子的评论(关于他的测试显示了哪些情况降低了性能),以及您对“适应Michael J的评论”斯图尔特对@gbn的“尝试捕获JFDI”过程的改编”指出:

如果您要比选择现有值更频繁地插入新值,则这可能比@srutzky的版本更具性能。否则,我更喜欢@srutzky的版本。

关于gbn / Michael / Aaron关于“ JFDI”方法的讨论,将我的建议等同于gbn的“ JFDI”方法是不正确的。由于“获取或插入”操作的性质,非常需要执行SELECT来获取ID现有记录的值。此SELECT用作IF EXISTS检查,这使此方法更等同于Aaron测试的“ CheckTryCatch”变体。Michael的重写代码(以及您对Michael的改编的最终改编)还包括一个WHERE NOT EXISTS先进行相同检查的代码。因此,我的建议(连同Michael的最终代码以及您对他的最终代码的修改)实际上并不会CATCH经常出现。可能只有两次会议,ItemNameINSERT...SELECT在完全相同的时刻,这两个会话在完全相同的时刻都收到“ true” WHERE NOT EXISTS,因此都试图INSERT在完全相同的时刻进行。这种非常具体的情况发生的频率比没有其他进程在同一时刻尝试选择现有的情况ItemName或插入新的情况要少得多。ItemName

考虑到所有以上这些:为什么我更喜欢我的方法?

首先,让我们看一下“可序列化”方法中发生的锁定。如上所述,锁定的“范围”取决于新键值适合的任一侧的现有键值。如果在该方向上不存在键值,则范围的开始或结束也可以分别是索引的开始或结束。假设我们有以下索引和键(^代表索引的开始而$代表索引的结束):

Range #:    |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value:  ^         C         F         J         $

如果会话55尝试插入以下键值:

  • A,则范围#1(从^C)被锁定:会话56无法插入值B,即使唯一有效(尚未)。但会议56可以插入值DGM
  • D,则范围#2(从CF)被锁定:会话56无法插入值E(尚未)。但会议56可以插入值AGM
  • M,则范围#4(从J$)被锁定:会话56无法插入值X(尚未)。但会议56可以插入值ADG

随着添加更多的键值,键值之间的范围会变窄,从而降低了在同一时间范围内同时插入多个值的可能性/频率。诚然,这不是主要问题,幸运的是,这似乎是随着时间的推移实际上减少的问题。

上面描述了我的方法的问题:仅当两个会话试图同时插入相同的键值时才会发生。在这方面,归结为发生的可能性更高:同时尝试两个不同但接近的键值,还是同时尝试相同的键值?我想答案就在于执行插入操作的应用程序的结构,但总的来说,我认为插入的是恰好共享同一范围的两个不同值的可能性更大。但是真正知道的唯一方法是在OPs系统上进行测试。

接下来,让我们考虑两种情况以及每种方法如何处理它们:

  1. 所有请求都是唯一的键值:

    在这种情况下,CATCH我的建议中的块永远不会输入,因此不会出现“问题”(即4个tran日志条目及其花费的时间)。但是,在“可序列化”方法中,即使所有插入都是唯一的,在相同范围内(尽管时间不会很长),总有可能会阻塞其他插入。

  2. 同时请求相同键值的频率很高:

    在这种情况下-就传入请求中不存在的键值而言,唯一性非常低- CATCH我会定期输入建议中的代码块。这样的结果是,每个失败的插入将需要自动回滚并将这4个条目写入事务日志,这每次都会对性能造成轻微的影响。但是整个操作永远都不会失败(至少不是由于这个原因)。

    (以前版本的“更新”方法存在一个问题,使它遭受死锁。updlock添加了提示以解决此问题,并且它不再陷入死锁。)但是,在“可序列化”方法(甚至是更新的优化版本)中,操作将死锁。为什么?因为该serializable行为仅阻止INSERT已读取并因此被锁定的范围内的操作;它不会阻止SELECT在该范围内进行操作。

    serializable在这种情况下,该方法似乎没有额外的开销,并且可能比我建议的性能稍好。

与许多/大多数关于性能的讨论一样,由于会影响结果的因素太多,因此,真正了解某项效果的唯一方法就是在目标环境中进行测试。到那时,这将不再是意见问题了。


7

更新的答案


回复@srutzky

这种“可序列化事务+ OUTPUT子句”方法的另一个(尽管较小)问题是OUTPUT子句(按其当前用法)将数据作为结果集发送回。与简单的OUTPUT参数相比,结果集需要更多的开销(可能在两端:在SQL Server中用于管理内部游标,在应用程序层中用于管理DataReader对象)。假设我们只处理单个标量值,并且假设执行频率很高,那么结果集的额外开销可能会加起来。

我同意,出于同样的原因,谨慎时,我也会使用输出参数。我很懒,我没有在初始答案上使用输出参数是我的错误。

下面是使用一个输出参数,额外的优化,随着修订过程next value for@srutzky在他的回答解释说

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50), @vValueId int output) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                                        /* if @vName is empty, return early */
  select  @vValueId = Id                                              /* go get the Id */
    from  dbo.NameLookup
    where ItemName = @vName;
  if @vValueId is not null                                 /* if we got the id, return */
    return;
  begin try;                                  /* if it is not there, then get the lock */
    begin tran;
      select  @vValueId = Id
        from  dbo.NameLookup with (updlock, serializable) /* hold key range for @vName */
        where ItemName = @vName;
      if @@rowcount = 0                    /* if we still do not have an Id for @vName */
      begin;                                         /* get a new Id and insert @vName */
        set @vValueId = next value for dbo.IdSequence;      /* get next sequence value */
        insert into dbo.NameLookup (ItemName, Id)
          values (@vName, @vValueId);
      end;
    commit tran;
  end try
  begin catch;
    if @@trancount > 0 
      begin;
        rollback transaction;
        throw;
      end;
  end catch;
end;

更新说明updlock在这种情况下,包括select 在内将获得适当的锁。感谢@srutzky,他指出,仅serializable在上使用时,这可能会导致死锁select

注意:可能不是这种情况,但是如果可能的话,将使用的值@vValueId,包括set @vValueId = null;after 来调用该过程set xact_abort on;,否则可以将其删除。


关于@srutzky的键范围锁定行为的示例:

@srutzky仅在他的表中使用一个值,并锁定“ next” /“ infinity”键进行测试,以说明键范围的锁定。尽管他的测试说明了在这些情况下会发生什么,但我认为,提供信息的方式可能导致错误的假设,即serializable在原始问题中提出的方案中使用锁时,可能会遇到的锁定量。

即使我在他的解释和键范围锁定示例的方式上感觉到偏差(也许是错误的),它们仍然是正确的。


经过更多研究,我发现了Michael J. Swart在2011年撰写的一篇特别相关的博客文章:神话:并发更新/插入解决方案。在其中,他测试了多种方法的准确性和并发性。方法4:提高隔离度+微调锁定基于Sam Saffron 在SQL Server上发表的插入或更新模式,并且是原始测试中唯一可以满足他期望的方法(稍后加入merge with (holdlock))。

2016年2月,迈克尔·J·斯瓦特(Michael J. Swart)发表了《丑陋的实用主义》一书。在那篇文章中,他介绍了他对Saffron upsert过程所做的一些其他调整,以减少锁定(我在上面的过程中包括了这一点)。

进行了这些更改之后,Michael不满意他的程序开始变得更加复杂,并向名为Chris的同事咨询了。克里斯阅读了Mythbusters的所有原始帖子并阅读了所有评论,并询问了@gbn TRY CATCH JFDI模式。这种模式类似于@srutzky的答案,并且是Michael在该实例中最终使用的解决方案。

迈克尔·J·斯瓦特:

昨天,我对最好的并发方法改变了主意。我在“神话破灭”中描述了几种方法:并发更新/插入解决方案。我的首选方法是增加隔离级别和微调锁定。

至少那是我的偏爱。我最近改变了方法,使用了gbn在评论中建议的方法。他将他的方法描述为“ TRY CATCH JFDI模式”。通常我会避免类似的解决方案。有一条经验法则说,开发人员不应依赖捕获错误或异常来控制流程。但是我昨天打破了这一经验法则。

顺便说一句,我喜欢gbn对“ JFDI”模式的描述。这让我想起了什叶派拉博夫的励志视频。


我认为,两种解决方案都是可行的。虽然我仍然希望提高隔离级别和微调锁定,但@srutzky的答案也有效,在您的特定情况下可能会或可能不会更有效。

也许将来我也会得出与迈克尔·J·斯瓦特(Michael J. Swart)相同的结论,但是我还没有到。


这不是我的偏爱,但这是我对Michael J. Stewart对@gbn的Try Catch JFDI过程进行的改编的内容,如下所示:

create procedure dbo.NameLookup_JFDI (
    @vName nvarchar(50)
  , @vValueId int output
  ) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                     /* if @vName is empty, return early */
  begin try                                                 /* JFDI */
    insert into dbo.NameLookup (ItemName)
      select @vName
      where not exists (
        select 1
          from dbo.NameLookup
          where ItemName = @vName);
  end try
  begin catch        /* ignore duplicate key errors, throw the rest */
    if error_number() not in (2601, 2627) throw;
  end catch
  select  @vValueId = Id                              /* get the Id */
    from  dbo.NameLookup
    where ItemName = @vName
  end;

如果您要比选择现有值更频繁地插入新值,那么它可能比@srutzky的version更具性能。否则,我更喜欢@srutzky的版本

亚伦·伯特兰(Aaron Bertrand)对迈克尔·斯瓦特(Michael J Swart)的帖子的评论与他所做的相关测试有关,并促成了此次交流。摘自“ 丑陋实用主义致胜”评论部分:

但是,有时JFDI会导致总体性能下降,具体取决于呼叫失败的百分比。引发异常会产生大量开销。我在几篇文章中对此进行了展示:

http://sqlperformance.com/2012/08/t-sql-queries/error-handling

https://www.mssqltips.com/sqlservertip/2632/checking-for-potential-constraint-violations-before-entering-sql-server-try-and-catch-logic/

Aaron Bertrand的评论— 2016年2月11日上午11:49

以及以下回复:

您说对了Aaron,我们进行了测试。

事实证明,在我们的案例中,失败的呼叫百分比为0(四舍五入到最接近的百分比)。

我认为您可以说明这一点,即应尽可能遵循以下经验法则对案例进行评估。

这也是为什么我们添加了非严格必要的WHERE NOT EXISTS子句的原因。

Michael J. Swart的评论— 2016年2月11日上午11:57


新链接:


原始答案


我仍然更喜欢Sam Saffron upsert方法merge而不是using ,特别是在处理单行时。

我将使upsert方法适应这种情况:

declare @vName nvarchar(50) = 'Invader';
declare @vValueId int       = null;

if nullif(@vName,'') is not null /* this gets your where condition taken care of before we start doing anything */
begin tran;
  select @vValueId = Id
    from dbo.NameLookup with (serializable) 
    where ItemName = @vName;
  if @@rowcount > 0 
    begin;
      select @vValueId as id;
    end;
    else
    begin;
      insert into dbo.NameLookup (ItemName)
        output inserted.id
          values (@vName);
      end;
commit tran;

我会与您的命名保持一致,并且serializable与相同holdlock,请选择一个并保持其使用一致。我倾向于使用serializable它,因为它与指定时使用的名称相同set transaction isolation level serializable

通过使用serializableholdlock基于范围的值,@vName如果其他操作选择或插入dbo.NameLookup包含where子句中值的值,则其他操作将等待。

为了使范围锁正常工作,ItemName在使用时在该列上也需要有一个索引merge


这是按照Erland Sommarskog的错误处理白皮书(使用)进行处理的大致过程。如果不是引发错误的方法,请更改它以与其余过程保持一致:throwthrow

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50) ) as
begin
  set nocount on;
  set xact_abort on;
  declare @vValueId int;
  if nullif(@vName,'') is null /* if @vName is null or empty, select Id as null */
    begin
      select Id = cast(null as int);
    end 
    else                       /* else go get the Id */
    begin try;
      begin tran;
        select @vValueId = Id
          from dbo.NameLookup with (serializable) /* hold key range for @vName */
          where ItemName = @vName;
        if @@rowcount > 0      /* if we have an Id for @vName select @vValueId */
          begin;
            select @vValueId as Id; 
          end;
          else                     /* else insert @vName and output the new Id */
          begin;
            insert into dbo.NameLookup (ItemName)
              output inserted.Id
                values (@vName);
            end;
      commit tran;
    end try
    begin catch;
      if @@trancount > 0 
        begin;
          rollback transaction;
          throw;
        end;
    end catch;
  end;
go

总结以上过程中发生的情况: set nocount on; set xact_abort on;就像您总是一样,然后如果我们的输入变量为is null空,则为select id = cast(null as int)结果。如果它不为null或为空,则Id保留该位置的情况下获取变量的for ,以防该位置不存在。如果Id存在,请将其发送出去。如果不存在,请将其插入并发送新的Id

同时,对该过程的其他调用试图找到具有相同值的ID,将等到第一个事务完成后再选择并返回它。对该过程的其他调用或寻找其他值的其他语句将继续进行,因为这不会妨碍您。

尽管我同意@srutzky的观点,即您可以处理冲突并解决此类问题的异常情况,但我个人更喜欢尝试制定一种解决方案,以尽可能避免这样做。在这种情况下,我不认为使用from锁serializable是徒劳的方法,并且我相信它可以很好地处理高并发性。

SQL Server文档中serializableholdlock引用表提示/

可序列化

相当于HOLDLOCK。通过保持共享锁直到事务完成,使共享锁更具限制性,而不是在不再需要所需的表或数据页时释放共享锁,而不管事务是否已完成。使用与SERIALIZABLE隔离级别上运行的事务相同的语义执行扫描。有关隔离级别的更多信息,请参见SET TRANSACTION ISOLATION LEVEL(Transact-SQL)。

引用SQL Server文档中有关事务隔离级别的信息serializable

SERIALIZABLE指定以下内容:

  • 语句无法读取已被其他事务修改但尚未提交的数据。

  • 在当前事务完成之前,没有其他事务可以修改当前事务已读取的数据。

  • 在当前事务完成之前,其他事务不能插入键值将落入当前事务中任何语句读取的键范围内的新行。


与上述解决方案相关的链接:

MERGE历史悠久,似乎需要花很多时间来确保代码在所有语法下都表现出您希望的样子。相关merge文章:

最后一个链接,肯德拉·利特尔Kendra Little)与vs进行了粗略的比较mergeinsert with left join,但要注意的是,她说“我没有对此进行全面的负载测试”,但这仍然是一本好书。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.