web-dev-qa-db-fra.com

Utilisation de merge..output pour obtenir le mappage entre source.id et target.id

Très simplifié, j'ai deux tableaux Source et 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')

Je voudrais déplacer toutes les lignes de @Source Vers @Target Et connaître le TargetID pour chaque SourceID car il y a aussi les tables SourceChild et TargetChild qui doit également être copié et je dois ajouter le nouveau TargetID dans la colonne FK TargetChild.TargetID.

Il y a quelques solutions à cela.

  1. Utilisez une boucle while ou des curseurs pour insérer une ligne (RBAR) dans Target à la fois et utilisez scope_identity() pour remplir le FK de TargetChild.
  2. Ajoutez une colonne temporaire à @Target Et insérez SourceID. Vous pouvez ensuite joindre cette colonne pour récupérer le TargetID pour le FK dans TargetChild.
  3. SET IDENTITY_INSERT OFF Pour @Target Et gérez vous-même l'attribution de nouvelles valeurs. Vous obtenez une plage que vous utilisez ensuite dans TargetChild.TargetID.

Je n'aime pas beaucoup d'entre eux. Celui que j'ai utilisé jusqu'à présent est celui des curseurs.

Ce que j'aimerais vraiment faire, c'est utiliser la clause output de l'instruction insert.

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

Mais ce n'est pas possible

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

Mais c'est possible avec une fusion.

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

Résultat

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

Je veux savoir si vous l'avez utilisé? Si vous avez des réflexions sur la solution ou voyez des problèmes avec elle? Cela fonctionne bien dans des scénarios simples, mais peut-être que quelque chose de laid pourrait se produire lorsque le plan de requête devient vraiment compliqué en raison d'une requête source compliquée. Le pire scénario serait que les paires TargetID/SourceID ne correspondent pas.

MSDN a ceci à dire sur le from_table_name De la clause output .

Est un préfixe de colonne qui spécifie une table incluse dans la clause FROM d'une instruction DELETE, UPDATE ou MERGE utilisée pour spécifier les lignes à mettre à jour ou à supprimer.

Pour une raison quelconque, ils ne disent pas "lignes à insérer, mettre à jour ou supprimer" uniquement "lignes à mettre à jour ou supprimer".

Toutes les pensées sont les bienvenues et des solutions totalement différentes au problème d'origine sont très appréciées.

59
Mikael Eriksson

À mon avis, c'est une excellente utilisation de MERGE et de sortie. J'ai utilisé dans plusieurs scénarios et je n'ai pas rencontré de bizarreries à ce jour. Par exemple, voici la configuration de test qui clone un dossier et tous les fichiers (identité) qu'il contient dans un dossier (guid) nouvellement créé.

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
44
Nathan Skerl

Je voudrais ajouter un autre exemple à ajouter à l'exemple de @ Nathan, car je l'ai trouvé un peu déroutant.

La mienne utilise des tables réelles pour la plupart, et non des tables temporaires.

J'ai aussi eu mon inspiration ici: n autre exemple

-- 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
1
Dragos Durlut