L' MERGE
istruzione ha una sintassi complessa e un'implementazione ancora più complessa, ma essenzialmente l'idea è quella di unire due tabelle, filtrare fino alle righe che devono essere modificate (inserite, aggiornate o eliminate) e quindi eseguire le modifiche richieste. Dati i seguenti dati di esempio:
DECLARE @CategoryItem AS TABLE
(
CategoryId integer NOT NULL,
ItemId integer NOT NULL,
PRIMARY KEY (CategoryId, ItemId),
UNIQUE (ItemId, CategoryId)
);
DECLARE @DataSource AS TABLE
(
CategoryId integer NOT NULL,
ItemId integer NOT NULL
PRIMARY KEY (CategoryId, ItemId)
);
INSERT @CategoryItem
(CategoryId, ItemId)
VALUES
(1, 1),
(1, 2),
(1, 3),
(2, 1),
(2, 3),
(3, 5),
(3, 6),
(4, 5);
INSERT @DataSource
(CategoryId, ItemId)
VALUES
(2, 2);
Bersaglio
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 1 ║ 1 ║
║ 2 ║ 1 ║
║ 1 ║ 2 ║
║ 1 ║ 3 ║
║ 2 ║ 3 ║
║ 3 ║ 5 ║
║ 4 ║ 5 ║
║ 3 ║ 6 ║
╚════════════╩════════╝
fonte
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 2 ║ 2 ║
╚════════════╩════════╝
Il risultato desiderato è sostituire i dati nella destinazione con i dati dall'origine, ma solo per CategoryId = 2
. Seguendo la descrizione di MERGE
cui sopra, dovremmo scrivere una query che unisce l'origine e la destinazione solo sulle chiavi e filtra solo le righe nelle WHEN
clausole:
MERGE INTO @CategoryItem AS TARGET
USING @DataSource AS SOURCE ON
SOURCE.ItemId = TARGET.ItemId
AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY SOURCE
AND TARGET.CategoryId = 2
THEN DELETE
WHEN NOT MATCHED BY TARGET
AND SOURCE.CategoryId = 2
THEN INSERT (CategoryId, ItemId)
VALUES (CategoryId, ItemId)
OUTPUT
$ACTION,
ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;
Questo dà i seguenti risultati:
╔═════════╦════════════╦════════╗
║ $ACTION ║ CategoryId ║ ItemId ║
╠═════════╬════════════╬════════╣
║ DELETE ║ 2 ║ 1 ║
║ INSERT ║ 2 ║ 2 ║
║ DELETE ║ 2 ║ 3 ║
╚═════════╩════════════╩════════╝
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 1 ║ 1 ║
║ 1 ║ 2 ║
║ 1 ║ 3 ║
║ 2 ║ 2 ║
║ 3 ║ 5 ║
║ 3 ║ 6 ║
║ 4 ║ 5 ║
╚════════════╩════════╝
Il piano di esecuzione è:
Si noti che entrambe le tabelle sono state digitalizzate completamente. Potremmo ritenerlo inefficiente, perché solo le righe in cui CategoryId = 2
saranno interessate nella tabella di destinazione. È qui che entrano in gioco gli avvisi di Books Online. Un tentativo errato di ottimizzazione per toccare solo le righe necessarie nel target è:
MERGE INTO @CategoryItem AS TARGET
USING
(
SELECT CategoryId, ItemId
FROM @DataSource AS ds
WHERE CategoryId = 2
) AS SOURCE ON
SOURCE.ItemId = TARGET.ItemId
AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
INSERT (CategoryId, ItemId)
VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
DELETE
OUTPUT
$ACTION,
ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;
La logica nella ON
clausola viene applicata come parte del join. In questo caso, il join è un join esterno completo ( per questo motivo, consultare questa voce della documentazione online ). L'applicazione del controllo per la categoria 2 sulle righe di destinazione come parte di un join esterno alla fine comporta la cancellazione di righe con un valore diverso (poiché non corrispondono alla sorgente):
╔═════════╦════════════╦════════╗
║ $ACTION ║ CategoryId ║ ItemId ║
╠═════════╬════════════╬════════╣
║ DELETE ║ 1 ║ 1 ║
║ DELETE ║ 1 ║ 2 ║
║ DELETE ║ 1 ║ 3 ║
║ DELETE ║ 2 ║ 1 ║
║ INSERT ║ 2 ║ 2 ║
║ DELETE ║ 2 ║ 3 ║
║ DELETE ║ 3 ║ 5 ║
║ DELETE ║ 3 ║ 6 ║
║ DELETE ║ 4 ║ 5 ║
╚═════════╩════════════╩════════╝
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 2 ║ 2 ║
╚════════════╩════════╝
La causa principale è la stessa ragione per cui i predicati si comportano in modo diverso in una ON
clausola di join esterna rispetto a quanto fanno se specificati nella WHERE
clausola. La MERGE
sintassi (e l'implementazione del join in base alle clausole specificate) rendono più difficile vedere che è così.
La guida nella documentazione online (ampliata nella voce Ottimizzazione delle prestazioni ) offre una guida che garantirà che la semantica corretta sia espressa utilizzando la MERGE
sintassi, senza che l'utente debba necessariamente comprendere tutti i dettagli dell'implementazione o tenere conto dei modi in cui l'ottimizzatore potrebbe riorganizzare legittimamente cose per motivi di efficienza di esecuzione.
La documentazione offre tre potenziali modi per implementare il filtro anticipato:
Specificare una condizione di filtro nella WHEN
clausola garantisce risultati corretti, ma può significare che vengono lette ed elaborate più righe dalle tabelle di origine e di destinazione di quanto sia strettamente necessario (come visto nel primo esempio).
L'aggiornamento tramite una vista che contiene la condizione di filtro garantisce anche risultati corretti (poiché le righe modificate devono essere accessibili per l'aggiornamento tramite la vista) ma ciò richiede una vista dedicata e una che segua le condizioni dispari per l'aggiornamento delle viste.
L'uso di un'espressione di tabella comune comporta rischi simili all'aggiunta di predicati alla ON
clausola, ma per motivi leggermente diversi. In molti casi sarà sicuro, ma richiede un'analisi approfondita del piano di esecuzione per confermarlo (e test pratici approfonditi). Per esempio:
WITH TARGET AS
(
SELECT *
FROM @CategoryItem
WHERE CategoryId = 2
)
MERGE INTO TARGET
USING
(
SELECT CategoryId, ItemId
FROM @DataSource
WHERE CategoryId = 2
) AS SOURCE ON
SOURCE.ItemId = TARGET.ItemId
AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY TARGET THEN
INSERT (CategoryId, ItemId)
VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
DELETE
OUTPUT
$ACTION,
ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;
Questo produce risultati corretti (non ripetuti) con un piano più ottimale:
Il piano legge solo le righe per la categoria 2 dalla tabella di destinazione. Questa potrebbe essere una considerazione importante delle prestazioni se la tabella di destinazione è grande, ma è fin troppo facile sbagliare usando la MERGE
sintassi.
A volte, è più facile scrivere MERGE
come operazioni DML separate. Questo approccio può persino funzionare meglio di un singolo MERGE
, un fatto che spesso sorprende le persone.
DELETE ci
FROM @CategoryItem AS ci
WHERE ci.CategoryId = 2
AND NOT EXISTS
(
SELECT 1
FROM @DataSource AS ds
WHERE
ds.ItemId = ci.ItemId
AND ds.CategoryId = ci.CategoryId
);
INSERT @CategoryItem
SELECT
ds.CategoryId,
ds.ItemId
FROM @DataSource AS ds
WHERE
ds.CategoryId = 2;