Come evitare l'utilizzo della query Unisci quando si esegue l'uperting di più dati utilizzando il parametro xml?


9

Sto cercando di aggiornare una tabella con una matrice di valori. Ogni elemento nella matrice contiene informazioni che corrispondono a una riga in una tabella nel database di SQL Server. Se la riga esiste già nella tabella, aggiorniamo quella riga con le informazioni nell'array specificato. Altrimenti, inseriamo una nuova riga nella tabella. Ho sostanzialmente descritto upsert.

Ora sto provando a raggiungere questo obiettivo in una procedura memorizzata che accetta un parametro XML. Il motivo per cui sto usando XML e non un parametro con valori di tabella è perché, facendo quest'ultimo, dovrò creare un tipo personalizzato in SQL e associare questo tipo alla procedura memorizzata. Se avessi mai cambiato qualcosa nella mia procedura memorizzata o nel mio schema db lungo la strada, avrei dovuto ripetere sia la procedura memorizzata che il tipo personalizzato. Voglio evitare questa situazione. Inoltre, la superiorità che TVP ha su XML non è utile per la mia situazione perché, la dimensione della mia matrice di dati non supererà mai 1000. Ciò significa che non posso usare la soluzione proposta qui: Come inserire più record usando XML in SQL Server 2008

Inoltre, una discussione simile qui ( UPSERT - Esiste un'alternativa migliore a MERGE o @@ rowcount? ) È diversa da quella che sto chiedendo perché, sto provando a caricare più righe su una tabella.

Speravo che avrei semplicemente usato il seguente set di query per confermare i valori dall'xml. Ma questo non funzionerà. Questo approccio dovrebbe funzionare solo quando l'input è una singola riga.

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

La prossima alternativa è quella di utilizzare un ESISTENTE esaustivo o una delle sue varianti nella forma seguente. Ma, lo rifiuto per motivi di efficienza non ottimale:

IF (SELECT COUNT ... ) > 0
    UPDATE
ELSE
    INSERT

L'opzione successiva è stata l'utilizzo dell'istruzione Merge come descritto qui: http://www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.html . Ma poi ho letto dei problemi con la query Unisci qui: http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ . Per questo motivo, sto cercando di evitare Merge.

Quindi, ora la mia domanda è: c'è qualche altra opzione o un modo migliore per ottenere più upsert usando il parametro XML nella procedura memorizzata di SQL Server 2008?

Si noti che i dati nel parametro XML possono contenere alcuni record che non devono essere UPSERTed perché più vecchi del record corrente. Esiste un ModifiedDatecampo sia nell'XML che nella tabella di destinazione che deve essere confrontato per determinare se il record deve essere aggiornato o scartato.


Cercare di evitare di apportare modifiche al proc in futuro non è davvero un buon motivo per non usare un TVP. se i dati passati nelle modifiche finiranno per apportare modifiche al codice in entrambi i modi.
Max Vernon,

1
@MaxVernon All'inizio ho avuto lo stesso pensiero e ho quasi fatto un commento molto simile perché quello da solo non è un motivo per evitare TVP. Ma fanno un po 'più di sforzo, e con l'avvertenza di "mai più di 1000 file" (implicito a volte, o forse anche spesso?) È un po' un gioco di parole. Tuttavia, suppongo che dovrei qualificare la mia risposta per affermare che <1000 righe alla volta non sono molto diverse dall'XML purché non vengano chiamate 10k volte di fila. Quindi le differenze di prestazioni minori si sommano sicuramente.
Solomon Rutzky il

I problemi MERGEche Bertrand evidenzia sono principalmente casi limite e inefficienze, non mostrano ostacoli: la MS non lo avrebbe rilasciato se fosse stato un vero campo minato. Sei sicuro che le convoluzioni che stai attraversando per evitare MERGEnon stanno creando più potenziali errori di quelli che stanno salvando?
Jon of All Trades,

@JonofAllTrades Ad essere sinceri, ciò che ho proposto non è davvero così contorto rispetto a MERGE. Le fasi INSERT e UPDATE di MERGE vengono comunque elaborate separatamente. La differenza principale nel mio approccio è la variabile di tabella che contiene gli ID record aggiornati e la query DELETE che utilizza quella variabile di tabella per rimuovere quei record dalla tabella temporanea dei dati in arrivo. E suppongo che SOURCE possa essere diretto da @ XMLparam.nodes () invece di scaricare su una tabella temporanea, ma non è un sacco di roba extra di cui non doversi preoccupare di ritrovarsi in uno di quei casi limite; - ).
Solomon Rutzky,

Risposte:


11

Che la fonte sia XML o TVP non fa una grande differenza. L'operazione complessiva è essenzialmente:

  1. AGGIORNA righe esistenti
  2. INSERISCI righe mancanti

Lo fai in questo ordine perché se INSERISCI per primo, allora tutte le righe esistono per ottenere l'AGGIORNAMENTO e farai un lavoro ripetuto per tutte le righe che sono state appena inserite.

Oltre a ciò, esistono diversi modi per ottenere questo risultato e vari modi per ottimizzarne l'efficienza aggiuntiva.

Cominciamo con il minimo indispensabile. Poiché l'estrazione dell'XML è probabilmente una delle parti più costose di questa operazione (se non la più costosa), non vogliamo farlo due volte (poiché abbiamo due operazioni da eseguire). Quindi, creiamo una tabella temporanea ed estraiamo i dati dall'XML in essa:

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);

Da lì facciamo l'aggiornamento e poi l'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
                     );

Ora che abbiamo eseguito l'operazione di base, possiamo fare alcune cose per ottimizzare:

  1. acquisire @@ ROWCOUNT di inserire nella tabella temporanea e confrontarlo con @@ ROWCOUNT dell'aggiornamento. Se sono uguali, possiamo saltare INSERT

  2. acquisire i valori ID aggiornati tramite la clausola OUTPUT ed ELIMINARE quelli dalla tabella temporanea. Quindi INSERT non ha bisogno diWHERE NOT EXISTS(...)

  3. SE ci sono righe nei dati in arrivo che non devono essere sincronizzate (ovvero né inserite né aggiornate), tali record devono essere rimossi prima di eseguire l'aggiornamento

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;

Ho usato questo modello più volte su Import / ETL che hanno oltre 1000 righe o forse 500 in un batch su un set totale di 20k - oltre un milione di righe. Tuttavia, non ho testato la differenza di prestazioni tra il DELETE delle righe aggiornate dalla tabella temporanea e il semplice aggiornamento del campo [IsUpdate].


Si prega di notare per quanto riguarda la decisione di utilizzare XML su TVP a causa della possibilità di importare al massimo 1000 righe alla volta (menzionate nella domanda):

Se questo viene chiamato alcune volte qua e là, molto probabilmente il minor guadagno in termini di prestazioni in TVP potrebbe non valere il costo di manutenzione aggiuntivo (è necessario abbandonare il proc prima di modificare il tipo di tabella definito dall'utente, le modifiche al codice dell'app, ecc.) . Ma se stai importando 4 milioni di righe, inviandone 1000 alla volta, ovvero 4000 esecuzioni (e 4 milioni di righe di XML da analizzare, indipendentemente dal modo in cui è suddiviso), e anche una differenza di prestazioni minore se eseguita solo poche volte aggiungere fino a una notevole differenza.

Detto questo, il metodo come ho descritto non cambia al di fuori della sostituzione di SELECT FROM @XmlInputParam in SELEZIONA DA @TVP. Poiché i TVP sono di sola lettura, non potresti eliminarli. Immagino che potresti semplicemente aggiungere un WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)a quel SELECT finale (legato all'insert) invece del semplice WHERE IsUpdate = 0. Se dovessi utilizzare la @UpdateIDsvariabile della tabella in questo modo, potresti persino evitare di scaricare le righe in entrata nella tabella temporanea.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.