如何优化写入大型远程表?

时间:2015-02-12 21:55:03

标签: sql sql-server stored-procedures join linked-server

我正在使用一组服务器,这些服务器都记录通常每天发生多次的事件,然后调用存储过程将这些记录复制到中央远程服务器上的匹配表。该存储过程的关键部分如下:

首先,因为事件需要几分钟,所以当它们被复制并且中央服务器中的某些记录在某些列中具有空值时,有时它们将无法完成。存储过程会更新上次发生的记录:

UPDATE r SET r.ToUpdateA = l.ToUpdateA, r.ToUpdateB = l.ToUpdateB
FROM LocalTable l INNER JOIN RemoteServer.RemoteDB.dbo.RemoteTable r
ON l.IdentifierA = r.IdentifierA AND l.IdentifierB = r.IdentifierB
WHERE r.ToUpdateB IS NULL AND l.ToUpdateB IS NOT NULL;

IdentifierAIdentifierB都是识别给定记录所必需的;第一个标识它来自哪个服务器。

第二个是更新本身,识别远程表上不是的本地表上的记录并插入它们:

INSERT INTO RemoteServer.RemoteDB.dbo.RemoteTable (A, B, C...)
SELECT l.A, l.B, l.C...
FROM LocalTable l LEFT OUTER JOIN RemoteServer.RemoteDB.dbo.RemoteTable r
ON l.IdentifierA = r.IdentifierA AND l.IdentifierB = r.IdentifierB
WHERE r.uid IS NULL;

随着中央远程表的增长,这些连接将花费太长时间,特别是在较大的服务器上。估计的执行计划表明大部分工作是在UPDATE的内部联接(与r.ToUpdateB IS NULL部分相关)的远程扫描中完成的,并且是{{的远程查询1}}左外连接(从整个INSERT中选择三列)。我可以想到三种类型的解决方案:

  1. 删除旧记录。我们从未需要回顾一个月左右。
  2. 在"辐条"之间拆分存储过程之间的工作。和" hub"服务器。这意味着只是盲目地将新记录复制到" hub"上的新中间表,可能在"辐条"上有一个额外的RemoteTable列。指示是否已复制给定记录,并且具有" hub"除掉重复本身。
  3. 修改连接速度更快。这是我想要做的事情 - 这可能是将最近的数据发送到中心服务器的方法在同一个查询中指示它如何处理它,而不是从集线器中获取大量数据。我尝试将BIT更改为INNER JOIN,但如果我正确地解释修改后的执行计划,则会花费数量级更长
  4. #3可行吗?如果是这样,怎么样?

1 个答案:

答案 0 :(得分:1)

到目前为止,我发现在链接服务器DML语句上大幅度提高性能的最好方法是不执行它们;-)。幸运的是,我比讽刺更加厚颜无耻:)。

诀窍是在桌子所在的服务器上进行DML工作。为了做到这一点,你:

  • 收集相关/相关数据
  • 将其打包为XML(但存储在NVARCHAR(MAX)变量中,因为XML不是链接服务器调用的有效数据类型)
  • 在远程服务器上执行proc,传入该数据集,将XML解包到临时表中并加入到该临时表中(因此是本地事务)。

我将此方法详细解释为两个答案:


上述方法涉及如何更快地传输数据,但没有解决在识别首先要移动的数据时可以进行的改进。

扫描目标表,即使它只是在同一实例上的不同数据库中,每次确定丢失的记录都非常昂贵,因为行计数会增加。通过将新记录转储到队列表中可以避免这种昂贵。此队列表仅包含需要插入和可能更新的记录。一旦您知道记录已远程同步,就会删除这些记录。这类似于问题中的选项#3,但在单个查询中没有这样做,因为无法识别" new"扫描目标表之外的记录(简单但不会缩放)或在它们进入时捕获它们(稍微努力但是可以很好地扩展)。

队列表可以是:

  • 用户创建的表,通过INSERT触发器填充。该表可以只是关键字段和状态(需要跟踪INSERT与潜在的更新)
  • 通过在源表上启用变更数据捕获(CDC)或更改跟踪而创建的系统表

在任何一种情况下,你都可以采取以下措施:

创建队列表

CREATE TABLE RemoteTableQueue
(
  RemoteTableQueueID INT NOT NULL IDENTITY(-2140000000, 1)
                       CONSTRAINT [PK_RemoteTableQueue] PRIMARY KEY,
  IdentifierA DATATYPE NOT NULL,
  IdentifierB DATATYPE NOT NULL,
  StatusID TINYINT NOT NULL,
);

创建一个AFTER INSERT触发器

INSERT INTO RemoteTableQueue (IdentifierA, IdentifierB, StatusID)
  SELECT IdentifierA, IdentifierB, 1
  FROM   INSERTED;

更新您的ETL过程(假设这是单线程的)

CREATE TABLE #TempUpdate
(
  IdentifierA DATATYPE NOT NULL,
  IdentifierB DATATYPE NOT NULL,
  ToUpdateA DATATYPE NOT NULL,
  ToUpdateB DATATYPE NOT NULL
);

BEGIN TRAN;

INSERT INTO #TempUpdate (IdentifierA, IdentifierB, ToUpdateA, ToUpdateB)
  SELECT lt.IdentifierA, lt.IdentifierB, lt.ToUpdateA, lt.ToUpdateB
  FROM   LocalTable lt
  INNER JOIN RemoteTableQueue rtq
          ON lt.IdentifierA = rtq.IdentifierA
         AND lt.IdentifierB = rtq.IdentifierB
  WHERE  rtq.StatusID = 2 -- rows eligible for UPDATE
  AND    lt.ToUpdateB IS NOT NULL;

DECLARE @UpdateData NVARCHAR(MAX);

SET @UpdateData = (
     SELECT *
     FROM   #TempUpdate
     FOR XML ...);

EXEC RemoteServer.RemoteDB.dbo.UpdateProc @UpdateData;

DELETE rtq
FROM   RemoteTableQueue rtq
INNER JOIN #TempUpdate tmp
        ON tmp.IdentifierA = rtq.IdentifierA
       AND tmp.IdentifierB = rtq.IdentifierB;

TRUNCATE TABLE #TempUpdate;

INSERT INTO #TempUpdate (IdentifierA, IdentifierB, ToUpdateA, ToUpdateB)
  SELECT lt.IdentifierA, lt.IdentifierB, lt.ToUpdateA, lt.ToUpdateB
  FROM   LocalTable lt
  INNER JOIN RemoteTableQueue rtq
          ON lt.IdentifierA = rtq.IdentifierA
         AND lt.IdentifierB = rtq.IdentifierB
  WHERE  rtq.StatusID = 1 -- rows to INSERT;

SET @UpdateData = (
     SELECT lt.*
     FROM   LocalTable lt
     INNER JOIN #TempUpdate tmp
             ON tmp.IdentifierA = rtq.IdentifierA
            AND tmp.IdentifierB = rtq.IdentifierB
     FOR XML ...);

EXEC RemoteServer.RemoteDB.dbo.InsertProc @UpdateData;

-- no need to check for changed value later if it already has it now
DELETE rtq
FROM   RemoteTableQueue rtq
INNER JOIN #TempUpdate tmp
        ON tmp.IdentifierA = rtq.IdentifierA
       AND tmp.IdentifierB = rtq.IdentifierB
WHERE  tmp.ToUpdateB IS NOT NULL;

-- we know these records will need to be checked later since they are NULL
UPDATE rtq
SET    rtq.StatusID = 2 -- rows eligible for UPDATE
FROM   RemoteTableQueue rtq
INNER JOIN #TempUpdate tmp
        ON tmp.IdentifierA = rtq.IdentifierA
       AND tmp.IdentifierB = rtq.IdentifierB
WHERE  tmp.ToUpdateB IS NULL;

COMMIT;

其他步骤

  • 将TRY / CATCH逻辑添加到ETL过程以正确处理ROLLBACK

  • 更新远程INSERT和UPDATE过程以将传入数据批量处理到目标表中(循环通过传入XML填充的临时表,一次处理1000行直到完成)。

  • 如果"讲话"之间存在太多争用服务器同时报告,在远程服务器上创建一个传入的队列表,只需插入传入的XML数据,无需额外的逻辑。这是一个非常干净和快速的操作。然后在远程服务器上创建本地作业以检查每隔几分钟,如果传入的队列表中存在行,则将它们处理到目标表中。这将源服务器/表与目标服务器/表之间的事务分开,从而减少争用。

  • [RemoteTableQueueID]字段存在,以防您将ETL模型更改为全天每3-10分钟运行一次,抓取要处理的TOP (@BatchSize)行,在这种情况下,您将想要ORDER BY [RemoteTableQueueID] ASC