IF EXISTS before INSERT, UPDATE, DELETE 优化

Posted

技术标签:

【中文标题】IF EXISTS before INSERT, UPDATE, DELETE 优化【英文标题】:IF EXISTS before INSERT, UPDATE, DELETE for optimization 【发布时间】:2011-01-17 10:41:04 【问题描述】:

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

例子

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

插入或删除呢?

【问题讨论】:

为什么不看执行计划? 【参考方案1】:

我不完全确定,但我觉得这个问题真的是关于 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

如果您以后需要了解所做的事情,您甚至可以将受MERGE 影响的行OUTPUT 放入表变量中。简单、快速、无风险。去做吧。

【讨论】:

MERGE 非常酷 - 想指出,因为我最近才发现这一点:除非添加适当的提示,否则 MERGE 不会避免竞争条件 (WITH(HOLDLOCK))。见Dan Guzman's work on pointing out the UPSERT Race Condition with MERGE +1 表示“开发人员出身的 DBA 经常天真地逐行编写”!【参考方案2】:

这对于仅一次更新/删除/插入没有用。 如果在 if 条件之后有多个运算符,则可能会增加性能。 在最后一种情况下最好写

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

【讨论】:

+1。我必须承认,在第一次阅读这个问题时,(至少对我而言)这就是 OP 所指的并不明显。一定要迟到了…… 没错。执行通常需要的操作,检查结果,然后在必要时执行替代情况。 +1 我也打算走这条路,但他的问题并不清楚......很好的答案 这对UPDATEDELETE 很好,但如果有唯一索引,它可能对INSERT 根本不起作用。【参考方案3】:

您不应该为UPDATEDELETE 这样做,好像对性能有影响,它不是积极的

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

【讨论】:

【参考方案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 作为下一个可用的唯一值是不安全的。

【讨论】:

【参考方案5】:

IF EXISTS 基本上会执行 SELECT - 与 UPDATE 相同。

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

【讨论】:

另一种方法是burnall 的回答:先进行更新,然后检查@@rowcount。如果它为零,则没有更新任何内容,您调用 fall through 到插入。 IF EXISTS 不是适合插入或更新操作的良好设计。它有一个竞争条件,即使在 BEGIN TRAN/COMMIT TRAN 块内。【参考方案6】:

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

确定的最佳方法是测试这两种差异,看看哪一种能提供适当的性能。

【讨论】:

【参考方案7】:

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

是的,您拥有的 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>

【讨论】:

【参考方案8】:

IF EXISTS 语句的性能:

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

取决于满足查询的索引。

【讨论】:

【参考方案9】:

有轻微的影响,因为你做了两次相同的检查,至少在你的例子中:

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

不得不查询,看看有没有,如果有则:

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

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

【讨论】:

【参考方案10】:
IF EXISTS....UPDATE

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

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

如果它确实找到了匹配项,并且如果您在它前面加上 IF EXISTS,它必须找到两次相同的匹配项。在并发环境中,对于 EXISTS 正确的情况对于 UPDATE 可能不再正确。

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

【讨论】:

【参考方案11】:

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

【讨论】:

【参考方案12】:

如果您使用的是 mysql,那么您可以使用insert ... on duplicate。

【讨论】:

以上是关于IF EXISTS before INSERT, UPDATE, DELETE 优化的主要内容,如果未能解决你的问题,请参考以下文章

oracle insert if not exists 语句

MySQL 当记录不存在时插入(insert if not exists)

MySQL atomic insert-if-not-exists 具有稳定的自动增量

在Core Data中,如何在swift 4中执行`if exists update else insert`? [复制]

INSERT IF NOT EXISTS 类型函数供我在不将列转换为主键的情况下使用?

模型事件注意点,before_deleteafter_deletebefore_writeafter_writebefore_updateafter_updatebefore_insert