Wie vermeide ich die Verwendung der Zusammenführungsabfrage, wenn mehrere Daten mithilfe des XML-Parameters aktualisiert werden?

9

Ich versuche, eine Tabelle mit einem Array von Werten zu aktualisieren. Jedes Element im Array enthält Informationen, die mit einer Zeile in einer Tabelle in der SQL Server-Datenbank übereinstimmen. Wenn die Zeile bereits in der Tabelle vorhanden ist, aktualisieren wir diese Zeile mit den Informationen im angegebenen Array. Andernfalls fügen wir eine neue Zeile in die Tabelle ein. Ich habe im Grunde Upsert beschrieben.

Jetzt versuche ich dies in einer gespeicherten Prozedur zu erreichen, die einen XML-Parameter verwendet. Der Grund, warum ich XML und keine tabellenwertigen Parameter verwende, ist, dass ich in letzterem Fall einen benutzerdefinierten Typ in SQL erstellen und diesen Typ der gespeicherten Prozedur zuordnen muss. Wenn ich jemals etwas an meiner gespeicherten Prozedur oder meinem Datenbankschema ändern würde, müsste ich sowohl die gespeicherte Prozedur als auch den benutzerdefinierten Typ wiederholen. Ich möchte diese Situation vermeiden. Außerdem ist die Überlegenheit, die TVP gegenüber XML hat, für meine Situation nicht nützlich, da meine Datenarraygröße niemals 1000 überschreitet. Dies bedeutet, dass ich die hier vorgeschlagene Lösung nicht verwenden kann: So fügen Sie mehrere Datensätze mit XML in SQL Server 2008 ein

Eine ähnliche Diskussion hier ( UPSERT - Gibt es eine bessere Alternative zu MERGE oder @@ rowcount? ) Unterscheidet sich von dem, was ich frage, weil ich versuche, mehrere Zeilen in eine Tabelle einzufügen .

Ich hatte gehofft, dass ich einfach die folgenden Abfragen verwenden würde, um die Werte aus der XML zu erhöhen. Aber das wird nicht funktionieren. Dieser Ansatz soll nur funktionieren, wenn die Eingabe eine einzelne Zeile ist.

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

Die nächste Alternative besteht darin, ein erschöpfendes IF EXISTS oder eine seiner Variationen der folgenden Form zu verwenden. Ich lehne dies jedoch aus Gründen der suboptimalen Effizienz ab:

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

Die nächste Option war die Verwendung der Merge-Anweisung wie hier beschrieben: http://www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.html . Aber dann habe ich hier über Probleme mit der Zusammenführungsabfrage gelesen: http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ . Aus diesem Grund versuche ich, Merge zu vermeiden.

Meine Frage lautet nun: Gibt es eine andere Option oder einen besseren Weg, um mithilfe von XML-Parametern in gespeicherten SQL Server 2008-Prozeduren mehrere Upsert zu erzielen?

Bitte beachten Sie, dass die Daten im XML-Parameter möglicherweise einige Datensätze enthalten, die nicht UPSERTed werden sollten, da sie älter als der aktuelle Datensatz sind. ModifiedDateSowohl in der XML- als auch in der Zieltabelle gibt es ein Feld, das verglichen werden muss, um festzustellen, ob der Datensatz aktualisiert oder verworfen werden soll.

GMalla
quelle
Der Versuch, Änderungen am Proc in Zukunft zu vermeiden, ist kein guter Grund, kein TVP zu verwenden. Wenn die Daten in Änderungen übergeben werden, werden Sie in beiden Fällen Änderungen am Code vornehmen.
Max Vernon
1
@MaxVernon Ich hatte zuerst den gleichen Gedanken und machte fast einen sehr ähnlichen Kommentar, weil das allein kein Grund ist, TVP zu vermeiden. Aber sie sind etwas anstrengender und mit der Einschränkung "nie über 1000 Zeilen" (manchmal oder vielleicht sogar oft impliziert?) Ist es ein bisschen durcheinander. Ich nehme jedoch an, ich sollte meine Antwort dahingehend qualifizieren, dass <1000 Zeilen gleichzeitig sich nicht allzu sehr von XML unterscheiden, solange sie nicht 10.000 Mal hintereinander aufgerufen werden. Dann summieren sich sicherlich geringfügige Leistungsunterschiede.
Solomon Rutzky
Die Probleme MERGE, auf die Bertrand hinweist, sind meistens Randfälle und Ineffizienzen, keine Stopper - MS hätte es nicht veröffentlicht, wenn es ein echtes Minenfeld gewesen wäre. Sind Sie sicher, dass die Windungen, die Sie vermeiden MERGEmöchten, nicht mehr potenzielle Fehler verursachen als sie speichern?
Jon of All Trades
@ JonofAllTrades Um fair zu sein, was ich vorgeschlagen habe, ist nicht wirklich so kompliziert im Vergleich zu MERGE. Die Schritte INSERT und UPDATE von MERGE werden weiterhin separat verarbeitet. Der Hauptunterschied in meinem Ansatz ist die Tabellenvariable, die die aktualisierten Datensatz-IDs enthält, und die DELETE-Abfrage, die diese Tabellenvariable verwendet, um diese Datensätze aus der temporären Tabelle der eingehenden Daten zu entfernen. Und ich nehme an, die QUELLE könnte direkt von @ XMLparam.nodes () stammen, anstatt in eine temporäre Tabelle zu kopieren, aber das ist nicht viel zusätzliches Zeug, um sich keine Sorgen machen zu müssen, jemals in einem dieser Randfälle zu sein; - ).
Solomon Rutzky

Antworten:

11

Ob die Quelle XML oder ein TVP ist, macht keinen großen Unterschied. Der Gesamtbetrieb ist im Wesentlichen:

  1. AKTUALISIEREN Sie vorhandene Zeilen
  2. Fehlende Zeilen einfügen

Sie tun dies in dieser Reihenfolge, denn wenn Sie zuerst EINFÜGEN, sind alle Zeilen vorhanden, um das UPDATE zu erhalten, und Sie werden wiederholt für alle Zeilen arbeiten, die gerade eingefügt wurden.

Darüber hinaus gibt es verschiedene Möglichkeiten, dies zu erreichen, und verschiedene Möglichkeiten, um die Effizienz zu steigern.

Beginnen wir mit dem Nötigsten. Da das Extrahieren des XML wahrscheinlich einer der teureren Teile dieses Vorgangs ist (wenn nicht der teuerste), möchten wir dies nicht zweimal tun müssen (da wir zwei Vorgänge ausführen müssen). Also erstellen wir eine temporäre Tabelle und extrahieren die Daten aus dem XML in diese:

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

Von dort aus machen wir das UPDATE und dann das 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
                     );

Nachdem wir die grundlegende Operation ausgeführt haben, können wir einige Maßnahmen zur Optimierung ergreifen:

  1. Erfassen Sie @@ ROWCOUNT der Einfügung in die temporäre Tabelle und vergleichen Sie sie mit @@ ROWCOUNT des UPDATE. Wenn sie gleich sind, können wir das INSERT überspringen

  2. Erfassen Sie die über die OUTPUT-Klausel aktualisierten ID-Werte und löschen Sie diese aus der temporären Tabelle. Dann braucht das INSERT das nichtWHERE NOT EXISTS(...)

  3. Wenn die eingehenden Daten Zeilen enthalten, die nicht synchronisiert (dh weder eingefügt noch aktualisiert) werden sollen, sollten diese Datensätze vor dem UPDATE entfernt werden

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;

Ich habe dieses Modell mehrmals für Importe / ETLs verwendet, die entweder weit über 1000 Zeilen oder vielleicht 500 in einem Stapel von insgesamt 20.000 Zeilen enthalten - über eine Million Zeilen. Ich habe jedoch den Leistungsunterschied zwischen dem LÖSCHEN der aktualisierten Zeilen aus der temporären Tabelle und dem Aktualisieren des Felds [IsUpdate] nicht getestet.


Bitte beachten Sie die Entscheidung, XML über TVP zu verwenden, da höchstens 1000 Zeilen gleichzeitig importiert werden müssen (siehe Frage):

Wenn dies hier und da einige Male aufgerufen wird, ist der geringfügige Leistungsgewinn in TVP möglicherweise die zusätzlichen Wartungskosten nicht wert (Sie müssen den Prozess löschen, bevor Sie den benutzerdefinierten Tabellentyp ändern, den App-Code ändern usw.). . Wenn Sie jedoch 4 Millionen Zeilen importieren und jeweils 1000 senden, sind dies 4000 Ausführungen (und 4 Millionen XML-Zeilen zum Parsen, unabhängig davon, wie sie aufgeteilt sind), und selbst ein geringfügiger Leistungsunterschied, wenn sie nur einige Male ausgeführt werden summieren sich zu einem spürbaren Unterschied.

Abgesehen davon ändert sich die von mir beschriebene Methode nicht, außer dass SELECT FROM @XmlInputParam durch SELECT FROM @TVP ersetzt wird. Da TVPs schreibgeschützt sind, können Sie sie nicht löschen. Ich denke, Sie könnten einfach ein WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)letztes SELECT (gebunden an das INSERT) anstelle des einfachen hinzufügen WHERE IsUpdate = 0. Wenn Sie die @UpdateIDsTabellenvariable auf diese Weise verwenden würden, könnten Sie sogar davonkommen, die eingehenden Zeilen nicht in die temporäre Tabelle zu kopieren.

Solomon Rutzky
quelle