如果EXERT在INSERT,UPDATE,DELETE之前进行优化

时间:2010-02-16 15:09:26

标签: sql sql-server tsql optimization

当您需要根据某些条件执行INSERT,UPDATE或DELETE语句时,通常会出现这种情况。我的问题是,对命令性能的影响是否在命令之前添加了IF EXISTS。

实施例

IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1)
    UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1

INSERT或DELETE如何?

12 个答案:

答案 0 :(得分:70)

我不完全确定,但我觉得这个问题实际上是关于upsert,这是以下原子操作:

  • 如果源和目标中都存在该行,UPDATE目标;
  • 如果行仅存在于源中,INSERT该行进入目标;
  • (可选)如果行存在于目标中但源,则DELETE来自目标的行。

开发人员转向DBA经常天真地逐行编写,如下所示:

-- For each row in source
IF EXISTS(<target_expression>)
    IF @delete_flag = 1
        DELETE <target_expression>
    ELSE
        UPDATE target
        SET <target_columns> = <source_values>
        WHERE <target_expression>
ELSE
    INSERT target (<target_columns>)
    VALUES (<source_values>)

这是你能做的最糟糕的事情,原因如下:

  • 它有竞争条件。该行可以在IF EXISTS与后续DELETEUPDATE之间消失。

  • 这很浪费。对于每笔交易,您都要执行额外的操作;也许这是微不足道的,但这完全取决于你的索引程度。

  • 最糟糕的是 - 它遵循一个迭代模型,在一行的层面上思考这些问题。这将对所有人的整体表现产生最大(最差)的影响。

一个非常小的(我强调次要的)优化就是尝试UPDATE无论如何;如果该行不存在,@@ROWCOUNT将为0,然后您可以“安全地”插入:

-- For each row in source
BEGIN TRAN

UPDATE target
SET <target_columns> = <source_values>
WHERE <target_expression>

IF (@@ROWCOUNT = 0)
    INSERT target (<target_columns>)
    VALUES (<source_values>)

COMMIT

最糟糕的是,这仍然会为每个事务执行两个操作,但至少只有一个机会只执行一个,它也消除了竞争条件(种类)。

但真正的问题是,这仍然是针对源中的每一行进行的。

在SQL Server 2008之前,你必须使用一个笨拙的3阶段模型来处理设置级别(仍然比逐行):

BEGIN TRAN

INSERT target (<target_columns>)
SELECT <source_columns> FROM source s
WHERE s.id NOT IN (SELECT id FROM target)

UPDATE t SET <target_columns> = <source_columns>
FROM target t
INNER JOIN source s ON t.d = s.id

DELETE t
FROM target t
WHERE t.id NOT IN (SELECT id FROM source)

COMMIT

正如我所说,在这方面表现相当糟糕,但仍然比一次一行的方法好很多。但是,SQL Server 2008最终引入了MERGE语法,所以现在你所要做的就是:

MERGE target
USING source ON target.id = source.id
WHEN MATCHED THEN UPDATE <target_columns> = <source_columns>
WHEN NOT MATCHED THEN INSERT (<target_columns>) VALUES (<source_columns>)
WHEN NOT MATCHED BY SOURCE THEN DELETE;

就是这样。一个声明。如果您正在使用SQL Server 2008并且需要执行INSERTUPDATEDELETE的任何序列,具体取决于该行是否已存在 - 即使它只是一行行 - 没有借口不使用MERGE

如果您需要在事后找到所做的事情,您甚至可以OUTPUT将受MERGE影响的行{{1}}放入表变量中。简单,快速,无风险。做吧。

答案 1 :(得分:8)

仅对一次更新/删除/插入没有用 如果有几个操作员在条件之后,可能会增加性能 在最后一种情况下更好写

update a set .. where ..
if @@rowcount > 0 
begin
    ..
end

答案 2 :(得分:4)

您不应该对UPDATEDELETE执行此操作,就好像对性能有影响一样,不是肯定的

对于INSERT,可能会出现INSERT会引发异常(UNIQUE CONSTRAINT违规等)的情况,在这种情况下,您可能希望使用IF EXISTS阻止它并且更优雅地处理它。

答案 3 :(得分:4)

无论

UPDATE … IF (@@ROWCOUNT = 0) INSERT

,也不

IF EXISTS(...) UPDATE ELSE INSERT

模式在高并发性下按预期工作。两者都可能失败。两者都可能经常失败。 MERGE是国王 - 它的表现要好得多。让我们做一些压力测试并亲自看看。

以下是我们将要使用的表格:

CREATE TABLE dbo.TwoINTs
    (
      ID INT NOT NULL PRIMARY KEY,
      i1 INT NOT NULL ,
      i2 INT NOT NULL ,
      version ROWVERSION
    ) ;
GO

INSERT  INTO dbo.TwoINTs
        ( ID, i1, i2 )
VALUES  ( 1, 0, 0 ) ;    

IF EXISTS(...)THEN模式经常在高并发性下失败。

让我们使用以下简单逻辑在循环中插入或更新行:如果存在具有给定ID的行,则更新它,否则插入新行。以下循环实现此逻辑。将其剪切并粘贴到两个选项卡中,在两个选项卡中切换到文本模式,然后同时运行它们。

-- hit Ctrl+T to execute in text mode

SET NOCOUNT ON ;

DECLARE @ID INT ;

SET @ID = 0 ;
WHILE @ID > -100000
    BEGIN ;
        SET @ID = ( SELECT  MIN(ID)
                    FROM    dbo.TwoINTs
                  ) - 1 ;
        BEGIN TRY ;

            BEGIN TRANSACTION ;
            IF EXISTS ( SELECT  *
                        FROM    dbo.TwoINTs
                        WHERE   ID = @ID )
                BEGIN ;
                    UPDATE  dbo.TwoINTs
                    SET     i1 = 1
                    WHERE   ID = @ID ;
                END ;
            ELSE
                BEGIN ;
                    INSERT  INTO dbo.TwoINTs
                            ( ID, i1, i2 )
                    VALUES  ( @ID, 0, 0 ) ;
                END ;
            COMMIT ; 
        END TRY
        BEGIN CATCH ;
            ROLLBACK ; 
            SELECT  error_message() ;
        END CATCH ;
    END ; 

当我们在两个标签中同时运行此脚本时,我们将立即在两个标签中获得大量主键违规。这表明IF EXISTS模式在高并发性下执行时的不可靠性。

注意:此示例还演示如果我们在并发下使用SELECT MAX(ID)+1或SELECT MIN(ID)-1作为下一个可用的唯一值是不安全的。

答案 4 :(得分:3)

在大多数情况下,你不应该这样做。根据您的事务级别,您已经创建了一个竞争条件,现在在您的示例中,这并不重要,但数据可以从第一个选择更改为更新。而你所做的就是强制SQL做更多的工作

最明确的方法是测试这两个差异,看看哪一个给你适当的表现。

答案 5 :(得分:3)

IF EXISTS基本上会做一个SELECT - 与UPDATE相同的那个。

因此,它会降低性能 - 如果没有更新,你做了相同的工作量(UPDATE会查询与你的选择相同的行缺乏)以及是否有更新的东西,你juet做了一个不需要的选择。

答案 6 :(得分:2)

IF EXISTS声明的表现:

IF EXISTS(SELECT 1 FROM mytable WHERE someColumn = someValue)

取决于满足查询的索引。

答案 7 :(得分:2)

有一个轻微的影响,因为你进行了两次相同的检查,至少在你的例子中是这样的:

IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1)

必须查询,看看是否有,如果是,那么:

UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1

必须查询,查看哪些...同样检查两次无缘无故。现在,如果您要查找的条件已编入索引,则它应该很快,但对于大型表格,您可能会因为运行选择而看到一些延迟。

答案 8 :(得分:2)

这在很大程度上重复了前面的(按时间)五个(不,六个)(没有,七个)答案,但是:

是的,您拥有的IF EXISTS结构将使数据库完成的工作翻倍。虽然IF EXISTS在找到第一个匹配的行时会“停止”(它不需要全部找到它们),但它仍然是额外的,最终毫无意义的努力 - 用于更新和删除。

  • 如果不存在此类行,则IF EXISTS将进行全面扫描(表或索引)以确定此行。
  • 如果存在一个或多个这样的行,IF EXISTS将读取足够的表/索引来查找第一个,然后UPDATE或DELETE将重新读取该表以再次找到它并处理它 - 和它将读取表格的“其余部分”以查看是否还有其他处理过程。 (如果正确编入索引,则足够快,但仍然。)

无论哪种方式,你最终都会读完整个表或索引一次。但是,为什么要首先考虑IF EXISTS呢?

UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1 
无论是否有任何行要处理,

或类似的DELETE都可以正常运行。没有行,表扫描,没有修改,你已经完成; 1行以上,扫描表格,应该修改的所有内容,再次完成。一次通过,没有大惊小怪,没有麻烦,不必担心“我的第一个查询和第二个查询之间的数据库被另一个用户更改了”。

INSERT是可能有用的情况 - 在添加行之前检查该行是否存在,以避免主键或唯一键违规。当然你必须担心并发 - 如果其他人试图在你同时添加这一行怎么办?将这一切包装成单个INSERT将在隐式事务中处理它(记住您的ACID属性!):

INSERT Contacs (col1, col2, etc) values (val1, val2, etc) where not exists (select 1 from Contacs where col1 = val1)
IF @@rowcount = 0 then <didn't insert, process accordingly>

答案 9 :(得分:1)

是的,这会影响表现(影响表现的程度会受到多种因素的影响)。实际上,您正在“两次”执行相同的查询(在您的示例中)。问问自己,在你的查询中是否需要保持这种防御性以及在什么情况下行不存在?此外,对于更新语句,受影响的行可能是确定是否已更新任何内容的更好方法。

答案 10 :(得分:0)

如果您使用的是MySQL,则可以使用insert ... on duplicate

答案 11 :(得分:0)

IF EXISTS....UPDATE

不要这样做。它强制进行两次扫描/搜索,而不是一次。

如果更新在WHERE子句中找不到匹配项,则update语句的成本只是一个搜索/扫描。

如果确实找到了匹配项,并且如果你以IF EXISTS为前缀,它必须找到相同的匹配两次。在并发环境中,EXISTS的真实情况可能不再适用于UPDATE。

这正是UPDATE / DELETE / INSERT语句允许WHERE子句的原因。使用它!