MERGE
Оператор має складний синтаксис і ще більш складна реалізацію, але по суті ідея полягає в тому, щоб з'єднати дві таблиці, відфільтрувати до рядків , які повинні бути змінені (вставлено, оновлено або видалено), а потім виконати необхідні зміни. З огляду на наступні вибіркові дані:
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);
Ціль
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 1 ║ 1 ║
║ 2 ║ 1 ║
║ 1 ║ 2 ║
║ 1 ║ 3 ║
║ 2 ║ 3 ║
║ 3 ║ 5 ║
║ 4 ║ 5 ║
║ 3 ║ 6 ║
╚════════════╩════════╝
Джерело
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 2 ║ 2 ║
╚════════════╩════════╝
Бажаний результат - замінити дані в цілі на дані з джерела, але лише для CategoryId = 2
. Виходячи з MERGE
наведеного вище опису , ми повинні написати запит, який приєднується до джерела та цілі лише по клавішах і фільтрує рядки лише в 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
;
Це дає такі результати:
╔═════════╦════════════╦════════╗
║ $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 ║
╚════════════╩════════╝
План виконання:
Зверніть увагу, що обидві таблиці скануються повністю. Ми можемо вважати це неефективним, оскільки CategoryId = 2
в цільовій таблиці впливатимуть лише рядки . Ось тут надходять попередження у Books Online. Одна помилкова спроба оптимізувати торкання лише потрібних рядків у цілі:
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
;
Логіка в ON
пункті застосовується як частина з'єднання. У цьому випадку приєднання - це повне зовнішнє з'єднання (для цього див. Цю книгу Online Books ). Застосування чека на категорію 2 на цільових рядках як частина зовнішнього з'єднання в кінцевому підсумку призводить до видалення рядків з іншим значенням (оскільки вони не відповідають джерелу):
╔═════════╦════════════╦════════╗
║ $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 ║
╚════════════╩════════╝
Першопричина - це та сама причина, що предикати поводяться по-іншому в ON
застереженні про зовнішнє з'єднання, ніж вони, якщо зазначено в WHERE
пункті. MERGE
Синтаксис (і реалізація приєднатися в залежності від положень , зазначених) просто зробити це важче , щоб побачити , що це так.
Посібник у Книгах онлайн (розгорнутий у статті оптимізації продуктивності ) пропонує вказівки, які забезпечують правильну семантичну вираженість за допомогою MERGE
синтаксису, без необхідності користувачеві розуміти всі деталі реалізації або враховувати способи, за допомогою яких оптимізатор може правомірно переставити речі з міркувань ефективності виконання.
Документація пропонує три потенційні способи здійснення ранньої фільтрації:
Вказівка умови фільтрації у WHEN
пункті гарантує правильні результати, але може означати, що з рядків та цільових таблиць читається та обробляється більше рядків, ніж це суворо необхідно (як це показано в першому прикладі).
Оновлення за допомогою представлення, яке містить умову фільтрування, також гарантує правильні результати (оскільки змінені рядки повинні бути доступні для оновлення через перегляд), але для цього потрібен спеціальний перегляд і такий, який дотримується непарних умов для оновлення подань.
Використання загального виразу таблиці несе аналогічні ризики додавання предикатів до ON
пункту, але з дещо інших причин. У багатьох випадках це буде безпечно, але для підтвердження цього (і широкого практичного тестування) потрібен експертний аналіз плану виконання. Наприклад:
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
;
Це дає правильні результати (не повторюються) з більш оптимальним планом:
План читає лише рядки для категорії 2 з цільової таблиці. Це може бути важливим питанням ефективності, якщо цільова таблиця велика, але все це занадто просто, щоб помилитися з використанням MERGE
синтаксису.
Іноді простіше записати MERGE
окремі операції DML. Цей підхід може бути навіть кращим, ніж один MERGE
, факт, який часто дивує людей.
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;