A MERGE
instrução tem uma sintaxe complexa e uma implementação ainda mais complexa, mas essencialmente a ideia é unir duas tabelas, filtrar para linhas que precisam ser alteradas (inseridas, atualizadas ou excluídas) e, em seguida, executar as alterações solicitadas. Dados os seguintes dados de amostra:
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);
Alvo
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 1 ║ 1 ║
║ 2 ║ 1 ║
║ 1 ║ 2 ║
║ 1 ║ 3 ║
║ 2 ║ 3 ║
║ 3 ║ 5 ║
║ 4 ║ 5 ║
║ 3 ║ 6 ║
╚════════════╩════════╝
Fonte
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 2 ║ 2 ║
╚════════════╩════════╝
O resultado desejado é substituir os dados no destino por dados da origem, mas apenas para CategoryId = 2
. Seguindo a descrição MERGE
fornecida acima, devemos escrever uma consulta que une a origem e o destino apenas nas chaves e filtrar as linhas apenas nas WHEN
cláusulas:
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
;
Isso fornece os seguintes resultados:
╔═════════╦════════════╦════════╗
║ $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 ║
╚════════════╩════════╝
O plano de execução é:
Observe que as duas tabelas são verificadas completamente. Podemos achar isso ineficiente, porque apenas as linhas CategoryId = 2
serão afetadas na tabela de destino. É aqui que entram os avisos no Books Online. Uma tentativa equivocada de otimizar para tocar apenas as linhas necessárias no destino é:
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
;
A lógica na ON
cláusula é aplicada como parte da junção. Nesse caso, a associação é uma associação externa completa (consulte esta entrada do Books Online para saber o porquê). A aplicação da verificação da categoria 2 nas linhas de destino como parte de uma junção externa resulta em linhas com um valor diferente sendo excluído (porque elas não correspondem à origem):
╔═════════╦════════════╦════════╗
║ $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 ║
╚════════════╩════════╝
A causa raiz é o mesmo motivo pelo qual os predicados se comportam de maneira diferente em uma ON
cláusula de junção externa do que se especificado na WHERE
cláusula. A MERGE
sintaxe (e a implementação da junção, dependendo das cláusulas especificadas) apenas tornam mais difícil perceber que isso é verdade.
As orientações nos Manuais Online (expandidas na entrada Otimizando desempenho ) oferecem orientações que garantirão que a semântica correta seja expressa usando a MERGE
sintaxe, sem que o usuário precise necessariamente entender todos os detalhes da implementação ou que explique as maneiras pelas quais o otimizador pode legitimamente reorganizar coisas por razões de eficiência de execução.
A documentação oferece três maneiras possíveis de implementar a filtragem antecipada:
A especificação de uma condição de filtragem na WHEN
cláusula garante resultados corretos, mas pode significar que mais linhas são lidas e processadas a partir das tabelas de origem e destino do que o estritamente necessário (como visto no primeiro exemplo).
A atualização através de uma visualização que contém a condição de filtragem também garante resultados corretos (já que as linhas alteradas devem estar acessíveis para atualização através da visualização), mas isso requer uma visualização dedicada e uma que siga as condições ímpares para atualizar as visualizações.
O uso de uma expressão de tabela comum acarreta riscos semelhantes à adição de predicados à ON
cláusula, mas por razões ligeiramente diferentes. Em muitos casos, será seguro, mas requer análise especializada do plano de execução para confirmar isso (e testes práticos extensivos). Por exemplo:
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
;
Isso produz resultados corretos (não repetidos) com um plano mais ideal:
O plano lê apenas linhas da categoria 2 da tabela de destino. Isso pode ser uma consideração importante de desempenho se a tabela de destino for grande, mas é muito fácil cometer erros usando a MERGE
sintaxe.
Às vezes, é mais fácil gravar as MERGE
operações DML separadas. Essa abordagem pode até ter um desempenho melhor que um único MERGE
, fato que muitas vezes surpreende as pessoas.
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;