“不存在”似乎正在减慢插入速度

mep*_*ope 4 performance sql-server insert logging query-performance

我正在尝试运行一个脚本,该脚本一次将超过 2000 万条记录批量插入到 10,000 条表中。在运行开始时,它似乎工作正常。尽管一旦插入了大量记录(270,000),脚本的完成速度就开始变慢了。插入另外 30,000 条记录需要 23 小时。我最好的猜测是,随着新记录数量的增加,脚本检查新记录是否已经存在的部分需要更长的时间。我已经创建了脚本中使用的表的索引,但我需要为这个脚本缩短运行时间。任何帮助将非常感激。我的脚本如下。

CREATE NONCLUSTERED INDEX [plan2TMP] ON [dbo].[plan2]
(
       [l_dr_plan1] ASC
)
INCLUDE
(
l_address,
l_provider
)ON [PRIMARY]
GO

CREATE NONCLUSTERED INDEX [nameTMP] ON [dbo].[name]
(
       [dr_id],
       [nationalid]
)ON [PRIMARY]
GO

CREATE NONCLUSTERED INDEX [plan1TMP] ON [dbo].[plan1]
(
[dr_id],
[cmt]
)ON [PRIMARY]
GO

DECLARE @BatchSize int = 10000

WHILE 1 = 1
BEGIN

INSERT INTO plan2(
                     l_dr_plan1
                     , l_address
                     , l_provider
                     )
SELECT TOP (@BatchSize)
                     dr1.link
                     ,ad1.link
                     ,dr.link
from plan1 dr1
INNER JOIN name dr ON dr1.dr_id = dr.dr_id
INNER JOIN name2 dr2 on  dr2.nationalid = dr1.cmt
INNER JOIN address1 ad1 ON ad1.dr_id = dr2.dr_id
WHERE NOT EXISTS (
                  SELECT l_plan1
                  FROM plan2
                  WHERE ltrim(rtrim(dr1.link)) + ltrim(rtrim(ad1.link)) + ltrim(rtrim(dr.link)) = ltrim(rtrim(l_dr_plan1)) +ltrim(rtrim(l_address)) +ltrim(rtrim(l_provider))
                  )
AND dr1.cmt <> ''

IF @@ROWCOUNT < @BatchSize
  BREAK
END
Run Code Online (Sandbox Code Playgroud)

源数据并不是很糟糕。我可以在没有遇到任何问题的情况下拉出 ltrim/rtrim。我有串联的原因是我无法想出更好的方法来比较我插入的值和已经插入的值。

链接值是每个表中的唯一列(但不是 PK 或 FK)。plan2表引用plan1、address1和name表的链接,在后端连接这些记录,并在前端一起显示。

一次插入 10k 的目的是日志文件变得如此之大,以至于服务器内存达到大约 10 mbs。对于批处理,它将分块提交记录并防止日志文件扩展失控。

Joe*_*ish 5

您现在拥有的代码可以保证在每个连续循环上做更多的工作。我假设目标表一开始是空的,所以SELECT查询的部分只需要返回 10k 行,以便将 10k 行插入到plan2. 对于下一个循环,SELECT查询返回的前 10k 行已经在目标表中,因此需要返回 20k 行才能将 10k 额外行插入目标表。您预计总共插入 2000 万行。批量大小为 10k 行时,您需要执行 2000 个批次。平均需要每一批将来自读取10个百万行SELECT查询,让你的程序来完成你所需要的SELECT部分INSERT生成 200 亿行。这是大量的工作,而且对于您在这里使用的循环方法来说,这项工作是不可避免的。

另一件立即突出的事情是在@BatchSize没有RECOMPILE提示的情况下使用局部变量。您在TOP语句中使用了它,默认情况下 SQL Server 会假设您只需要查询返回 100 行。如果没有RECOMPILE提示或其他提示,查询优化器不知道局部变量的值是什么。查询优化器可以根据TOP运算符的存在选择不同的计划,如果估计值为 100,它可能会选择一个对您尝试执行的操作效率非常低的计划。这是检查和发布估计或实际查询计划真正有用的地方。

让我们假设查询优化器选择做一个嵌套循环连接对plan2表中NOT EXISTS。查询的那部分将在每个连续循环上做更多的工作,但它甚至比以前更糟。对于第 N 个循环,您需要扫描10000 * (N - 1)10000 * N次。如果您已经在表中插入了 270k 行,这意味着 N 的值为 28,那么您可能正在扫描 10000 * 27 * 10000 * 28 = 75600000000 行,只是为了插入接下来的 10k 行。这很容易需要 7 个小时才能完成。您可能会遇到这个问题,或者您可能会遇到一个完全不同的问题。摆脱@BatchSize变量或使用提示可能会有所帮助。

我对批量插入的方法是尽可能避免在SELECT零件上循环。您是否能够在插入之前删除目标表上的所有非聚集索引?是否有具有恢复模型的数据库SIMPLE,您可以将数据的临时副本写入其中?如果那里有足够的空间,Tempdb 可以工作。您可以利用最少的日志记录来跳过循环。可能所需要的只是添加一个TABLOCK暗示。如果您当前的数据库具有正确的恢复模型,您可能只能执行一次 2000 万行的插入操作。如果没有,您可以插入到另一个数据库中,并遍历该数据的临时副本以插入到您的目标表中。重要的是,如果你需要循环,那么每个循环都需要花费相同的时间。您不希望每个循环的工作量像当前一样线性或二次增加。

如果不能进行单个插入,则应更改循环样式。目标是避免对源表中的同一行进行多次处理。如果可能,您还希望按目标表的聚簇顺序插入。我不知道你的数据模型,所以我不能给出非常具体的建议,但是plan1表有主键吗?plan1表中的行数与您需要插入的行数之间是否存在某种关系plan2?例如,您可以尝试编写处理plan110k 行块中的表的代码。有了正确的索引,可以进行相对高效的查询。我有一篇博文介绍了一系列不同的循环方法。帖子中的示例只是从单个表中读取数据,但您可以将它们调整为SELECT您拥有的查询。再次强调这一点,您正在寻找一种在每个循环中执行恒定工作量的策略。

如果NOT EXISTS出于某种原因确实需要保留以下代码,则更典型,我认为它可以完成您的需求:

WHERE NOT EXISTS (
    SELECT 1
    FROM plan2
    WHERE dr1.link = plan2.l_dr_plan1
    AND ad1.link = plan2.l_address
    AND dr.link = plan2.l_provider
)
Run Code Online (Sandbox Code Playgroud)

我不清楚问题出在ltrimor 上rtrim,但是尽可能删除这样的函数总是好的。