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;