在同一连接中跨函数调用访问数据

Posted

技术标签:

【中文标题】在同一连接中跨函数调用访问数据【英文标题】:Access data across function calls in same connection 【发布时间】:2021-03-18 10:34:30 【问题描述】:

我需要帮助解决与 SQL Server 中的递归函数相关的性能问题。我有一个项目任务表,每个项目都有一个提前期。我的函数递归调用自己来计算每个任务的截止日期,基于前面任务的总和(简单地说..)。该功能在大规模上执行缓慢,我认为主要是因为必须为每个祖先,每个后续任务重新计算截止日期。

所以我想知道,有没有一种方法可以存储计算值,该值可以从函数调用持续到函数调用,并且只会持续连接的生命周期?然后,如果我的函数找到预先计算的值,它可能会“短路”,并避免对每个到期日期请求进行重新评估。 基本架构如下,对所讨论的函数进行了粗略的表示(这个函数也可以用 cte 完成,但计算仍然重复相同的计算):

Create Table Projects(id int, DueDate DateTime)
Create Table Items(id int, Parent int, Project int, Offset int)
Create Table Tasks (id int, Parent int, Leadtime Int, Sequence int)

insert into Projects Values
(100,'1/1/2021')

Insert into Items Values
(0,null, 100, 0)
,(1,12, null, 0)
,(2,15, null, 1)

Insert into Tasks Values
 (10,0,1,1)
,(11,0,1,2)
,(12,0,2,3)
,(13,0,1,4)
,(14,1,1,1)
,(15,1,1,2)
,(16,2,2,1)
,(17,2,1,2);

CREATE FUNCTION GetDueDate(@TaskID int)
    Returns DATETIME
    AS BEGIN
    Declare @retval DateTime = null
    Declare @parent int = (Select Parent from Tasks where ID = @TaskID)
    Declare @parentConsumingOp int = (select Parent from Items where ID = @parent)
    Declare @parentOffset int = (select Offset from Items where ID = @parent)
    Declare @seq int = (Select Sequence from Tasks where ID = @TaskID)
    Declare @NextTaskID int = (select ID from Tasks where Parent = @parent and Sequence = @seq-1)
    Declare @Due DateTime = (select DueDate from Projects where ID = (Select Project from Items where ID = (Select Parent from Tasks where ID = @TaskID)))
    Declare @leadTime int = (Select LeadTime from Tasks where ID = @TaskID)
    if @NextTaskID is not null
    BEGIN
        SET @retval = DateAdd(Day,@leadTime * -1,dbo.GetDueDate(@NextTaskID))
    END ELSE IF @parentConsumingOp Is Not Null
    BEGIN
        SET @retval = DateAdd(Day,(@leadTime + @parentOffset)*-1,dbo.GetDueDate(@parentConsumingOp))
    END ELSE SET @retval = DateAdd(Day,@parentOffset*-1,@Due)
    Return @retval
END

编辑:Sql Fiddle Here

【问题讨论】:

为什么不使用临时表来保存值?临时表存在于会话中,因此您的代码需要检查它是否存在,如果不存在则创建它(即在第一次调用函数时)。如果您可以在同一会话中多次调用该函数,那么最好在每次递归结束时显式删除它(或删除其内容) 提供样本数据、期望的结果以及您想要实现的语言的解释。你可能不需要单独的函数或递归 CTE 来做你想做的事(不管是什么)。 感谢@GordonLinoff,提供示例数据,并在 sql fiddle 链接中复制。期望的结果是:更快地计算截止日期。在原始问题中解释了我如何实现这一点。所有这些都显示在 Sql Fiddle 中。我错过了什么吗? @NickW,看起来我无法从函数写入任何表(临时表或基本表),这是我有点拖延的地方...... 这对sqlclr来说是个好机会吗? 【参考方案1】:

警告:以下内容基于您提供的示例数据,而不是尝试通过您的函数中的逻辑(即您想要实现的目标而不是您如何实现它)...

函数的结果似乎是:

对于“这个任务”

project.due_date - (sum(tasks.leadtime) +1) 其中 tasks.sequence

如果是这种情况,那么这个函数给出的结果与你的相同,但更简单:

CREATE FUNCTION GetDueDate1(@TaskID int)
    Returns DATETIME
    AS BEGIN
    Declare @retval DateTime = null
    Declare @parent int = (Select Parent from Tasks where ID = @TaskID)
    Declare @seq int = (Select sequence from Tasks where ID = @TaskID)
    Declare @totlead int = (select Sum(Leadtime) - 1 from Tasks where parent = @parent and sequence <= @Seq)
    Declare @duedate DateTime = (select p.DueDate from tasks t inner join items i on t.parent = i.id inner join projects p on i.Project = p.id where t.id = 13)
    SET @retval = DateAdd(Day,@totlead * -1,@duedate)
    Return @retval
END;

如果我对您的数据运行这两个函数:

select id
,leadtime
, sequence
, [dbo].[GetDueDate](id) "YourFunction"
, [dbo].[GetDueDate1](id) "MyFunction"
from tasks
where parent = 0;

我得到相同的结果:

id  leadtime    sequence    YourFunction            MyFunction
10  1           1           2021-01-01 00:00:00.000 2021-01-01 00:00:00.000
11  1           2           2020-12-31 00:00:00.000 2020-12-31 00:00:00.000
12  2           3           2020-12-29 00:00:00.000 2020-12-29 00:00:00.000
13  1           4           2020-12-28 00:00:00.000 2020-12-28 00:00:00.000

希望这有帮助吗?如果不是,请提供一些示例数据,其中我的函数产生的结果与您的不同

更新以下评论

好点,上面的代码不适用于您的所有数据。 我一直在思考这个问题,并提出了以下建议 - 如果我有任何误解,请随时指出:

    显然,您的函数将只返回作为参数传入的任务的截止日期。在此过程中,它还将只计算一次之前每个任务的到期日期 因此没有必要“保存”为其他任务计算的到期日期,因为它们只会在初始任务 ID 的计算中使用一次(因此保留这些值不会获得性能提升,因为它们不会被重复使用)如果您再次调用该函数,它们将不会被使用 - 因为这不是函数的工作方式:它无法“知道”您之前可能已经调用过该函数并且已经计算了该任务 ID 的到期日期作为中间步骤的一部分

重新阅读您最初的解释,您似乎实际上想要计算许多任务(或所有任务?)的截止日期 - 而不仅仅是单个任务。如果是这种情况,那么我不会(仅)使用函数(本质上仅限于 1 个任务),而是编写一个存储过程,循环遍历所有任务,计算它们的截止日期并将其保存到表(新表或更新您的任务表)。

    您需要确保以适当的顺序处理任务,以便首先计算用于后续任务的计算 您可以在函数中重复使用逻辑(甚至从 SP 中调用该函数),但添加检查到期日期是否已计算的步骤(即尝试从表中选择它) ,如果有就使用它,如果没有就计算它(并将其保存到表格中) 只要修改了计算中使用的表格中的相关数据,您就需要运行此 SP

【讨论】:

非常感谢@NickW,您上面的代码对于前 4 行是正确的,但与我在后四行的结果不匹配。这里缺少的环节是每个项目可能有一个父任务,在这种情况下,提前期需要递归汇总,因此计算父任务的截止日期,然后将其用作汇总提前期的基础为当前项目。

以上是关于在同一连接中跨函数调用访问数据的主要内容,如果未能解决你的问题,请参考以下文章

函数调用的成本是多少?

如何通过php在访问中调用模块函数?

WPF中跨项目调用窗体导致资源丢失,求解决

多个线程同时调用一个函数的情况

Javascript中跨多个文件的全局变量

BigQuery SQL 中跨多个字段的拆分函数