Como evitar o uso de consulta de mesclagem ao converter vários dados usando o parâmetro xml?

9

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 ModifiedDatecampo no XML e na tabela de destino que precisa ser comparado para determinar se o registro deve ser atualizado ou descartado.

GMalla
fonte
Tentar evitar fazer alterações no processo no futuro não é realmente um bom motivo para não usar um TVP. se os dados transmitidos forem alterados, você fará alterações no código de qualquer maneira.
Max Vernon
11
@ MaxVernon Eu tive o mesmo pensamento no começo e quase fiz um comentário muito semelhante, porque isso por si só não é um motivo para evitar o TVP. Mas eles exigem um pouco mais de esforço e, com a ressalva de "nunca mais de 1000 linhas" (implícita às vezes, ou talvez até com frequência?), É um pouco de uma brincadeira. No entanto, suponho que devo qualificar minha resposta para afirmar que <1000 linhas por vez não é muito diferente do XML, desde que não seja chamado 10 mil vezes seguidas. Então, pequenas diferenças de desempenho certamente aumentam.
Solomon Rutzky
Os problemas com os MERGEquais 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 evitar MERGEnão estão criando mais erros em potencial do que estão salvando?
Jon of All Trades
@ JonofAllTrades Para ser justo, o que eu propus não é realmente tão complicado assim 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; - )
Solomon Rutzky

Respostas:

11

Se a fonte é XML ou TVP, não faz uma diferença enorme. A operação geral é essencialmente:

  1. ATUALIZAR linhas existentes
  2. INSERIR linhas ausentes

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:

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

INSERT INTO #TempImport (Field1, Field2, ...)
  SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
         tab.col.value('XQueryForField2', 'DataType') AS [Field2],
         ...
  FROM   @XmlInputParam.nodes('XQuery') tab(col);

A partir daí, fazemos o UPDATE e depois o INSERT:

UPDATE tab
SET    tab.Field1 = tmp.Field1,
       tab.Field2 = tmp.Field2,
       ...
FROM   [SchemaName].[TableName] tab
INNER JOIN #TempImport tmp
        ON tmp.IDField = tab.IDField
        ... -- more fields if PK or alternate key is composite

INSERT INTO [SchemaName].[TableName]
  (Field1, Field2, ...)
  SELECT tmp.Field1, tmp.Field2, ...
  FROM   #TempImport tmp
  WHERE  NOT EXISTS (
                       SELECT  *
                       FROM    [SchemaName].[TableName] tab
                       WHERE   tab.IDField = tmp.IDField
                       ... -- more fields if PK or alternate key is composite
                     );

Agora que temos a operação básica desativada, podemos fazer algumas coisas para otimizar:

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

  2. 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 doWHERE NOT EXISTS(...)

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

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

DECLARE @ImportRows INT;
DECLARE @UpdatedIDs TABLE ([IDField] INT NOT NULL);

BEGIN TRY

  INSERT INTO #TempImport (Field1, Field2, ...)
    SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
           tab.col.value('XQueryForField2', 'DataType') AS [Field2],
           ...
    FROM   @XmlInputParam.nodes('XQuery') tab(col);

  SET @ImportRows = @@ROWCOUNT;

  IF (@ImportRows = 0)
  BEGIN
    RAISERROR('Seriously?', 16, 1); -- no rows to import
  END;

  -- optional: test to see if it helps or hurts
  -- ALTER TABLE #TempImport
  --   ADD CONSTRAINT [PK_#TempImport]
  --   PRIMARY KEY CLUSTERED (PKField ASC)
  --   WITH FILLFACTOR = 100;


  -- optional: remove any records that should not be synced
  DELETE tmp
  FROM   #TempImport tmp
  INNER JOIN [SchemaName].[TableName] tab
          ON tab.IDField = tmp.IDField
          ... -- more fields if PK or alternate key is composite
  WHERE  tmp.ModifiedDate < tab.ModifiedDate;

  BEGIN TRAN;

  UPDATE tab
  SET    tab.Field1 = tmp.Field1,
         tab.Field2 = tmp.Field2,
         ...
  OUTPUT INSERTED.IDField
  INTO   @UpdatedIDs ([IDField]) -- capture IDs that are updated
  FROM   [SchemaName].[TableName] tab
  INNER JOIN #TempImport tmp
          ON tmp.IDField = tab.IDField
          ... -- more fields if PK or alternate key is composite

  IF (@@ROWCOUNT < @ImportRows) -- if all rows were updates then skip, else insert remaining
  BEGIN
    -- get rid of rows that were updates, leaving only the ones to insert
    DELETE tmp
    FROM   #TempImport tmp
    INNER JOIN @UpdatedIDs del
            ON del.[IDField] = tmp.[IDField];

    -- OR, rather than the DELETE, maybe add a column to #TempImport for:
    -- [IsUpdate] BIT NOT NULL DEFAULT (0)
    -- Then UPDATE #TempImport SET [IsUpdate] = 1 JOIN @UpdatedIDs ON [IDField]
    -- Then, in below INSERT, add:  WHERE [IsUpdate] = 0

    INSERT INTO [SchemaName].[TableName]
      (Field1, Field2, ...)
      SELECT tmp.Field1, tmp.Field2, ...
      FROM   #TempImport tmp
  END;

  COMMIT TRAN;

END TRY
BEGIN CATCH
  IF (@@TRANCOUNT > 0)
  BEGIN
    ROLLBACK;
  END;

  -- THROW; -- if using SQL 2012 or newer, use this and remove the following 3 lines
  DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
  RAISERROR(@ErrorMessage, 16, 1);
  RETURN;
END CATCH;

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 simples WHERE IsUpdate = 0. Se você usasse a @UpdateIDsvariável da tabela dessa maneira, poderia até não despejar as linhas de entrada na tabela temporária.

Solomon Rutzky
fonte