MERGE un sottoinsieme della tabella di destinazione


71

Sto cercando di utilizzare MERGEun'istruzione per inserire o eliminare righe da una tabella, ma voglio solo agire su un sottoinsieme di quelle righe. La documentazione per MERGEha un avvertimento piuttosto forte:

È importante specificare solo le colonne della tabella di destinazione che vengono utilizzate per scopi corrispondenti. Cioè, specificare le colonne dalla tabella di destinazione che vengono confrontate con la colonna corrispondente della tabella di origine. Non tentare di migliorare le prestazioni della query filtrando le righe nella tabella di destinazione nella clausola ON, ad esempio specificando AND NOT target_table.column_x = value. Ciò potrebbe restituire risultati imprevisti e non corretti.

ma questo è esattamente ciò che sembra che devo fare per fare il mio MERGElavoro.

I dati che ho sono una tabella di join standard molti-a-molti di elementi a categorie (ad esempio quali elementi sono inclusi in quali categorie) in questo modo:

CategoryId   ItemId
==========   ======
1            1
1            2
1            3
2            1
2            3
3            5
3            6
4            5

Quello che devo fare è sostituire efficacemente tutte le righe in una categoria specifica con un nuovo elenco di elementi. Il mio tentativo iniziale di farlo è simile al seguente:

MERGE INTO CategoryItem AS TARGET
USING (
  SELECT ItemId FROM SomeExternalDataSource WHERE CategoryId = 2
) AS SOURCE
ON SOURCE.ItemId = TARGET.ItemId AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
    INSERT ( CategoryId, ItemId )
    VALUES ( 2, ItemId )
WHEN NOT MATCHED BY SOURCE AND TARGET.CategoryId = 2 THEN
    DELETE ;

Questo sembra funzionare nei miei test, ma sto facendo esattamente ciò che MSDN mi avvisa esplicitamente di non fare. Questo mi preoccupa che in seguito incontrerò problemi imprevisti, ma non riesco a vedere nessun altro modo per MERGEinfluire solo sulle righe con il valore del campo specifico ( CategoryId = 2) e ignorare le righe di altre categorie.

Esiste un modo "più corretto" per ottenere lo stesso risultato? E quali sono i "risultati imprevisti o errati" che MSDN mi sta avvisando?


Sì, la documentazione sarebbe più utile se avesse un esempio concreto di "risultati imprevisti e non corretti".
AK,

3
@AlexKuznetsov C'è un esempio qui .
Paul White

@SQLKiwi grazie per il link - IMO la documentazione sarebbe molto meglio se fosse riferita dalla pagina originale.
AK,

1
@AlexKuznetsov Concordato. Sfortunatamente, la riorganizzazione della BOL per il 2012 ha rotto questo, tra le altre cose. È stato collegato abbastanza bene nella documentazione del 2008 R2.
Paul White

Risposte:


103

L' MERGEistruzione 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 MERGEcui sopra, dovremmo scrivere una query che unisce l'origine e la destinazione solo sulle chiavi e filtra solo le righe nelle WHENclausole:

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 è: Unisci piano

Si noti che entrambe le tabelle sono state digitalizzate completamente. Potremmo ritenerlo inefficiente, perché solo le righe in cui CategoryId = 2saranno 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 ONclausola 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 ONclausola di join esterna rispetto a quanto fanno se specificati nella WHEREclausola. La MERGEsintassi (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 MERGEsintassi, 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 WHENclausola 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 ONclausola, 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:

Unisci piano 2

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 MERGEsintassi.

A volte, è più facile scrivere MERGEcome 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;

So che questa è una domanda davvero vecchia ... ma ogni possibilità che puoi approfondire su "L'uso di un'espressione di tabella comune comporta rischi simili all'aggiunta di predicati alla clausola ON, ma per motivi leggermente diversi." So che BOL ha anche un avviso altrettanto vago "Questo metodo è simile alla specifica di criteri di ricerca aggiuntivi nella clausola ON e può produrre risultati errati. Ti consigliamo di evitare di utilizzare questo metodo ...". Il metodo CTE sembra risolvere il mio caso d'uso, tuttavia mi chiedo se c'è uno scenario che non sto prendendo in considerazione.
Henry Lee,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.