xmlパラメータを使用して複数のデータをアップサートするときにマージクエリの使用を回避する方法


9

値の配列でテーブルを更新しようとしています。配列の各項目には、SQL Serverデータベースのテーブルの行と一致する情報が含まれています。行がテーブルに既に存在する場合、その行を指定された配列の情報で更新します。それ以外の場合は、テーブルに新しい行を挿入します。基本的にアップサートについて説明しました。

現在、私はこれを、XMLパラメーターを受け取るストアード・プロシージャーで実現しようとしています。テーブル値パラメーターではなくXMLを使用する理由は、後者の場合、SQLでカスタムタイプを作成し、このタイプをストアドプロシージャに関連付ける必要があるためです。将来、ストアドプロシージャまたはdbスキーマの何かを変更した場合、ストアドプロシージャとカスタムタイプの両方をやり直す必要があります。このような状況は避けたいです。さらに、データ配列のサイズが1000を超えることはないため、TVPがXMLよりも優れていることは、私の状況では役に立ちません。これは、ここで提案されているソリューションを使用できないことを意味します。SQLServer 2008でXMLを使用して複数のレコードを挿入する方法

また、ここでの同様のディスカッション(UPSERT-MERGEまたは@@ rowcountのより良い代替案はありますか?)は、テーブルに複数の行をアップサートしようとしているため、私が求めているものとは異なります。

次の一連のクエリを使用して、xmlから値を更新することを望んでいました。しかし、これはうまくいきません。このアプローチは、入力が単一の行である場合にのみ機能すると想定されています。

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

次の代替方法は、完全なIF EXISTSまたは次の形式のバリエーションの1つを使用することです。しかし、私は次善の効率であることを理由にこれを拒否します:

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

次のオプションは、http//www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.htmlで説明されているようにMergeステートメントを使用することでした。しかし、それから私はここでマージクエリの問題について読みました:http : //www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/。このため、私はMergeを回避しようとしています。

だから、今私の質問です:SQL Server 2008ストアドプロシージャでXMLパラメーターを使用して複数のアップサートを実現するためのその他のオプションまたはより良い方法はありますか?

XMLパラメータのデータには、現在のレコードよりも古いためUPSERTすべきではないレコードが含まれている場合があることに注意してください。ModifiedDateXMLと宛先テーブルの両方に、レコードを更新するか破棄するかを決定するために比較する必要があるフィールドがあります。


将来的に proc 変更を加えないようにすることは、TVPを使用しない理由にはなりません。渡されたデータが変更された場合、いずれかの方法でコードを変更することになります。
Max Vernon

1
@MaxVernon私は最初は同じ考えを持っていて、それだけではTVPを回避する理由はないため、ほとんど同じようなコメントをしました。しかし、それらはもう少し努力する必要があり、「1000行を超えない」という警告(時々、またはおそらくはそれが暗示されるか?)があるため、これはちょっとしたやり過ぎです。ただし、一度に1000行未満が、1行で1万回呼び出されない限り、XMLとそれほど変わらないという私の答えを修飾する必要があると思います。その場合、パフォーマンスの小さな違いが確実に加わります。
ソロモンルツキー2015年

MERGEBertrandが指摘する問題は、ほとんどがエッジケースと非効率であり、ストッパーを示していません。MSが実際の地雷原である場合、MSはそれをリリースしなかったでしょう。回避しようとしている畳み込みによって、MERGE保存している以上の潜在的なエラーが発生していないことを確信していますか?
Jon of All Trades、

@JonofAllTrades公平に言うと、私が提案したものは、に比べて実際にはそれほど複雑ではありませんMERGE。MERGEのINSERTおよびUPDATEステップは引き続き個別に処理されます。私のアプローチの主な違いは、更新されたレコードIDを保持するテーブル変数と、そのテーブル変数を使用して受信データの一時テーブルからそれらのレコードを削除するDELETEクエリです。そして、SOURCEは一時テーブルにダンプするのではなく@ XMLparam.nodes()から直接である可能性があると思いますが、それでも、これらのエッジケースのいずれかで自分自身を見つけることを心配する必要がない余分なものは多くありません;- )。
ソロモンルツキー2015年

回答:


11

ソースがXMLでもTVPでも、大きな違いはありません。全体的な操作は基本的に次のとおりです。

  1. 既存の行を更新する
  2. 不足している行を挿入

最初にINSERTすると、すべての行が存在してUPDATEを取得し、挿入されたばかりの行に対して繰り返し作業を行うため、この順序で実行します。

それを超えて、これを達成するためのさまざまな方法と、それからいくつかの追加の効率を調整するさまざまな方法があります。

最小限のことから始めましょう。XMLの抽出は、この操作の中で最も高価な部分の1つである可能性が高いため(最も高価ではないにしても)、2回行う必要はありません(実行する操作が2つあるため)。そこで、一時テーブルを作成して、XMLからデータを抽出します。

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

そこからUPDATEを実行し、次に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
                     );

基本的な操作が完了したので、最適化するためにいくつかのことができます。

  1. 一時テーブルへの挿入の@@ ROWCOUNTをキャプチャし、UPDATEの@@ ROWCOUNTと比較します。それらが同じ場合、INSERTをスキップできます

  2. OUTPUT句を介して更新されたID値をキャプチャし、それらを一時テーブルから削除します。その後、INSERTは必要ありませんWHERE NOT EXISTS(...)

  3. 受信データに同期してはならない(つまり、挿入も更新もしない)行がある場合、それらのレコードは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;

私はこのモデルを、20kの合計セットのうち100行を超える1000行を超える、またはバッチで500行を超えるインポート/ ETLで数回使用しました。ただし、一時テーブルからの更新された行のDELETEと[IsUpdate]フィールドの更新だけのパフォーマンスの違いはテストしていません。


一度にインポートする行が最大で1000行あるため、XML over TVPを使用する決定について注意してください(質問に記載されています)。

これがあちこちで何度か呼び出されている場合、TVPのマイナーなパフォーマンスの向上は、追加のメンテナンスコストに見合わない可能性があります(ユーザー定義のテーブルタイプを変更する前にプロシージャを削除する必要がある、アプリのコードの変更など)。 。しかし、400万行をインポートし、一度に1000を送信する場合、それは4000の実行(そして、どのように分解しても400万行のXMLを解析する)であり、数回だけ実行したときの小さなパフォーマンスの違いでさえ目立つ違いに追加します。

そうは言っても、私が説明した方法は、SELECT FROM @XmlInputParamをSELECT FROM @TVPに置き換える以外は変わりません。TVPは読み取り専用であるため、TVPから削除することはできません。WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)シンプルなの代わりに、最後のSELECT(INSERTに関連付けられた)に単にを追加することができると思いますWHERE IsUpdate = 0@UpdateIDsこの方法でtable変数を使用すると、受信した行を一時テーブルにダンプしないことで問題を回避できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.