使用xml参数更新多个数据时,如何避免使用合并查询?


9

我正在尝试使用值数组更新表。数组中的每个项目都包含与SQL Server数据库中的表中的一行相匹配的信息。如果表中已经存在该行,则使用给定数组中的信息更新该行。否则,我们在表中插入新行。我已经基本描述了upsert。

现在,我试图在采用XML参数的存储过程中实现这一目标。我使用XML而不是表值参数的原因是因为这样做,我将不得不在SQL中创建自定义类型并将该类型与存储过程相关联。如果我曾经更改过存储过程或数据库模式中的某些内容,则必须重做存储过程和自定义类型。我想避免这种情况。此外,TVP优于XML的优势对我的情况没有用,因为我的数据数组大小永远不会超过1000。这意味着我无法使用此处提出的解决方案:如何在SQL Server 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或其以下形式的变体之一。但是,我以效率不佳为由拒绝了这一点:

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/。因此,我试图避免合并。

因此,现在我的问题是:在SQL Server 2008存储过程中,是否还有其他选择或更好的方法可以使用XML参数实现多次更新?

请注意,XML参数中的数据可能包含一些记录,由于它们比当前记录要旧,因此不应该被UPSERTed。ModifiedDateXML和目标表中都存在一个需要比较的字段,以确定是否应该更新或丢弃记录。


尝试避免将来对proc进行更改,这并不是不使用TVP的充分理由。如果传入的数据发生更改,您最终将以两种方式更改代码。
Max Vernon

1
@MaxVernon起初我有相同的想法,并且几乎发表了非常相似的评论,因为仅此一项并不是避免使用TVP的理由。但是他们确实要付出更多的努力,并且要注意“从不超过1000行”(有时甚至是什至经常暗示?),这有点麻烦。但是,我想我应该对我的回答进行限定,以便声明一次少于1000行与XML的区别不大,只要连续调用不超过1万次即可。那么,性能上的细微差别肯定会加起来。
所罗门·鲁兹基

MERGEBertrand指出的问题主要是极端情况和效率低下,没有显示出障碍物-如果它是真正的雷区,MS不会发布它。您确定要避免的卷积MERGE不会产生比所保存的更多的潜在错误吗?
所有行业的乔恩

@JonofAllTrades坦白说,与相比,我提出的建议确实不是那么复杂MERGE。MERGE的INSERT和UPDATE步骤仍然单独处理。我的方法的主要区别是表变量保存更新的记录ID,DELETE查询使用该表变量从传入数据的临时表中删除这些记录。而且我认为SOURCE可以直接来自@ XMLparam.nodes()而不是转储到临时表,但是,这仍然不是很多额外的事情,不必担心在这些极端情况之一中发现自己;- )。
所罗门·鲁兹基

Answers:


11

源是XML还是TVP并没有太大的区别。总体操作实质上是:

  1. 更新现有行
  2. 插入缺少的行

您按该顺序进行操作,因为如果先插入,则所有行都将获得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
                     );

现在我们已经完成了基本操作,我们可以做一些优化的事情:

  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;

我已经在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这种方式使用表变量,那么甚至可以避免不将传入的行转储到临时表中。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.