使用xml参数插入多个数据时如何避免使用Merge查询?

GMa*_*lla 11 xml sql-server stored-procedures t-sql upsert

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

现在,我试图在采用 XML 参数的存储过程中实现这一点。我使用 XML 而不是表值参数的原因是,执行后者时,我必须在 SQL 中创建自定义类型并将此类型与存储过程相关联。如果我以后更改了存储过程或数据库架构中的某些内容,则必须重做存储过程和自定义类型。我想避免这种情况。此外,TVP 相对于 XML 的优势对我的情况没有用,因为我的数据数组大小永远不会超过 1000。这意味着我不能使用这里提出的解决方案:How to insert multiple records using XML in SQL server 2008

此外,这里的类似讨论(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
Run Code Online (Sandbox Code Playgroud)

下一个替代方法是使用详尽的 IF EXISTS 或其以下形式的变体之一。但是,我以次优效率为由拒绝了这一点:

IF (SELECT COUNT ... ) > 0
    UPDATE
ELSE
    INSERT
Run Code Online (Sandbox Code Playgroud)

下一个选项是使用 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 参数实现多个 upsert?

请注意,XML 参数中的数据可能包含一些由于比当前记录更旧而不应进行 UPSERT 的记录。ModifiedDateXML 和目标表中都有一个字段需要进行比较,以确定是否应该更新或丢弃记录。

Sol*_*zky 13

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

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

您按该顺序执行此操作,因为如果您先 INSERT,则所有行都存在以获取 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);
Run Code Online (Sandbox Code Playgroud)

从那里我们执行 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
                     );
Run Code Online (Sandbox Code Playgroud)

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

  1. 捕获 @@ROWCOUNT 插入临时表并与更新的 @@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;
Run Code Online (Sandbox Code Playgroud)

我在导入/ETL 上多次使用过这个模型,它们要么有超过 1000 行,要么是 500 行,总共有 2 万行——超过一百万行。但是,我尚未测试从临时表中删除更新行与仅更新 [IsUpdate] 字段之间的性能差异。


请注意关于使用 XML over TVP 的决定,因为一次最多导入 1000 行(在问题中提到):

如果这被多次调用,那么很可能在 TVP 中的微小性能提升可能不值得额外的维护成本(需要在更改用户定义的表类型、应用程序代码更改等之前删除 proc) . 但是如果你导入 400 万行,一次发送 1000 行,那就是 4000 次执行(还有 400 万行 XML 解析,不管它如何分解),即使只执行几次也会有微小的性能差异加起来有明显的差异。

话虽如此,除了将 SELECT FROM @XmlInputParam 替换为 SELECT FROM @TVP 之外,我所描述的方法不会改变。由于 TVP 是只读的,您将无法从中删除。我想您可以简单地将 a 添加WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)到最终的 SELECT(与 INSERT 绑定)而不是简单的WHERE IsUpdate = 0. 如果您以@UpdateIDs这种方式使用table 变量,那么您甚至可以不将传入的行转储到临时表中。