使用IF NOT EXIST语句

时间:2017-08-03 18:42:41

标签: sql-server debugging vbscript duplicates

我使用下面的代码插入数据并避免SQL Server中的重复行。 它在测试环境(本地SQL Server)中对我来说效果很好,但是在工作环境(远程SQL服务器)中工作了几天之后,我在表中找到了几个重复的行。我想知道它是如何可能的?主要问题我该如何调试这个问题?也许SQL Server中有一些日志显示已执行命令的历史记录? 任何建议都表示赞赏!

SQLcmd = _
    "IF ( NOT EXISTS ( SELECT * FROM " & TableName & " WHERE" & _
     " SSYS_ID = "          & SmartTags( "SSYS_ID" ) & _
     " AND TASK_ID = "      & SmartTags( "TASK_ID" ) & _
     " AND COPM_ID = "      & SmartTags( "COPM_ID" ) & _
     " AND SILAGE_ID = "    & SmartTags( "SILAGE_ID" ) & _
     " AND WCELL_ID = "     & SmartTags( "WCELL_ID" ) & _
     " ) ) " & _
    " BEGIN" & _
    " INSERT INTO " & TableName & _
            "([SSYS_ID]" & _
            ",[TASK_ID]" & _
            ",[COPM_ID]" & _
            ",[SILAGE_ID]" & _
            ",[RECIPE_ID]" & _
            ",[WCELL_ID]" & _
     "VALUES (" & _
            " " & SmartTags( "SSYS_ID" ) & _
            "," & SmartTags( "TASK_ID" ) & _
            "," & SmartTags( "COPM_ID" ) & _
            "," & SmartTags( "SILAGE_ID" ) & _
            "," & SmartTags( "RECIPE_ID" ) & _
            "," & SmartTags( "WCELL_ID" ) & _
            ")" & _
    " END"

    conn.Execute SQLcmd, RecordsAffected, adExecuteNoRecords

2 个答案:

答案 0 :(得分:2)

首先,我同意上面关于sql注入的声明。您应该切换到参数化查询。

其次,这不是线程安全的。 2个线程可能会尝试同时插入相同的值。两个线程都进行IF检查,但没有找到匹配项,然后都插入。这听起来像是在大批量生产环境中发生的事情。您需要在单个语句中执行它,例如MERGE

DECLARE
    @SSYS_ID INTEGER
    , @TASK_ID INTEGER
    , @COPM_ID INTEGER
    , @SILAGE_ID INTEGER
    , @RECIPE_ID INTEGER
    , @WCELL_ID INTEGER

MERGE TABLENAME AS target
USING
(
    SELECT
        @SSYS_ID AS SSYS_ID
        , @TASK_ID AS TASK_ID
        , @COPM_ID AS COPM_ID
        , @SILAGE_ID AS SILAGE_ID
        , @RECIPE_ID AS RECIPE_ID
        , @WCELL_ID AS WCELL_ID
) AS source
    ON
    (
        target.SSYS_ID = source.SSYS_ID
        AND target.TASK_ID = source.TASK_ID
        AND target.COPM_ID = source.COPM_ID
        AND target.SILAGE_ID = source.SILAGE_ID
        AND target.WCELL_ID = source.WCELL_ID
    )
WHEN NOT MATCHED THEN
    INSERT (SSYS_ID, TASK_ID, COPM_ID, SILAGE_ID, WCELL_ID)
    VALUES (source.SSYS_ID, source.TASK_ID, source.COPM_ID, source.SILAGE_ID, source.WCELL_ID)
;

答案 1 :(得分:1)

您所描述的问题表明多个用户同时尝试同时将相同的数据插入表中(即SSYS_IDTASK_IDCOPM_IDSILAGE_IDWCELL_ID)。在单用户开发/测试环境中,作为唯一用户,您测试代码时问题无法浮现(或难以重现)。在将应用程序部署到多用户生产环境之后问题表现出来的事实可能表明存在大量并发用户将数据插入表中,因此多个用户尝试在该表中插入相同内容的概率同一时间很高。因此,您应该仔细选择不会妨碍数据库性能的解决方案。

例如,假设有两个用户(即交易):用户A和用户B.同时两个用户都开始插入相同的数据。因此,可能会发生以下情况:

  1. User AUser B两者都开始插入相同的数据
  2. User A检查表中是否已存在包含该数据的行。数据不存在,因此User A继续插入数据。
  3. User B检查表中是否已存在具有相同数据的行。数据仍然不存在,因此User B也会继续插入数据。
  4. 然后User A继续并插入数据。
  5. 此外,User B会继续并插入相同的数据
  6. 现在,有两行具有相同的数据。
  7. 解决方案

    您可以使用以下几个选项来解决此问题:

    我建议第一个选项,考虑到大量并发用户,只需通过在列上创建唯一约束来强制执行在数据库级别拥有唯一数据的业务规则({ {1}},SSYS_IDTASK_IDCOPM_IDSILAGE_ID)。在这种情况下,您的代码应该只有WCELL_ID语句:

    INSERT

    您应该修改代码以检查是否发生了数据库错误,特别是错误2627,而不是检查SQLcmd = _ " INSERT INTO " & TableName & _ "([SSYS_ID]" & _ ",[TASK_ID]" & _ ",[COPM_ID]" & _ ",[SILAGE_ID]" & _ ",[RECIPE_ID]" & _ ",[WCELL_ID]" & _ "VALUES (" & _ " " & SmartTags( "SSYS_ID" ) & _ "," & SmartTags( "TASK_ID" ) & _ "," & SmartTags( "COPM_ID" ) & _ "," & SmartTags( "SILAGE_ID" ) & _ "," & SmartTags( "RECIPE_ID" ) & _ "," & SmartTags( "WCELL_ID" ) & _ ")" conn.Execute SQLcmd, RecordsAffected, adExecuteNoRecords 以查看是否插入了行。(如果您使用改为使用唯一索引,检查错误2601;请参阅How to troubleshoot Error 2601 Cannot insert duplicate key row in object '%.*ls' with unique index '%.*ls'. The duplicate key value is %ls.)。

    此解决方案不应像下面的第二个解决方案那样损害数据库性能。它会导致插入和更新的性能降低,从而维护唯一索引。它还将在数据库级别强制执行规则,以便无论使用哪种SQL语句插入行,都将保证将拒绝重复的行。这是今天和明天的设计。

    第二个选项是向RecordsAffected语句添加表提示,以检查是否已有行。因此,代码应如下所示:

    SELECT

    请注意IF (NOT EXISTS( SELECT * FROM YourTable WITH(UPDLOCK, HOLDLOCK) WHERE SSYS_ID = @SSYS_ID AND TASK_ID = @TASK_ID AND COPM_ID = @COPM_ID AND SILAGE_ID = @SILAGE_ID AND WCELL_ID = @WCELL_ID )) BEGIN INSERT INTO YourTable( SSYS_ID ,TASK_ID ,COPM_ID ,SILAGE_ID ,RECIPE_ID ,WCELL_ID ) VALUES ( @SSYS_ID ,@TASK_ID ,@COPM_ID ,@SILAGE_ID ,@RECIPE_ID ,@WCELL_ID ) END UPDLOCK。他们锁定表的目的是更新它直到事务结束,在你的情况下应该直到ADO命令完成。因此,HOLDLOCK锁定表,检查表中是否已有行,然后命令继续(如有必要)插入行,导致其他并发命令等待SELECT s直到插入完成,命令完成。例如,事件流可能如下所示:

    1. SELECTUser A两者都开始插入相同的数据
    2. User B锁定表并检查是否已存在包含该数据的行
    3. User A等待表格解锁以继续检查
    4. 数据没有行,因此User B会插入数据。
    5. User A的命令(事务)完成,表格解锁
    6. 当表格解锁时,User A会锁定表格并检查表格中是否有一行
    7. 表中已存在行,因此User B不会插入该重复行
    8. User B的命令(事务)完成,表格解锁
    9. 请谨慎使用此解决方案,并且仅在您的情况下无法创建唯一约束或唯一索引时,因为它会因锁定表而降低数据库性能。这些命令(lock-check-then-insert)将阻止表上的所有其他操作:其他插入,更新以及具有更新意图的其他选择。此外,如果您在代码中显式启动事务(ADO的User B),请确保在任何情况下提交(ADO的BeginTrans)或回滚(ADO的CommitTrans)事务(定期或在异常/错误处理程序中),否则一个未终止的事务将继续保持表上的锁定阻止表上的所有其他进一步操作。

      另外两个选项基本上只是前一个选项的变体:RollbackTransMERGE。除非使用相同的表提示修改它们,否则它们都不能解决并发问题。因此,使用表提示,性能会因先前的解决方案中的锁定和阻塞而受到影响。他们是:

      INSERT INTO SELECTMERGE

      HOLDLOCK

      请注意,它只有MERGE YourTable WITH (HOLDLOCK) AS dst USING ( SELECT @SSYS_ID AS SSYS_ID ,@TASK_ID AS TASK_ID ,@COPM_ID AS COPM_ID ,@SILAGE_ID AS SILAGE_ID ,@WCELL_ID AS WCELL_ID ) AS src ON dst.SSYS_ID = src.SSYS_ID AND dst.TASK_ID = src.TASK_ID AND dst.COPM_ID = src.COPM_ID AND dst.SILAGE_ID = src.SILAGE_ID AND dst.WCELL_ID = src.WCELL_ID WHEN NOT MATCHED THEN INSERT ( SSYS_ID ,TASK_ID ,COPM_ID ,SILAGE_ID ,RECIPE_ID ,WCELL_ID ) VALUES ( @SSYS_ID ,@TASK_ID ,@COPM_ID ,@SILAGE_ID ,@RECIPE_ID ,@WCELL_ID ); 。它不需要HOLDLOCK,因为它自己发出更新锁。

      UPDLOCKINSERT INTO SELECTUPDLOCK

      HOLDLOCK

      命令超时

      由于服务器负载较高且响应速度较慢,查询时间较长或有时仅因为连接速度较慢,可能会发生超时。它发生在客户端,而不是服务器上。当它发生时,客户端通知服务器有关超时的信息,然后服务器在其决定的最佳时间结束当前正在执行的语句,但它不会回滚事务(除非INSERT INTO YourTable( SSYS_ID ,TASK_ID ,COPM_ID ,SILAGE_ID ,RECIPE_ID ,WCELL_ID ) SELECT @SSYS_ID ,@TASK_ID ,@COPM_ID ,@SILAGE_ID ,@RECIPE_ID ,@WCELL_ID WHERE NOT EXISTS( SELECT * FROM YourTable WITH (UPDLOCK, HOLDLOCK) WHERE SSYS_ID = @SSYS_ID AND TASK_ID = @TASK_ID AND COPM_ID = @COPM_ID AND SILAGE_ID = @SILAGE_ID AND WCELL_ID = @WCELL_ID ) XACT_ABORT })。

      在您的情况下,可能由于服务器负载较高,在插入完成后以及事务仍在提交(自动)时发生超时。然后,当您立即重新触发该命令时,它会设法执行第一个ON语句,该语句在上一个命令的事务仍在提交时检查该行是否存在。

      要防止这种情况发生,您应该在代码中显式启动事务,调用命令,然后在命令成功时提交事务,或者在超时时回滚事务然后重试。请注意,当服务器负载很高或事务很大并且需要时间提交或回滚时,提交或回滚事务也会超时,但超时不会导致提交或回滚被停止或撤消 -  他们总是成功。

      摘要......

      1. 使新行的插入可靠。或者,创建唯一约束,或者如果唯一约束不是您的选项,请使用其中一个带有表提示的插入。
      2. 显式启动事务(SELECT),执行命令(BeginTrans),然后在成功时提交事务(Execute)或在失败时回滚它(CommitTrans })。
      3. 如果发生超时,则回滚事务并重试。
      4. 在命令中使用参数。
      5. 希望它有所帮助。