带有 FAST_FORWARD 游标循环的存储过程开始快,结束慢

Posted

技术标签:

【中文标题】带有 FAST_FORWARD 游标循环的存储过程开始快,结束慢【英文标题】:Stored procedure with FAST_FORWARD cursor loop starts fast, ends slow 【发布时间】:2010-10-12 14:59:23 【问题描述】:

我有一个存储过程,它使用 FAST_FORWARD 游标按时间顺序循环一组约 300k 记录,并根据许多运行变量和标志的状态将它们分配给声明集,部分实现为表变量。我已经对如何基于集合进行了很多思考,但我就是做不到。所以我坚持使用游标方法,需要优化这段代码。

我注意到,前 10% 的进度加载和处理速度非常快(2000 行/秒),接近 20% 的进度已减慢到约 300 行/秒,最后减慢到大约 60 行/秒。

IMO 这可能是由于 4 个原因:

光标变慢了,我认为使用 FAST_FORWARD 光标不太可能 处理速度变慢。对于我的“组”,我使用插入、更新和删除的表变量。在任何给定时刻都有最大值。这些变量中大约有 10 行。 插入目标表的速度变慢。我不明白为什么会这样,我没有在它们上定义触发器,它们只是普通的表。 邪恶的魔法

那个或者我的百分比计数器坏了:

SET @curprogress = @curprogress + 1
IF (@curprogress - ((@totprogress / 100) * (FLOOR(@curprogress * 100 / @totprogress)))) BETWEEN 0 AND 1 BEGIN
    SET @msg = CAST(FLOOR(@curprogress * 100 / @totprogress) AS VARCHAR)
    RAISERROR('%s%s', 0, 1, @msg, '%...') WITH NOWAIT;
END

有没有人知道要查找什么以及如何继续加快此查询的速度?

我的代码的符号摘录:

WHILE....
-- Fetch new record to be assigned to one of the open declaration sets
FETCH NEXT INTO @row_field1, @row_field2....
IF (@flag2 = 1) AND ((@flag1 = 0) OR (@row_field1 <> @prevrow_field1)) 
BEGIN
    -- Logging info: we are closing a child declaration set
    INSERT INTO @logtable SELECT '--> LOG MESSAGE'
    INSERT INTO @logtable
    SELECT format_message(@row_field1, @calc_field2, field3...)
    FROM @runningtable_sub S LEFT JOIN @runningtable_main M ON S.MainID = M.ID

    -- Update enddate of parent
    UPDATE M SET M.enddate = DATEADD(day,365,S.enddate)
    FROM @runningtable_sub S
    LEFT JOIN @runningtable_main M
    ON S.MainID = M.ID

    -- close and save child
    INSERT INTO outputtable_main
    SELECT @field1, COALESCE(Z.Field1,'NULL'), S.startdate, S.enddate,
        M.Startdate, M.Enddate
    FROM @runningtable_sub S 
    LEFT JOIN @runningtable_main M ON S.MainID = M.ID

    -- delete child from running table
    DELETE FROM @runningtable_sub WHERE S.enddate < @curdate
END

【问题讨论】:

【参考方案1】:

我可以想到很多原因会变慢,但如果不知道数据的基数就很难缩小范围。

随机观察:

    你的 format_message() 函数肯定是狗;所有 UDF 都是。但是您每次要插入多少行? @runningtable_main 永远不会被清除。 更新成本很高 删除成本很高。如果您使用临时表并修改您的实现,您可以截断而不是删除

要自己解决一些问题,请添加工具:

DECLARE @now DATETIME, @duration INT, @rowcount INT

WHILE....
-- Fetch new record to be assigned to one of the open declaration sets
FETCH NEXT INTO @row_field1, @row_field2....
IF (@flag2 = 1) AND ((@flag1 = 0) OR (@row_field1 <> @prevrow_field1)) 
BEGIN
    PRINT '---------------'

    -- Logging info: we are closing a child declaration set
    INSERT INTO @logtable SELECT '--> LOG MESSAGE'
    SET @now = GETDATE()
    INSERT INTO @logtable
    SELECT format_message(@row_field1, @calc_field2, field3...)
    FROM @runningtable_sub S LEFT JOIN @runningtable_main M ON S.MainID = M.ID
    SELECT @rowcount = @@ROWCOUNT, @duration = DATEDIFF(ms,@now,GETDATE)
    RAISERROR('%i row(s) inserted into @logtable, %i milliseconds',-1,-1,@rowcount,@duration) WITH NOWAIT

    -- Update enddate of parent
    SET @now = GETDATE()
    UPDATE M SET M.enddate = DATEADD(day,365,S.enddate)
    FROM @runningtable_sub S
    LEFT JOIN @runningtable_main M
    ON S.MainID = M.ID
    SELECT @rowcount = @@ROWCOUNT, @duration = DATEDIFF(ms,@now,GETDATE)
    RAISERROR('%i row(s) updated in @runningtable_main, %i milliseconds',-1,-1,@rowcount,@duration) WITH NOWAIT

    -- close and save child
    SET @now = GETDATE()
    INSERT INTO outputtable_main
    SELECT @field1, COALESCE(Z.Field1,'NULL'), S.startdate, S.enddate,
        M.Startdate, M.Enddate
    FROM @runningtable_sub S 
    LEFT JOIN @runningtable_main M ON S.MainID = M.ID
    SELECT @rowcount = @@ROWCOUNT, @duration = DATEDIFF(ms,@now,GETDATE)
    RAISERROR('%i row(s) inserted into outputtable_main, %i milliseconds',-1,-1,@rowcount,@duration) WITH NOWAIT

    -- delete child from running table
    SET @now = GETDATE()
    DELETE FROM @runningtable_sub WHERE S.enddate < @curdate
    SELECT @rowcount = @@ROWCOUNT, @duration = DATEDIFF(ms,@now,GETDATE)
    RAISERROR('%i row(s) deleted from @runningtable_sub, %i milliseconds',-1,-1,@rowcount,@duration) WITH NOWAIT
END

如果您包含了整个代码(包括游标声明),而不仅仅是一个符号摘录,这里的某个人可能会为您重新编写它以提高效率和/或一起避免使用游标。

【讨论】:

我无法发布整个代码,不过会尝试发布更多内容。主表在其他地方被清除。我每次通过插入 0-3 行。表变量的更新和删除是否也很昂贵?光标位于通常在 10-16 秒内加载的视图上。我将光标更改为一个表变量和一个WHILE 循环,但没有任何变化。所以问题一定出在身体的某个地方。 原来是UPDATE FROM .. LEFT JOIN 声明,使用您的计时工具。每一行它也会变慢。奇怪的是,这个问题现在已经自行消失了,尽管我对 SP 没有做出明显的改变。我不知所措,但我用分组输入重写了该过程,因此我不必再使用运行表,而是使用运行变量。这本身要快得多,并且不再有 UPDATE 语句。我会接受你对仪表位的回答。

以上是关于带有 FAST_FORWARD 游标循环的存储过程开始快,结束慢的主要内容,如果未能解决你的问题,请参考以下文章

Oracle存储过程游标for循环怎么写

Oracle存储过程游标for循环怎么写

Oracle存储过程游标for循环怎么写

mysql存储过程 游标双重循环

在存储过程的循环中使用游标

调用存储过程的Oracle游标循环