it-swarm-eu.dev

MERGE podmnožina cílové tabulky

Snažím se pomocí příkazu MERGE vložit nebo smazat řádky z tabulky, ale chci jen jednat na podmnožině těchto řádků. Dokumentace pro MERGE obsahuje velmi důrazně formulované varování:

Je důležité určit pouze sloupce z cílové tabulky, které se používají pro účely shody. To znamená, určit sloupce z cílové tabulky, které jsou porovnány s odpovídajícím sloupcem zdrojové tabulky. Nepokoušejte se zlepšit výkon dotazu odfiltrováním řádků v cílové tabulce v klauzuli ON, například zadáním AND NOT target_table.column_x = value. Pokud tak učiníte, může dojít k neočekávaným a nesprávným výsledkům.

ale to je přesně to, co se zdá, že musím udělat, aby moje MERGE fungovala.

Data, která mám, jsou standardní spojovací tabulkou mezi mnoha položkami a kategoriemi (např. Které položky jsou zahrnuty do kterých kategorií):

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

Musím účinně nahradit všechny řádky v určité kategorii novým seznamem položek. Můj první pokus o to vypadá takto:

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 ;

To zdá se, že pracuje v mých testech, ale dělám přesně to, co mě MSDN výslovně varuje, abych neudělal. To mě znepokojuje, že se později setkám s neočekávanými problémy, ale nevidím žádný jiný způsob, jak můj MERGE ovlivní pouze řádky se specifickou hodnotou pole (CategoryId = 2) a ignorovat řádky z jiných kategorií.

Existuje „správnější“ způsob, jak dosáhnout stejného výsledku? A jaké jsou „neočekávané nebo nesprávné výsledky“, na které mě MSDN upozorňuje?

73
KutuluMike

Příkaz MERGE má složitou syntaxi a ještě složitější implementaci, ale v zásadě jde o spojení dvou tabulek, filtrování dolů na řádky, které je třeba změnit (vložit, aktualizovat nebo smazat), a poté provést požadované změny. Vzhledem k těmto vzorovým údajům:

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);

Cíl

╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║          1 ║      1 ║
║          2 ║      1 ║
║          1 ║      2 ║
║          1 ║      3 ║
║          2 ║      3 ║
║          3 ║      5 ║
║          4 ║      5 ║
║          3 ║      6 ║
╚════════════╩════════╝

Zdroj

╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║          2 ║      2 ║
╚════════════╩════════╝

Požadovaným výsledkem je nahrazení dat v cíli daty ze zdroje, ale pouze pro CategoryId = 2. Po výše uvedeném popisu MERGE bychom měli napsat dotaz, který spojuje zdroj a cíl pouze na klíče, a filtrovat řádky pouze v klauzulích WHEN:

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
;

To dává následující výsledky:

╔═════════╦════════════╦════════╗
║ $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 ║
╚════════════╩════════╝

Realizační plán je: Merge plan

Všimněte si, že jsou obě tabulky plně naskenovány. Můžeme to považovat za neefektivní, protože pouze řádky, kde CategoryId = 2 bude ovlivněno v cílové tabulce. Zde přicházejí varování v Books Online. Jeden chybný pokus o optimalizaci, aby se dotkl pouze nezbytných řádků v cíli, je:

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
;

Logika v klauzuli ON je použita jako součást spojení. V tomto případě je spojení úplným vnějším spojením (viz tento záznam Books Online proč). Použití kontroly kategorie 2 na cílových řádcích jako součásti vnějšího spojení nakonec povede k odstranění řádků s jinou hodnotou (protože neodpovídají zdroji):

╔═════════╦════════════╦════════╗
║ $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 ║
╚════════════╩════════╝

Kořenová příčina je ze stejného důvodu, proč se predikáty chovají v klauzuli vnější spojení ON odlišně, než když jsou specifikovány v klauzuli WHERE. Syntaxe MERGE (a implementace spojení v závislosti na zadaných klauzulích) jen ztěžují vidět, že tomu tak je.

navádění v Books Online (rozšířeno v položce Optimizing Performance ) nabízí navádění, které zajistí, že správná sémantika je vyjádřena pomocí syntaxe MERGE, aniž by uživatel nutně musel muset pochopit všechny podrobnosti implementace nebo zohlednit způsoby, kterými by optimalizátor mohl legitimně přeskupit věci z důvodů efektivnosti provádění.

Dokumentace nabízí tři možné způsoby implementace časného filtrování:

Zadání podmínky filtrování v klauzuli WHEN zaručuje správné výsledky, ale může znamenat, že více řádků je čteno a zpracováno ze zdroje a cíle tabulky, než je nezbytně nutné (viz první příklad).

Aktualizace prostřednictvím zobrazení , která obsahuje podmínku filtrování, také zaručuje správné výsledky (protože změněné řádky musí být přístupné pro aktualizaci prostřednictvím zobrazení), ale vyžaduje to vyhrazené zobrazení, které splňuje liché podmínky pro aktualizaci zobrazení.

Použití společného výrazu tabulky přináší podobné riziko při přidávání predikátů k klauzuli ON, ale z poněkud odlišných důvodů. V mnoha případech to bude bezpečné, ale vyžaduje to odbornou analýzu prováděcího plánu, aby se to potvrdilo (a rozsáhlé praktické testování). Například:

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
;

Výsledkem jsou správné výsledky (neopakované) s optimálnějším plánem:

Merge plan 2

Plán čte pouze řádky pro kategorii 2 z cílové tabulky. To může být důležitým hlediskem výkonu, pokud je cílová tabulka velká, ale je příliš snadné tuto chybu získat pomocí syntaxe MERGE.

Někdy je snazší zapsat MERGE jako samostatné operace DML. Tento přístup může dokonce vést lépe než jediný MERGE, což je skutečnost, která lidi často překvapuje.

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;
105
Paul White 9