源是XML还是TVP并没有太大的区别。总体操作实质上是:
- 更新现有行
- 插入缺少的行
您按该顺序进行操作,因为如果先插入,则所有行都将获得UPDATE,并且您将对刚刚插入的任何行进行重复的工作。
除此之外,还有其他方法可以完成此任务,还可以通过各种方法来调整一些其他效率。
让我们从最低限度开始。由于提取XML可能是此操作中较昂贵的部分之一(如果不是最昂贵的话),因此我们不想重复执行两次(因为我们要执行两个操作)。因此,我们创建了一个临时表并将数据从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(...)
如果传入数据中有任何行不应该同步(即既不插入也不更新),则应在执行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;
我已经在Imports / ETL上多次使用此模型,它们的行数超过1000行,或者批次中的行数可能超过500,总行数为2万(超过一百万行)。但是,我尚未测试临时表中已更新行的删除与仅更新[IsUpdate]字段之间的性能差异。
请注意有关决定通过TVP使用XML的决定,因为一次最多只能导入1000行(问题中提到):
如果在此多次调用此命令,那么TVP的小幅性能提升可能不值得额外的维护成本(需要在更改用户定义的表类型,更改应用程序代码之前删除proc)。 。但是,如果您要导入400万行,一次发送1000行,即执行4000次(无论如何拆分,则有400万行XML进行解析),并且仅执行几次,即使性能有微小的差异也将加起来有明显的区别。
话虽如此,我所描述的方法在将SELECT FROM @XmlInputParam替换为SELECT FROM @TVP之外不会改变。由于TVP是只读的,因此您将无法从中删除它们。我猜你可以简单地在WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)
最终的SELECT中添加一个(与INSERT绑定),而不是simple WHERE IsUpdate = 0
。如果要以@UpdateIDs
这种方式使用表变量,那么甚至可以避免不将传入的行转储到临时表中。