是否可以在存储过程中定义函数?

Sha*_*ehr 9 stored-procedures sql-server-2012 functions

我有一个存储过程:

create proc sp_MyProc(@calcType tinyint) as
begin
    -- some stuff collating data into #MyTempTable

    if (@calcType = 1) -- sum
        select A, B, C, CalcField = sum(Amount) 
        from #MyTempTable t 
        join AnotherTable a on t.Field1 = a.Field1;
        group by A, B, C
    else if (@calcType = 2) -- average
        select A, B, C, CalcField = avg(Amount) 
        from #MyTempTable t 
        join AnotherTable a on t.Field1 = a.Field1;
        group by A, B, C
    else if (@calcType = 3) -- some other fancy formula
        select A, B, C, CalcField = sum(case when t.Type = 1 then 1 else 0 end) * t.Factor 
        from #MyTempTable t 
        join AnotherTable a on t.Field1 = a.Field1;
        group by A, B, C
    -- plus a whole bunch of other, similar cases
    else    
        select A, B, C, CalcField = 0.0
        from #MyTempTable t 
        join AnotherTable a on t.Field1 = a.Field1;
        group by A, B, C
end
Run Code Online (Sandbox Code Playgroud)

现在,@calcType 的不同值的所有这些不同情况似乎都在浪费大量空间,并导致我复制和粘贴所有内容,这总是让我感到不寒而栗。

是否有某种方法可以为 CalcField 声明一个函数,类似于 C# 中的 lambda 符号,以使我的代码更紧凑和可维护?我想做这样的事情:

declare @func FUNCTION(@t #MyTempTable) as real -- i.e. input is of type #MyTempTable and output is of type real

if (@calcType = 1) -- sum
    set @func = sum(@t.Amount)
else if (@calcType = 2) -- average
    set @func = avg(@t.Amount)
else if (@calcType = 3) -- some other fancy formula
    set @func = sum(case when @t.Type = 1 then 1 else 0 end) * @t.Factor 
-- plus a whole bunch of other, similar cases
else    
    set @func = 0;

select A, B, C, CalcField = @func(t) 
from #MyTempTable t 
join AnotherTable a on t.Field1 = a.Field1;
group by A, B, C
Run Code Online (Sandbox Code Playgroud)

显然这里的语法不起作用,但是有什么可以实现我想要的吗?

Mar*_*ith 6

不,这是不可能的。

由于引用了永久的 TVF 或视图,因此无法选择 #MyTempTable

我看到了一个临时视图的连接项目请求,并且同意有时它们会很有用。这是作为一个请求模块级表表达式的副本而关闭的。

你也许可以重写为

SELECT A,
       B,
       C,
       CASE @calcType
         WHEN 1
           THEN sum(Amount)
         WHEN 2
           THEN avg(Amount)
       END
FROM   #MyTempTable t
       JOIN AnotherTable a
         ON t.Field1 = a.Field1
GROUP  BY A,
          B,
          C
Run Code Online (Sandbox Code Playgroud)

或者如果您的需求更复杂(例如相同的形状结果集和使用相同的来源但不同的分组条件)

WITH Base
     AS (SELECT A,
                B,
                C,
                Factor,
                Type
         FROM   #MyTempTable t
                JOIN AnotherTable a
                  ON t.Field1 = a.Field1) 
SELECT A,
       B,
       C,
       sum(Amount)
FROM   Base
WHERE  @calcType = 1
GROUP  BY A,
          B,
          C
UNION ALL
SELECT A,
       B,
       C,
       CalcField = 0.0 * t.Factor
FROM   Base
WHERE  @calcType NOT IN ( 1, 2, 3 ) 
Run Code Online (Sandbox Code Playgroud)

特别是最后一个,您可能会考虑OPTION (RECOMPILE)简化计划。(可能它会有一个带有启动谓词的过滤器,没有提示,实际上并不执行冗余分支,但您需要检查。如果保留启动谓词,这种方法的行估计也可能是错误的)。

如果这些都不适合,您将需要进入动态 SQL 的领域。


Sol*_*zky 5

严格来说(T-SQL 子程序):没有。

从技术上讲(一种抽象公式定义一次的方法):是的。

务实地说:这取决于:)。

以下是目前阻碍您对 T-SQL 函数进行限制的问题:

  • 它们不能动态声明
  • 他们无法访问临时表
  • 它们不能是聚合函数(这正是您要寻找的)

然而,这一切都可以在 SQLCLR 中完成(好吧,不是动态部分,但这似乎不是这里的重点)。使用 SQLCLR,您可以创建一个可以访问临时表的函数,它甚至可以是一个聚合函数。当然,对于诸如SUM和 之类的简单计算,AVG您可能会比减少代码重复所获得的性能损失更多,但这是一个测试问题(因此在很大程度上是“这取决于”的原因)。

现在在这种特定情况下,似乎不需要访问临时表,因为每行值自然会发送到用户定义的聚合中。假设 dbo.DynamicCalc 的签名为DynamicCalc(@CalcType TINYINT, @Amount FLOAT)

select A, B, C, CalcField = dbo.DynamicCalc(2, t.Amount) 
from #MyTempTable t 
join AnotherTable a
    on t.Field1 = a.Field1
group by A, B, C;
Run Code Online (Sandbox Code Playgroud)

或者:

select A, B, C, CalcField = dbo.DynamicCalc(3, IIF(t.Type = 1, t.Factor, 0)) 
from #MyTempTable t 
join AnotherTable a
    on t.Field1 = a.Field1
group by A, B, C;
Run Code Online (Sandbox Code Playgroud)

或者您可以将t.Factor其本身作为@Amount参数传入并添加一个额外的参数,@Type INT该参数t.Type将仅在@CalcType= 3 时使用。

同样,是否采用这种方法是一个实用性问题,很大程度上取决于您拥有什么公式。CASE @calcType如果公式足够简单(因为它们似乎基于问题中显示的代码),@Martin 建议执行在公式之间切换的语句会更有效。但是,如果这些公式变得非常复杂,或者您确实需要访问临时表,那么这是一个可以考虑的选项。