Estou tentando atualizar uma tabela com uma matriz de valores. Cada item da matriz contém informações que correspondem a uma linha em uma tabela no banco de dados do SQL Server. Se a linha já existir na tabela, atualizamos essa linha com as informações na matriz especificada. Senão, inserimos uma nova linha na tabela. Eu descrevi basicamente upsert.
Agora, estou tentando fazer isso em um procedimento armazenado que usa um parâmetro XML. O motivo pelo qual estou usando XML e não o parâmetro com valor de tabela é porque, ao fazer isso, terei que criar um tipo personalizado no SQL e associá-lo ao procedimento armazenado. Se alguma vez eu alterasse algo no meu procedimento armazenado ou no meu esquema db no futuro, precisaria refazer o procedimento armazenado e o tipo personalizado. Eu quero evitar essa situação. Além disso, a superioridade que o TVP possui sobre XML não é útil para minha situação, porque o tamanho do meu array de dados nunca excederá 1000. Isso significa que não posso usar a solução proposta aqui: Como inserir vários registros usando XML no SQL Server 2008
Além disso, uma discussão semelhante aqui ( UPSERT - Existe uma alternativa melhor para MERGE ou @@ rowcount? ) É diferente do que estou perguntando, porque estou tentando alterar várias linhas para uma tabela.
Eu esperava que simplesmente usasse o seguinte conjunto de consultas para alterar os valores do xml. Mas isso não vai funcionar. Essa abordagem deve funcionar quando a entrada for uma única linha.
begin tran
update table with (serializable) set select * from xml_param
where key = @key
if @@rowcount = 0
begin
insert table (key, ...) values (@key,..)
end
commit tran
A próxima alternativa é usar um IF EXISTENTE exaustivo ou uma de suas variações da seguinte forma. Mas, rejeito isso por ter uma eficiência abaixo do ideal:
IF (SELECT COUNT ... ) > 0
UPDATE
ELSE
INSERT
A próxima opção foi usar a instrução Merge, conforme descrito aqui: http://www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.html . Mas, então, li sobre problemas com a consulta Merge aqui: http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ . Por esse motivo, estou tentando evitar a mesclagem.
Então, agora minha pergunta é: existe alguma outra opção ou uma maneira melhor de obter vários upsert usando o parâmetro XML no procedimento armazenado do SQL Server 2008?
Observe que os dados no parâmetro XML podem conter alguns registros que não devem ser UPSERTed devido a serem mais antigos que o registro atual. Há um ModifiedDate
campo no XML e na tabela de destino que precisa ser comparado para determinar se o registro deve ser atualizado ou descartado.
fonte
MERGE
quais Bertrand aponta são principalmente casos extremos e ineficiências, não mostram rolhas - a MS não a teria liberado se fosse um campo minado real. Tem certeza de que as convulsões que você está passando para evitarMERGE
não estão criando mais erros em potencial do que estão salvando?MERGE
. As etapas INSERT e UPDATE do MERGE ainda são processadas separadamente. A principal diferença em minha abordagem é a variável de tabela que contém os IDs de registro atualizados e a consulta DELETE que usa essa variável de tabela para remover esses registros da tabela temporária dos dados recebidos. E suponho que a SOURCE possa ser direta de @ XMLparam.nodes () em vez de despejar em uma tabela temporária, mas ainda assim, isso não é muita coisa extra para você não ter que se preocupar em encontrar-se em um desses casos extremos; - )Respostas:
Se a fonte é XML ou TVP, não faz uma diferença enorme. A operação geral é essencialmente:
Você faz isso nessa ordem porque, se você INSERIR primeiro, todas as linhas existirão para obter o UPDATE e você fará um trabalho repetido para todas as linhas que foram inseridas.
Além disso, existem diferentes maneiras de conseguir isso e várias maneiras de ajustar alguma eficiência adicional.
Vamos começar com o mínimo. Como a extração do XML provavelmente é uma das partes mais caras desta operação (se não a mais cara), não queremos fazer isso duas vezes (pois temos duas operações para executar). Portanto, criamos uma tabela temporária e extraímos os dados do XML para ela:
A partir daí, fazemos o UPDATE e depois o INSERT:
Agora que temos a operação básica desativada, podemos fazer algumas coisas para otimizar:
capture @@ ROWCOUNT da inserção na tabela temporária e compare com @@ ROWCOUNT da UPDATE. Se eles são iguais, então podemos pular o INSERT
capture os valores de ID atualizados por meio da cláusula OUTPUT e DELETE aqueles da tabela temporária. Então o INSERT não precisa do
WHERE NOT EXISTS(...)
Se houver alguma linha nos dados recebidos que não deva ser sincronizada (ou seja, nem inserida nem atualizada), esses registros deverão ser removidos antes de fazer o UPDATE
Eu usei esse modelo várias vezes em Imports / ETLs que possuem bem mais de 1000 linhas ou talvez 500 em um lote de um conjunto total de 20k - mais de um milhão de linhas. No entanto, não testei a diferença de desempenho entre o DELETE das linhas atualizadas fora da tabela temporária e apenas a atualização do campo [IsUpdate].
Observe a decisão de usar XML sobre TVP, devido à existência de, no máximo, 1000 linhas para importar por vez (mencionado na pergunta):
Se isso for chamado algumas vezes aqui e ali, é bem provável que o menor ganho de desempenho no TVP não valha o custo de manutenção adicional (é necessário interromper o processo antes de alterar o tipo de tabela definido pelo usuário, alterações no código do aplicativo etc.) . Mas se você estiver importando 4 milhões de linhas, enviando 1000 por vez, ou seja, 4.000 execuções (e 4 milhões de linhas de XML para analisar, independentemente de como estão divididas), e mesmo uma pequena diferença de desempenho quando executada apenas algumas vezes somam uma diferença notável.
Dito isto, o método que descrevi não muda fora da substituição do SELECT FROM @XmlInputParam para ser SELECT FROM @TVP. Como os TVPs são somente leitura, você não poderá excluir deles. Eu acho que você poderia simplesmente adicionar um
WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)
ao SELECT final (vinculado ao INSERT) em vez do simplesWHERE IsUpdate = 0
. Se você usasse a@UpdateIDs
variável da tabela dessa maneira, poderia até não despejar as linhas de entrada na tabela temporária.fonte