【问题标题】:Using merge..output to get mapping between source.id and target.id使用 merge..output 获取 source.id 和 target.id 之间的映射
【发布时间】:2011-07-18 22:45:16
【问题描述】:

很简单,我有两张表Source和Target。

declare @Source table (SourceID int identity(1,2), SourceName varchar(50))
declare @Target table (TargetID int identity(2,2), TargetName varchar(50))

insert into @Source values ('Row 1'), ('Row 2')

我想将所有行从 @Source 移动到 @Target 并知道每个 SourceIDTargetID 因为还有表 SourceChildTargetChild 也需要复制,并且我需要将新的TargetID 添加到TargetChild.TargetID FK 列中。

有几个解决方案。

  1. 使用 while 循环或游标一次将一行 (RBAR) 插入到 Target,并使用 scope_identity() 填充 TargetChild 的 FK。
  2. 将临时列添加到@Target 并插入SourceID。然后,您可以加入该列以获取 TargetID 中的 FK 的 TargetChild
  3. SET IDENTITY_INSERT OFF 用于 @Target 并自己处理分配新值。您会得到一个范围,然后在 TargetChild.TargetID 中使用。

我不是很喜欢他们中的任何一个。我目前使用的是游标。

我真正想做的是使用插入语句的output 子句。

insert into @Target(TargetName)
output inserted.TargetID, S.SourceID
select SourceName
from @Source as S

但这是不可能的

The multi-part identifier "S.SourceID" could not be bound.

但是可以通过合并来实现。

merge @Target as T
using @Source as S
on 0=1
when not matched then
  insert (TargetName) values (SourceName)
output inserted.TargetID, S.SourceID;

结果

TargetID    SourceID
----------- -----------
2           1
4           3

我想知道你有没有用过这个?如果您对解决方案有任何想法或发现任何问题?它在简单的场景中运行良好,但当查询计划由于复杂的源查询而变得非常复杂时,可能会发生一些丑陋的事情。最坏的情况是 TargetID/SourceID 对实际上不匹配。

MSDN 对output 子句的from_table_name 有这样的说法。

是一个列前缀,它指定包含在 DELETE、UPDATE 或 MERGE 语句的 FROM 子句中的表,用于指定要更新或删除的行。

出于某种原因,他们不会说“要插入、更新或删除的行”,而只会说“要更新或删除的行”。

欢迎提出任何想法,非常感谢对原始问题完全不同的解决方案。

【问题讨论】:

  • 他们没有提到'insert'的原因是因为from_table_name在insert into/output语句中是无效的,“deleted”前缀也是如此(因为没有现有数据可以通过插入来更改)跨度>
  • Adam Machanic 关于合并功能的博文太棒了!解决了我的确切问题。感谢马丁史密斯发帖。希望我能给的不仅仅是 +1
  • Adam Machanic 文章的替代链接dataeducation.com/…

标签: sql-server sql-server-2008 merge


【解决方案1】:

在我看来,这是对 MERGE 和输出的一个很好的使用。我已经在几个场景中使用过,到目前为止还没有遇到任何奇怪的事情。 例如,这是将文件夹和其中的所有文件(身份)克隆到新创建的文件夹(guid)中的测试设置。

DECLARE @FolderIndex TABLE (FolderId UNIQUEIDENTIFIER PRIMARY KEY, FolderName varchar(25));
INSERT INTO @FolderIndex 
    (FolderId, FolderName)
    VALUES(newid(), 'OriginalFolder');

DECLARE @FileIndex TABLE (FileId int identity(1,1) PRIMARY KEY, FileName varchar(10));
INSERT INTO @FileIndex 
    (FileName)
    VALUES('test.txt');

DECLARE @FileFolder TABLE (FolderId UNIQUEIDENTIFIER, FileId int, PRIMARY KEY(FolderId, FileId));
INSERT INTO @FileFolder 
    (FolderId, FileId)
    SELECT  FolderId, 
            FileId
    FROM    @FolderIndex
    CROSS JOIN  @FileIndex;  -- just to illustrate

DECLARE @sFolder TABLE (FromFolderId UNIQUEIDENTIFIER, ToFolderId UNIQUEIDENTIFIER);
DECLARE @sFile TABLE (FromFileId int, ToFileId int);

-- copy Folder Structure
MERGE @FolderIndex fi
USING   (   SELECT  1 [Dummy],
                    FolderId, 
                    FolderName
            FROM    @FolderIndex [fi]
            WHERE   FolderName = 'OriginalFolder'
        ) d ON  d.Dummy = 0
WHEN NOT MATCHED 
THEN INSERT 
    (FolderId, FolderName)
    VALUES (newid(), 'copy_'+FolderName)
OUTPUT  d.FolderId,
        INSERTED.FolderId
INTO    @sFolder (FromFolderId, toFolderId);

-- copy File structure
MERGE   @FileIndex fi
USING   (   SELECT  1 [Dummy],
                    fi.FileId, 
                    fi.[FileName]
            FROM    @FileIndex fi
            INNER
            JOIN    @FileFolder fm ON 
                    fi.FileId = fm.FileId
            INNER
            JOIN    @FolderIndex fo ON 
                    fm.FolderId = fo.FolderId
            WHERE   fo.FolderName = 'OriginalFolder'
        ) d ON  d.Dummy = 0
WHEN NOT MATCHED 
THEN INSERT ([FileName])
    VALUES ([FileName])
OUTPUT  d.FileId,
        INSERTED.FileId
INTO    @sFile (FromFileId, toFileId);

-- link new files to Folders
INSERT INTO @FileFolder (FileId, FolderId)
    SELECT  sfi.toFileId, sfo.toFolderId
    FROM    @FileFolder fm
    INNER
    JOIN    @sFile sfi ON  
            fm.FileId = sfi.FromFileId
    INNER
    JOIN    @sFolder sfo ON 
            fm.FolderId = sfo.FromFolderId
-- return    
SELECT  * 
FROM    @FileIndex fi 
JOIN    @FileFolder ff ON  
        fi.FileId = ff.FileId 
JOIN    @FolderIndex fo ON  
        ff.FolderId = fo.FolderId

【讨论】:

  • 很有趣,但是要实现复制一行和一组子行的简单任务似乎非常复杂......游标实际上不是更容易理解吗?
【解决方案2】:

我想添加另一个示例以添加到 @Nathan 的示例中,因为我发现它有些混乱。

我的大部分使用真实表,而不是临时表。

我的灵感也是从这里得到的:another example

-- Copy the FormSectionInstance
DECLARE @FormSectionInstanceTable TABLE(OldFormSectionInstanceId INT, NewFormSectionInstanceId INT)

;MERGE INTO [dbo].[FormSectionInstance]
USING
(
    SELECT
        fsi.FormSectionInstanceId [OldFormSectionInstanceId]
        , @NewFormHeaderId [NewFormHeaderId]
        , fsi.FormSectionId
        , fsi.IsClone
        , @UserId [NewCreatedByUserId]
        , GETDATE() NewCreatedDate
        , @UserId [NewUpdatedByUserId]
        , GETDATE() NewUpdatedDate
    FROM [dbo].[FormSectionInstance] fsi
    WHERE fsi.[FormHeaderId] = @FormHeaderId 
) tblSource ON 1=0 -- use always false condition
WHEN NOT MATCHED
THEN INSERT
( [FormHeaderId], FormSectionId, IsClone, CreatedByUserId, CreatedDate, UpdatedByUserId, UpdatedDate)
VALUES( [NewFormHeaderId], FormSectionId, IsClone, NewCreatedByUserId, NewCreatedDate, NewUpdatedByUserId, NewUpdatedDate)

OUTPUT tblSource.[OldFormSectionInstanceId], INSERTED.FormSectionInstanceId
INTO @FormSectionInstanceTable(OldFormSectionInstanceId, NewFormSectionInstanceId);


-- Copy the FormDetail
INSERT INTO [dbo].[FormDetail]
    (FormHeaderId, FormFieldId, FormSectionInstanceId, IsOther, Value, CreatedByUserId, CreatedDate, UpdatedByUserId, UpdatedDate)
SELECT
    @NewFormHeaderId, FormFieldId, fsit.NewFormSectionInstanceId, IsOther, Value, @UserId, CreatedDate, @UserId, UpdatedDate
FROM [dbo].[FormDetail] fd
INNER JOIN @FormSectionInstanceTable fsit ON fsit.OldFormSectionInstanceId = fd.FormSectionInstanceId
WHERE [FormHeaderId] = @FormHeaderId

【讨论】:

    【解决方案3】:

    这是一个不使用 MERGE 的解决方案(我曾多次遇到问题,如果可能的话,我会尽量避免)。它依赖于两个内存表(如果需要,您可以使用临时表)与匹配的 IDENTITY 列,重要的是,在执行 INSERT 时使用 ORDER BY,以及在两个 INSERT 之间匹配的 WHERE 条件......第一个持有源 ID,第二个保存目标 ID。

    -- Setup...   We have a table that we need to know the old IDs and new IDs after copying.
    -- We want to copy all of DocID=1
    DECLARE @newDocID int = 99;
    DECLARE @tbl table (RuleID int PRIMARY KEY NOT NULL IDENTITY(1, 1), DocID int, Val varchar(100));
    INSERT INTO @tbl (DocID, Val) VALUES (1, 'RuleA-2'), (1, 'RuleA-1'), (2, 'RuleB-1'), (2, 'RuleB-2'), (3, 'RuleC-1'), (1, 'RuleA-3')
    
    -- Create a break in IDENTITY values.. just to simulate more realistic data
    INSERT INTO @tbl (Val) VALUES ('DeleteMe'), ('DeleteMe');
    DELETE FROM @tbl WHERE Val = 'DeleteMe';
    INSERT INTO @tbl (DocID, Val) VALUES (6, 'RuleE'), (7, 'RuleF');
    
    SELECT * FROM @tbl t;
    
    -- Declare TWO temp tables each with an IDENTITY - one will hold the RuleID of the items we are copying, other will hold the RuleID that we create
    DECLARE @input table (RID int IDENTITY(1, 1), SourceRuleID int NOT NULL, Val varchar(100));
    DECLARE @output table (RID int IDENTITY(1,1), TargetRuleID int NOT NULL, Val varchar(100));
    
    -- Capture the IDs of the rows we will be copying by inserting them into the @input table
    -- Important - we must specify the sort order - best thing is to use the IDENTITY of the source table (t.RuleID) that we are copying
    INSERT INTO @input (SourceRuleID, Val) SELECT t.RuleID, t.Val FROM @tbl t WHERE t.DocID = 1 ORDER BY t.RuleID;
    
    -- Copy the rows, and use the OUTPUT clause to capture the IDs of the inserted rows.
    -- Important - we must use the same WHERE and ORDER BY clauses as above
    INSERT INTO @tbl (DocID, Val)
    OUTPUT Inserted.RuleID, Inserted.Val INTO @output(TargetRuleID, Val)
    SELECT @newDocID, t.Val FROM @tbl t 
    WHERE t.DocID = 1
    ORDER BY t.RuleID;
    
    -- Now @input and @output should have the same # of rows, and the order of both inserts was the same, so the IDENTITY columns (RID) can be matched
    -- Use this as the map from old-to-new when you are copying sub-table rows
    -- Technically, @input and @output don't even need the 'Val' columns, just RID and RuleID - they were included here to prove that the rules matched
    SELECT i.*, o.* FROM @output o
    INNER JOIN @input i ON i.RID = o.RID
    
    -- Confirm the matching worked
    SELECT * FROM @tbl t
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-07
      • 2021-06-15
      • 2020-10-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多