МЕРГЕ підмножину цільової таблиці


71

Я намагаюся використовувати MERGEоператор для вставки або видалення рядків із таблиці, але я хочу лише діяти на підмножині цих рядків. У документації на MERGEдосить чітко сформульоване попередження:

Важливо вказати лише стовпці з цільової таблиці, які використовуються для відповідності. Тобто вкажіть стовпці з цільової таблиці, які порівнюються з відповідним стовпцем вихідної таблиці. Не намагайтеся покращити ефективність запитів, фільтруючи рядки в цільовій таблиці в пункті ON, наприклад, вказавши ТА НЕ target_table.column_x = значення. Це може призвести до несподіваних і неправильних результатів.

але саме це, як видається, я повинен зробити, щоб зробити свою 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 рік порушила це, серед іншого. Це було пов'язано досить добре в документації на R2 2008 року.
Пол Білий

Відповіді:


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пункті застосовується як частина з'єднання. У цьому випадку приєднання - це повне зовнішнє з'єднання (для цього див. Цю книгу 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

План читає лише рядки для категорії 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.