MERGE um subconjunto da tabela de destino


71

Estou tentando usar uma MERGEinstrução para inserir ou excluir linhas de uma tabela, mas só quero atuar em um subconjunto dessas linhas. A documentação para MERGEpossui um aviso muito fortemente redigido:

É importante especificar apenas as colunas da tabela de destino que são usadas para fins de correspondência. Ou seja, especifique colunas da tabela de destino comparadas à coluna correspondente da tabela de origem. Não tente melhorar o desempenho da consulta filtrando as linhas na tabela de destino na cláusula ON, como especificando AND NOT target_table.column_x = value. Fazer isso pode retornar resultados inesperados e incorretos.

mas é exatamente isso que parece que tenho que fazer para fazer meu MERGEtrabalho.

Os dados que tenho são uma tabela de junção muitos-para-muitos padrão de itens para categorias (por exemplo, quais itens estão incluídos em quais categorias), assim:

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

O que preciso fazer é substituir efetivamente todas as linhas de uma categoria específica por uma nova lista de itens. Minha tentativa inicial de fazer isso é assim:

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 ;

Isso parece estar funcionando nos meus testes, mas estou fazendo exatamente o que o MSDN me adverte explicitamente para não fazer. Isso me preocupa com o fato de ter problemas inesperados mais tarde, mas não vejo outra maneira de fazer com que minhas MERGEúnicas linhas afetem o valor do campo específico ( CategoryId = 2) e ignore as linhas de outras categorias.

Existe uma maneira "mais correta" de alcançar esse mesmo resultado? E quais são os "resultados inesperados ou incorretos" sobre os quais o MSDN está me alertando?


Sim, a documentação seria mais útil se tivesse um exemplo concreto de "resultados inesperados e incorretos".
AK

3
@AlexKuznetsov Há um exemplo aqui .
Paul White

@SQLKiwi obrigado pelo link - IMO a documentação seria muito melhor se fosse consultada na página original.
AK

11
@AlexKuznetsov Concordou. Infelizmente, a reorganização da BOL para 2012 quebrou isso, entre muitas outras coisas. Ele foi vinculado bastante bem na documentação do R2 2008.
Paul White

Respostas:


103

A MERGEinstruçã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 MERGEfornecida acima, devemos escrever uma consulta que une a origem e o destino apenas nas chaves e filtrar as linhas apenas nas WHENclá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 é: Plano de mesclagem

Observe que as duas tabelas são verificadas completamente. Podemos achar isso ineficiente, porque apenas as linhas CategoryId = 2serã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 ONclá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 ONcláusula de junção externa do que se especificado na WHEREcláusula. A MERGEsintaxe (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 MERGEsintaxe, 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 WHENclá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 à ONclá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:

Mesclar plano 2

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 MERGEsintaxe.

Às vezes, é mais fácil gravar as MERGEoperaçõ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;

Sei que essa é uma pergunta muito antiga ... mas qualquer chance que você possa elaborar sobre "Usar uma expressão de tabela comum acarreta riscos semelhantes à adição de predicados à cláusula ON, mas por razões ligeiramente diferentes". Sei que a BOL também possui um aviso igualmente vago "Esse método é semelhante à especificação de critérios de pesquisa adicionais na cláusula ON e pode produzir resultados incorretos. Recomendamos que você evite usar esse método ...". O método CTE parece resolver meu caso de uso, no entanto, estou me perguntando se há um cenário que não estou considerando.
Henry Lee
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.