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предложении применяется как часть объединения. В этом случае объединение является полным внешним объединением (см. Эту запись Books Online, чтобы узнать почему). Применение проверки для категории 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Синтаксис (и реализация присоединиться в зависимости от положений , указанных) просто сделать это труднее , чтобы увидеть , что это так.
Руководство в Books Online (вспененное в оптимизации производительности ввода) предлагает рекомендацию , которые обеспечат правильную семантику выражаются с помощью 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;