値の配列でテーブルを更新しようとしています。配列の各項目には、SQL Serverデータベースのテーブルの行と一致する情報が含まれています。行がテーブルにすでに存在する場合、その行を指定された配列の情報で更新します。それ以外の場合は、テーブルに新しい行を挿入します。基本的にアップサートについて説明しました。
現在、私はこれを、XMLパラメーターを受け取るストアード・プロシージャーで実現しようとしています。テーブル値パラメーターではなくXMLを使用する理由は、後者の場合、SQLでカスタムタイプを作成し、このタイプをストアドプロシージャに関連付ける必要があるためです。将来、ストアドプロシージャまたはdbスキーマの何かを変更した場合、ストアドプロシージャとカスタムタイプの両方をやり直す必要があります。この状況は避けたいです。さらに、TVPがXMLよりも優れていることは、私のデータ配列サイズが1000を超えることはないため、私の状況では役に立ちません。これは、ここで提案されているソリューションを使用できないことを意味します SQLサーバーでXMLを使用して複数のレコードを挿入する方法2008
また、ここでの同様のディスカッション( PSERT-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
次のオプションは、ここで説明されているようにMergeステートメントを使用することでした: http://www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.html 。しかし、それから私はここでマージクエリの問題について読みました: http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ 。このため、私はMergeを回避しようとしています。
だから、今私の質問は:SQL Server 2008のストアドプロシージャでXMLパラメーターを使用して複数のアップサートを実現するための他のオプションまたはより良い方法はありますか?
XMLパラメータのデータには、現在のレコードよりも古いためにUPSERTするべきではないレコードが含まれている場合があります。 XMLと宛先テーブルの両方に、レコードを更新するか破棄するかを決定するために比較する必要があるModifiedDate
フィールドがあります。
ソースがXMLであってもTVPであっても、大きな違いはありません。全体的な操作は基本的に次のとおりです。
最初に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
);
_
基本的な操作が完了したので、最適化するためにいくつかのことができます。
一時テーブルへの挿入の@@ ROWCOUNTをキャプチャし、UPDATEの@@ ROWCOUNTと比較します。それらが同じ場合、INSERTをスキップできます
oUTPUT句を介して更新されたID値をキャプチャし、それらを一時テーブルから削除します。その後、INSERTはWHERE NOT EXISTS(...)
を必要としません
notを同期する必要がある行(つまり、挿入も更新もされない)がある場合、それらのレコードは、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;
_
私はこのモデルをインポート/ ETLで数回使用しました。このセットは、20kの合計セットから100万行を超えるバッチで、1000行を超えるか500バッチになる可能性があります。ただし、一時テーブルからの更新された行のDELETEと[IsUpdate]フィールドの更新だけのパフォーマンスの違いはテストしていません。
一度にインポートする行が最大で1000行あるため、XML over TVPを使用する決定について注意してください(質問に記載されています)。
これがあちこちで何度か呼び出されている場合、TVPでのマイナーなパフォーマンスの向上は、追加のメンテナンスコストに見合わない可能性があります(ユーザー定義のテーブルタイプ、アプリコードの変更などを変更する前に、プロシージャを削除する必要があります)。 。しかし、400万行をインポートし、一度に1000を送信する場合、それは4000の実行(そして、どのように分解しても400万行のXMLを解析する)であり、数回だけ実行した場合のわずかなパフォーマンスの違いでも目立つ違いに追加します。
そうは言っても、私が説明した方法は、SELECT FROM @XmlInputParamをSELECT FROM @TVPに置き換える以外は変わりません。 TVPは読み取り専用であるため、TVPから削除することはできません。単純な_WHERE IsUpdate = 0
_ではなく、WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)
を最後のSELECT(INSERTに関連付けられている)に追加するだけでよいと思います。この方法で_@UpdateIDs
_テーブル変数を使用すると、受信した行を一時テーブルにダンプしないで済むようになります。