MERGE подмножество целевой таблицы


71

Я пытаюсь использовать MERGEоператор для вставки или удаления строк из таблицы, но я хочу воздействовать только на подмножество этих строк. В документации MERGEесть довольно строго сформулированное предупреждение:

Важно указать только столбцы из целевой таблицы, которые используются для сопоставления. То есть укажите столбцы из целевой таблицы, которые сравниваются с соответствующим столбцом исходной таблицы. Не пытайтесь улучшить производительность запросов, отфильтровывая строки в целевой таблице в предложении ON, например, указав AND NOT target_table.column_x = value. Это может привести к неожиданным и неправильным результатам.

но это именно то, что я должен сделать, чтобы сделать свою MERGEработу.

Данные, которые у меня есть, представляют собой стандартную таблицу «многие ко многим», в которой элементы объединяются в категории (например, какие элементы включены в какие категории) следующим образом:

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

Что мне нужно сделать, так это эффективно заменить все строки в определенной категории новым списком элементов. Моя первая попытка сделать это выглядит так:

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 ;

Это кажется , что работает в моих тестах, но я делаю именно то , что MSDN явно предупреждает меня не делать. Это заставляет меня беспокоиться о том, что позже я столкнусь с неожиданными проблемами, но я не вижу другого способа сделать так, чтобы мои MERGEаффекты вступали только в строки с определенным значением поля ( CategoryId = 2) и игнорировали строки из других категорий.

Есть ли «более правильный» способ добиться такого же результата? И каковы «неожиданные или неправильные результаты», о которых меня предупреждает MSDN?


Да, документация была бы более полезной, если бы в ней был конкретный пример «неожиданных и неправильных результатов».
АК

3
@AlexKuznetsov Здесь есть пример .
Пол Уайт

@SQLKiwi спасибо за ссылку - IMO документация была бы намного лучше, если бы на нее ссылались с оригинальной страницы.
АК

1
@AlexKuznetsov Согласен. К сожалению, реорганизация BOL на 2012 год сломала это, среди прочего. Он был довольно хорошо связан с документацией 2008 R2.
Пол Уайт

Ответы:


103

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

План считывает только строки для категории 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;

Я знаю, что это действительно старый вопрос ... но любой шанс, который вы можете уточнить на "Использование общего табличного выражения, сопряжен с рисками добавления предикатов к предложению ON, но по несколько иным причинам". Я знаю, что у BOL также есть смутное предупреждение: «Этот метод похож на указание дополнительных критериев поиска в предложении ON и может давать неверные результаты. Мы рекомендуем вам избегать использования этого метода ...». Метод CTE, кажется, решает мой вариант использования, однако мне интересно, есть ли сценарий, который я не рассматриваю.
Генри Ли
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.