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?
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:
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:
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;