使用 T-SQL Merge 语句时如何避免插入重复记录

Posted

技术标签:

【中文标题】使用 T-SQL Merge 语句时如何避免插入重复记录【英文标题】:How to avoid inserting duplicate records when using a T-SQL Merge statement 【发布时间】:2011-09-29 09:01:27 【问题描述】:

我正在尝试使用 T-SQL 的 MERGE 语句插入许多记录,但是当源表中有重复记录时,我的查询无法插入。失败的原因是:

    目标表有一个基于两列的主键 源表可能包含违反目标表主键约束的重复记录(抛出“违反主键约束”)

我正在寻找一种方法来更改我的 MERGE 语句,以便它忽略源表中的重复记录和/或尝试/捕获 INSERT 语句以捕获可能发生的异常(即所有其他 INSERT 语句都将运行不管可能发生的几个坏蛋) - 或者,也许有更好的方法来解决这个问题?

这是我要解释的查询示例。下面的示例将向临时表添加 10 万条记录,然后尝试将这些记录插入目标表中 -

编辑 在我原来的帖子中,我只在示例表中包含了两个字段,这些字段让位于 SO 朋友提供 DISTINCT 解决方案以避免 MERGE 语句中的重复。我应该提到,在我的实际问题中,表有 15 个字段,在这 15 个字段中,其中两个字段是 CLUSTERED PRIMARY KEY。所以 DISTINCT 关键字不起作用,因为我需要选择所有 15 个字段并忽略基于其中两个字段的重复项。

我已经更新了下面的查询,增加了一个字段 col4。我需要在 MERGE 中包含 col4,但我只需要确保只有 col2 和 col3 是唯一的。

-- Create the source table
CREATE TABLE #tmp (
col2 datetime NOT NULL,
col3 int NOT NULL,
col4 int
)
GO

-- Add a bunch of test data to the source table
-- For testing purposes, allow duplicate records to be added to this table
DECLARE @loopCount int = 100000
DECLARE @loopCounter int = 0
DECLARE @randDateOffset int
DECLARE @col2 datetime
DECLARE @col3 int
DECLARE @col4 int

WHILE (@loopCounter) < @loopCount
BEGIN
    SET @randDateOffset = RAND() * 100000
    SET @col2 = DATEADD(MI,@randDateOffset,GETDATE())
    SET @col3 = RAND() * 1000
    SET @col4 = RAND() * 10
    INSERT INTO #tmp
    (col2,col3,col4)
    VALUES
    (@col2,@col3,@col4);

    SET @loopCounter = @loopCounter + 1
END

-- Insert the source data into the target table
-- How do we make sure we don't attempt to INSERT a duplicate record? Or how can we 
-- catch exceptions? Or?
MERGE INTO dbo.tbl1 AS tbl
    USING (SELECT * FROM #tmp) AS src
    ON (tbl.col2 = src.col2 AND tbl.col3 = src.col3)
    WHEN NOT MATCHED THEN 
        INSERT (col2,col3,col4)
        VALUES (src.col2,src.col3,src.col4);
GO

【问题讨论】:

当#tmp 中有 col2 和 col3 的重复项时,您必须决定应该从哪一行中选择 col4。例如,您可以使用group by col2, col3min(col4) as col4 【参考方案1】:

您可以使用分析函数代替 GROUP BY,允许您在重复记录集中选择特定记录进行合并。

MERGE INTO dbo.tbl1 AS tbl
USING (
    SELECT *
    FROM (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY col2, col3 ORDER BY ModifiedDate DESC) AS Rn
        FROM #tmp
    ) t
    WHERE Rn = 1    --choose the most recently modified record
) AS src
ON (tbl.col2 = src.col2 AND tbl.col3 = src.col3)

【讨论】:

解决了我在源表中有重复行时使用MERGE 的问题。很好的解决方案。感谢分享。【参考方案2】:

已解决您的新规范。只插入 col4 的最大值:这次我使用 group by 来防止重复行。

MERGE INTO dbo.tbl1 AS tbl 
USING (SELECT col2,col3, max(col4) col4 FROM #tmp group by col2,col3) AS src 
ON (tbl.col2 = src.col2 AND tbl.col3 = src.col3) 
WHEN NOT MATCHED THEN  
    INSERT (col2,col3,col4) 
    VALUES (src.col2,src.col3,src.col4); 

【讨论】:

我犯了一个错误,在我的查询示例中只使用了两个字段。事实是,我的目标表有两个以上的字段。但是,构成 PK 集群的只有两个字段。因此,您建议的 DISTINCT 解决方案是不够的。我更新了我的原始帖子以反映其他字段。无论如何,感谢您的 og 回复(+1 按原样回答问题)。【参考方案3】:

鉴于来源有重复,并且您没有完全使用 MERGE,我会使用 INSERT。

 INSERT dbo.tbl1 (col2,col3) 
 SELECT DISTINCT col2,col3
 FROM #tmp src
 WHERE NOT EXISTS (
       SELECT *
       FROM dbo.tbl1 tbl
       WHERE tbl.col2 = src.col2 AND tbl.col3 = src.col3)

MERGE 失败的原因是它没有被逐行检查。找到所有不匹配的内容,然后尝试插入所有这些内容。它不会检查同一批次中已经匹配的行。

这让我想起了"Halloween problem",其中原子操作的早期数据更改会影响以后的数据更改:这是不正确的

【讨论】:

我自己的脚本没有测试过,你是说它失败了吗? @t-clausen.dk:你也有一个 DISTINCT,所以应该没问题。鉴于 MERGE 的限制,为什么不只使用我认为的 INSERT... @Jed:如果 2 行的键相同,为什么其他列(更新的 col4)不同?例如,这意味着不完整的密钥。对此没有解决方案,因为我们永远无法知道要占用 2 行中的哪一行... @gbn - 尽管其他列很可能没有什么不同,但在我的实际情况下这在逻辑上是可能的。如果存在具有不同 col4 值的重复 PK,我可以选择任何重复记录并丢弃所有其他记录。 只需添加一个 MAX(col4), GROUP BY col2, col3 然后到我的选择中。也为其他列添加 MAX。现在,如果您有 2 行,这会带来另一个问题:您是否只想要一行中的值?如果是这样,MAX 不会这样做。也就是说,如果您添加 MAX(col5) 那么 col4 和 col5 可能来自不同的行。我要补充一点,如果您不在乎哪一行,那么应该完全忽略这些列

以上是关于使用 T-SQL Merge 语句时如何避免插入重复记录的主要内容,如果未能解决你的问题,请参考以下文章

Oracle MERGE 死锁

Oracle MERGE语句

如何使用带有 Spring JDBC 的 MERGE 语句插入/更新单个记录

使用T-SQL语句插入更新删除数据表

如何使用相同的SqlConnection对象在多个SqlCommands中声明和使用T-SQL变量来执行多个插入?

在 T-Sql 插入语句中使用 Row Constructor 语法有啥好处?