【问题标题】:Simplifying (aliasing) T-SQL CASE statements. Any improvement possible?简化(别名)T-SQL CASE 语句。有什么改进的可能吗?
【发布时间】:2009-06-04 17:01:40
【问题描述】:

如您所见,这很糟糕。有什么选择吗?我尝试在 group by 子句中使用列别名,但无济于事。

select count(callid) ,
case
        when callDuration > 0 and callDuration < 30 then 1
        when callDuration >= 30 and callDuration < 60 then 2
        when callDuration >= 60 and callDuration < 120 then 3
        when callDuration >= 120 and callDuration < 180 then 4
        when callDuration >= 180 and callDuration < 240 then 5
        when callDuration >= 240 and callDuration < 300 then 6
        when callDuration >= 300 and callDuration < 360 then 7
        when callDuration >= 360 and callDuration < 420 then 8
        when callDuration >= 420 and callDuration < 480 then 9
        when callDuration >= 480 and callDuration < 540 then 10
        when callDuration >= 540 and callDuration < 600 then 11
        when callDuration >= 600 then 12
end as duration
from callmetatbl
where programid = 1001 and callDuration > 0
group by case
        when callDuration > 0 and callDuration < 30 then 1
        when callDuration >= 30 and callDuration < 60 then 2
        when callDuration >= 60 and callDuration < 120 then 3
        when callDuration >= 120 and callDuration < 180 then 4
        when callDuration >= 180 and callDuration < 240 then 5
        when callDuration >= 240 and callDuration < 300 then 6
        when callDuration >= 300 and callDuration < 360 then 7
        when callDuration >= 360 and callDuration < 420 then 8
        when callDuration >= 420 and callDuration < 480 then 9
        when callDuration >= 480 and callDuration < 540 then 10
        when callDuration >= 540 and callDuration < 600 then 11
        when callDuration >= 600 then 12
end

编辑: 我真的想问如何拥有单个案例源,但无论如何都欢迎案例修改(尽管不太有用,因为间隔可能会被修改,甚至可能会自动生成)。

正如某些人所考虑的那样, callDuration 确实是一个浮点数,因此某些列出的解决方案对我的用例无效,因为将值排除在间隔之外。

课程:

  • 在 case 表达式中寻找模式以减少它,如果可能且值得的话

     case
        when callDuration > 0 AND callDuration < 30 then 1
        when callDuration > 600 then 12
        else floor(callDuration/60) + 2  end
     end as duration
    
  • 使用内联视图来获得案例的单一来源

    select count(d.callid), d.duration
    from (   
       select callid
            , case
               when callDuration > 0 AND callDuration < 30 then 1
               when callDuration > 600 then 12
               else floor(callDuration/60) + 2  end
              end as duration
        from callmetatbl
        where programid = 1001
              and callDuration > 0
    ) d
    group by d.duration
    
  • 或者使用公用表表达式

       with duration_case as (
          select callid ,
          case
            when callDuration > 0 AND callDuration < 30 then 1
            when callDuration > 600 then 12
            else floor(callDuration/60) + 2  end
          end as duration
       from callmetatbl
       where programid = 1001 and callDuration > 0 )
        select count(callid), duration
        from duration_case
        group by duration
    
  • 或者使用用户定义的函数(目前没有例子:-))

  • 或者使用查找表和连接

    DECLARE @t TABLE(durationFrom float, durationTo float, result INT)
    --populate table with values so the query works
    select count(callid) , COALESCE(t.result, 12)
    from callmetatbl JOIN @t AS t ON callDuration >= t.durationFrom 
    AND callDuration < t.durationTo 
    where programid = 1001 and callDuration > 0
    

感谢大家,我很难选择一个可以接受的答案,因为许多人涵盖了问题的不同部分(我当时认为这是一个简单的问题,答案很简单:-),抱歉混乱)。

【问题讨论】:

  • 如果问题是“我如何为复杂表达式设置别名,以便可以在 GROUP BY 子句中引用它”,一种方法是使用内联视图(请参阅我的答案)或视图定义存储在数据库中)。 other 问题(其他人似乎都在回答)是“我如何简化这个特定的表达式”,也有几种方法。
  • @vinko:我已经更新了我的答案以包含一个示例用户定义函数(只是一个标量函数来替换内联表达式)。表值函数可用于返回查找表......这也是一种可行的方法。使用查找表和连接条件(行被删除和/或重复的可能性)注意间隙和重叠。考虑需要测试的内容与灵活性的需要。 (根据我的经验,测试代码比编写代码需要更多的努力。)
  • @vinko:还考虑只指定每个“断点”值一次(只使用一个边界,并保证在满足条件时提前返回。)我推断每个 callDuration (> 0) 应该落入一个桶中,而不是丢失在两个桶之间的空隙中。

标签: sql-server tsql case


【解决方案1】:

问:如何在 GROUP BY 子句中获取别名

一种方法是使用内联视图。 [编辑] Remus Rusanu (+1!) 的答案给出了一个通用表表达式的例子来完成同样的事情。 [/编辑]

内联视图为您提供复杂表达式的简单“别名”,然后您可以在外部查询的 GROUP BY 子句中引用它:

select count(d.callid)
     , d.duration
  from (select callid
             , case
               when callDuration >= 600 then 12
               when callDuration >= 540 then 11
               when callDuration >= 480 then 10
               when callDuration >= 420 then 9
               when callDuration >= 360 then 8
               when callDuration >= 300 then 7
               when callDuration >= 240 then 6
               when callDuration >= 180 then 5
               when callDuration >= 120 then 4
               when callDuration >=  60 then 3
               when callDuration >=  30 then 2
               when callDuration >    0 then 1
               --else null
               end as duration
             from callmetatbl
            where programid = 1001
              and callDuration > 0
       ) d
group by d.duration

让我们打开它。

  • 内部(缩进)查询被调用并且内联视图(我们给它一个别名d
  • 在外部查询中,我们可以从d引用别名duration

这应该足以回答您的问题。如果您正在寻找等效的替换表达式,来自 tekBlues (+1 !) 的表达式是正确的答案(它适用于边界和非整数。 )

使用来自 tekBlues (+1!) 的替换表达式:

select count(d.callid)
     , d.duration
  from (select callid
             , case 
               when callduration >=30 and callduration<600
                    then floor(callduration/60)+2
               when callduration>0 and callduration< 30
                    then 1 
               when callduration>=600
                    then 12
               end as duration
          from callmetatbl
         where programid = 1001
           and callDuration > 0
       ) d
 group by d.duration

(这应该足以回答您的问题。)


[UPDATE:] 示例用户定义函数(替代内联 CASE 表达式)

CREATE FUNCTION [dev].[udf_duration](@cd FLOAT)
RETURNS SMALLINT
AS
BEGIN
  DECLARE @bucket SMALLINT
  SET @bucket = 
  CASE
  WHEN @cd >= 600 THEN 12
  WHEN @cd >= 540 THEN 11
  WHEN @cd >= 480 THEN 10
  WHEN @cd >= 420 THEN 9
  WHEN @cd >= 360 THEN 8
  WHEN @cd >= 300 THEN 7
  WHEN @cd >= 240 THEN 6
  WHEN @cd >= 180 THEN 5
  WHEN @cd >= 120 THEN 4
  WHEN @cd >=  60 THEN 3
  WHEN @cd >=  30 THEN 2
  WHEN @cd >    0 THEN 1
  --ELSE NULL
  END
  RETURN @bucket
END

select count(callid)
     , [dev].[udf_duration](callDuration)
  from callmetatbl
 where programid = 1001
   and callDuration > 0
 group by [dev].[udf_duration](callDuration)

注意:注意用户定义的函数会增加开销,并且(当然)会增加对另一个数据库对象的依赖。

此示例函数等效于原始表达式。 OP CASE 表达式没有任何间隙,但它确实引用了每个“断点”两次,我更喜欢只测试下限。 (CASE 在满足条件时返回。反向执行测试会让未处理的情况(ELSE NULL 不是必需的,但可以添加以确保完整性。

其他详情

(请务必检查性能和优化器计划,以确保它与原始版本相同(或不明显差)。过去,我在将谓词推送到内联视图时遇到问题,不是吗'看起来这对你来说不会是一个问题。)

存储视图

请注意,inline 视图也可以作为视图定义存储在数据库中。但是没有理由这样做,除了从您的语句中“隐藏”复杂的表达式。

简化复杂的表达式

使复杂表达式“更简单”的另一种方法是使用用户定义的函数。但是用户定义的函数有其自身的一系列问题(包括性能下降)。

添加数据库“查找”表

一些答案​​建议向数据库添加“查找”表。我不认为这真的有必要。这当然可以完成,如果您希望能够从 callDuration 中导出不同的 duration 值,则可以做到这一点, 无需修改您的查询和 无需运行任何 DDL 语句(例如,更改视图定义或修改用户定义的函数)。

通过连接到“查找”表,一个好处是您可以通过对“查找”表执行 DML 操作来使查询返回不同的结果集。

但同样的优势实际上也可能是一个缺点。

仔细考虑好处是否真的超过了坏处。考虑新表对单元测试的影响,如何验证查找表的内容是否有效且未更改(任何重叠?是否有任何差距?),对代码持续维护的影响(由于额外的复杂性)。

一些重大假设

这里给出的很多答案似乎都假设callDuration 是一个整数数据类型。似乎他们忽略了它不是整数的可能性,但也许我错过了问题中的那个金块。

这是一个相当简单的测试用例来证明:

callDuration BETWEEN 0 AND 30

等价于

callDuration > 0 AND callDuration < 30

【讨论】:

  • 你说得对,我多么愚蠢地认为拥有 23k 代表的人知道的足以理解我所说的要点。这是一个很好的答案,但你的态度至少是不必要的。
  • @Spencer:那些拥有 100 名代表的后来者呢?
  • 我的感觉是,任何无法从示例中正确推断的开发人员都没有希望。
【解决方案2】:

您有什么理由不使用between?案例陈述本身看起来还不错。如果你真的讨厌它,你可以把所有这些都放到一个表中并映射它。

Durations
------------------
low   high   value
0     30     1
31    60     2

等等……

(SELECT value FROM Durations WHERE callDuration BETWEEN low AND high) as Duration

编辑:或者,在使用浮点数并且between 变得很麻烦的情况下。

(SELECT value FROM Durations WHERE callDuration >= low AND callDuration <= high) as Duration

【讨论】:

  • 恕我直言,这是最好的解决方案
  • 这在浮动的情况下很难看,必须设置 high = 29.99999999 等。
  • 相应地调整了我的答案。
  • 你刚刚替换了它的等价物,你需要做 callDuration >= low 和 callDuration
  • 天哪,你是对的!而且地图不完整!并且 SQL 查询不完整!哦,伙计,我的榜样一定毫无价值!对不起大家!
【解决方案3】:

案例可以这样写:

case 
when callduration >=30 and callduration<600 then floor(callduration/60)+2
when callduration>0 and callduration< 30 then 1 
when callduration>=600 then 12
end

不需要,用“where callduration>0”替换它

我喜欢之前给出的翻译表答案!这是最好的解决方案

【讨论】:

  • 我称之为公式(这个答案)和查找表之间的折腾。您使用哪个取决于您的位置:公式实现的模式是否一致?它(必须)在 12 点达到顶峰吗? “他们”是否希望随着时间的推移更改报告范围(如果是表格,则更容易做到)?
  • +1 这个表达式相当于OP查询中的CASE expr。
【解决方案4】:

您需要将 CASE 向下推到查询树的下方,以便 GROUP BY 可以看到它的投影。这可以通过两种方式实现:

  1. 使用派生表(Spencer、Adam 和 Jeremy 已经展示了如何使用)
  2. 使用公用表表达式

    with duration_case as (
    select callid ,
    case
        when callDuration > 0 and callDuration < 30 then 1
        when callDuration >= 30 and callDuration < 60 then 2
        when callDuration >= 60 and callDuration < 120 then 3
        when callDuration >= 120 and callDuration < 180 then 4
        when callDuration >= 180 and callDuration < 240 then 5
        when callDuration >= 240 and callDuration < 300 then 6
        when callDuration >= 300 and callDuration < 360 then 7
        when callDuration >= 360 and callDuration < 420 then 8
        when callDuration >= 420 and callDuration < 480 then 9
        when callDuration >= 480 and callDuration < 540 then 10
        when callDuration >= 540 and callDuration < 600 then 11
        when callDuration >= 600 then 12
    end as duration
    from callmetatbl
    where programid = 1001 and callDuration > 0 )
       select count(callid), duration
       from duration_case
       group by duration
    

两种解决方案在各个方面都是等效的。我发现 CTE 更具可读性,有些人更喜欢派生表,因为它更便携。

【讨论】:

  • 谁编辑了代码并让它显示得很好,它有什么问题?我想不通
  • 您在某些行中缺少一些空格,所有行必须缩进 4 个空格
  • 如果一切都失败了,添加更多空格:-)
  • +1 !公用表表达式作为内联视图的替代方案,很好的答案!
【解决方案5】:

callDuration 除以 60:

case
        when callDuration between 1 AND 29 then 1
        when callDuration > 600 then 12
        else (callDuration /60) + 2  end
end as duration

请注意,between 包含边界,我假设 callDuration 将被视为整数。


更新:
将此与其他一些答案结合起来,您可以将整个查询归结为:

select count(d.callid), d.duration
from (   
       select callid
            , case
                when callDuration between 1 AND 29 then 1
                when callDuration > 600 then 12
                else (callDuration /60) + 2  end
              end as duration
        from callmetatbl
        where programid = 1001
              and callDuration > 0
    ) d
group by d.duration

【讨论】:

  • +1 我喜欢。我做过类似的事情 - 请参阅下面的答案。
【解决方案6】:
select count(callid), duration from
(
    select callid ,
    case
            when callDuration > 0 and callDuration < 30 then 1
            when callDuration >= 30 and callDuration < 60 then 2
            when callDuration >= 60 and callDuration < 120 then 3
            when callDuration >= 120 and callDuration < 180 then 4
            when callDuration >= 180 and callDuration < 240 then 5
            when callDuration >= 240 and callDuration < 300 then 6
            when callDuration >= 300 and callDuration < 360 then 7
            when callDuration >= 360 and callDuration < 420 then 8
            when callDuration >= 420 and callDuration < 480 then 9
            when callDuration >= 480 and callDuration < 540 then 10
            when callDuration >= 540 and callDuration < 600 then 11
            when callDuration >= 600 then 12
    end as duration
    from callmetatbl
    where programid = 1001 and callDuration > 0
) source
group by duration

【讨论】:

    【解决方案7】:

    未经测试:

    select  count(callid) , duracion
    from
        (select 
            callid,
            case        
                when callDuration > 0 and callDuration < 30 then 1        
                when callDuration >= 30 and callDuration < 60 then 2        
                when callDuration >= 60 and callDuration < 120 then 3        
                when callDuration >= 120 and callDuration < 180 then 4        
                when callDuration >= 180 and callDuration < 240 then 5        
                when callDuration >= 240 and callDuration < 300 then 6        
                when callDuration >= 300 and callDuration < 360 then 7        
                when callDuration >= 360 and callDuration < 420 then 8        
                when callDuration >= 420 and callDuration < 480 then 9        
                when callDuration >= 480 and callDuration < 540 then 10        
                when callDuration >= 540 and callDuration < 600 then 11        
                when callDuration >= 600 then 12        
                else 0
            end as duracion
        from callmetatbl
        where programid = 1001) GRP
    where duracion > 0
    group by duracion
    

    【讨论】:

      【解决方案8】:

      将所有案例添加到表变量中并进行外连接

      DECLARE @t TABLE(durationFrom INT, durationTo INT, result INT)
      --        when callDuration > 0 and callDuration < 30 then 1
      INSERT INTO @t VALUES(1, 30, 1);
      --        when callDuration >= 30 and callDuration < 60 then 2
      INSERT INTO @t VALUES(30, 60, 2);
      
      select count(callid) , COALESCE(t.result, 12)
      from callmetatbl JOIN @t AS t ON callDuration >= t.durationFrom AND callDuration  < t.durationTo 
      where programid = 1001 and callDuration > 0
      

      【讨论】:

        【解决方案9】:

        这是我的尝试。您需要的所有组件都可以直接用 SQL 完成。

        select
          count(1) as total
         ,(fixedDuration / divisor) + adder as duration
        from
        (
            select
              case/*(30s_increments_else_60s)*/when(callDuration<60)then(120)else(60)end as divisor
             ,case/*(increment_by_1_else_2)*/when(callDuration<30)then(1)else(2)end as adder
             ,(/*duration_capped@600*/callDuration+600-ABS(callDuration-600))/2 as fixedDuration
             ,callDuration
            from 
              callmetatbl
            where
              programid = 1001
            and 
              callDuration > 0
        ) as foo
        group by
          (fixedDuration / divisor) + adder
        

        这是我用于测试的 SQL。 (我没有自己的私人电话metatbl ;)

        select
          count(1) as total
         ,(fixedDuration / divisor) + adder as duration
        from
        (
            select
              case/*(30s_increments_else_60s)*/when(callDuration<60)then(120)else(60)end as divisor
             ,case/*(increment_by_1_else_2)*/when(callDuration<30)then(1)else(2)end as adder
             ,(/*duration_capped@600*/callDuration+600-ABS(callDuration-600))/2 as fixedDuration
             ,callDuration
            from -- callmetatbl -- using test view below
              (  
               select 1001 as programid,   0 as callDuration union
               select 1001 as programid,   1 as callDuration union
               select 1001 as programid,  29 as callDuration union
               select 1001 as programid,  30 as callDuration union
               select 1001 as programid,  59 as callDuration union
               select 1001 as programid,  60 as callDuration union
               select 1001 as programid, 119 as callDuration union
               select 1001 as programid, 120 as callDuration union
               select 1001 as programid, 179 as callDuration union
               select 1001 as programid, 180 as callDuration union
               select 1001 as programid, 239 as callDuration union
               select 1001 as programid, 240 as callDuration union
               select 1001 as programid, 299 as callDuration union
               select 1001 as programid, 300 as callDuration union
               select 1001 as programid, 359 as callDuration union
               select 1001 as programid, 360 as callDuration union
               select 1001 as programid, 419 as callDuration union
               select 1001 as programid, 420 as callDuration union
               select 1001 as programid, 479 as callDuration union
               select 1001 as programid, 480 as callDuration union
               select 1001 as programid, 539 as callDuration union
               select 1001 as programid, 540 as callDuration union
               select 1001 as programid, 599 as callDuration union
               select 1001 as programid, 600 as callDuration union
               select 1001 as programid,1000 as callDuration
              ) as callmetatbl
            where
              programid = 1001
            and 
              callDuration > 0
        ) as foo
        group by
          (fixedDuration / divisor) + adder
        

        SQL 输出如下所示,每个持续时间(存储桶)从 1 到 12 计算 2 条记录。

        total  duration
        2             1
        2             2
        2             3
        2             4
        2             5
        2             6
        2             7
        2             8
        2             9
        2            10
        2            11
        2            12
        

        以下是“foo”子查询的结果:

        divisor adder   fixedDuration  callDuration
        120         1               1             1
        120         1              29            29
        120         2              30            30
        120         2              59            59
        60          2              60            60
        60          2             119           119
        60          2             120           120
        60          2             179           179
        60          2             180           180
        60          2             239           239
        60          2             240           240
        60          2             299           299
        60          2             300           300
        60          2             359           359
        60          2             360           360
        60          2             419           419
        60          2             420           420
        60          2             479           479
        60          2             480           480
        60          2             539           539
        60          2             540           540
        60          2             599           599
        60          2             600           600
        60          2             600          1000
        

        干杯。

        【讨论】:

          【解决方案10】:

          这里的用户定义函数有什么问题?您既可以直观地清理代码,也可以通过这种方式集中功能。在性能方面,除非您在所述 UDF 中做一些真正延迟的事情,否则我看不出命中太可怕了。

          【讨论】:

          • 用户定义的函数没有任何“错误”。与等效的内联表达式相比,存在一些性能开销,并且它确实添加了对另一个数据库对象的依赖,并且它有效地“隐藏”了语句中的逻辑。这不是错误的问题,而是权衡的问题,以及使用用户定义函数的好处是否大于缺点。
          • 明白了。我认为从架构的角度来看,将这个(显然是共享的)功能包装在函数中很有意义。提供的示例似乎表明他们需要将呼叫持续时间映射到特定整数,并且可能需要始终使用相同的逻辑。
          【解决方案11】:

          duration创建一个查找表
          使用查找表也会加快 SELECT 语句的速度。

          这是查找表的最终结果。

          select  count(a.callid), b.ID as duration
          from    callmetatbl a
                  inner join DurationMap b 
                   on a.callDuration >= b.Minimum
                  and a.callDuration < IsNUll(b.Maximum, a.CallDuration + 1)
          group by  b.ID
          

          这是查找表。

          create table DurationMap (
              ID          int identity(1,1) primary key,
              Minimum     int not null,
              Maximum     int 
          )
          
          insert  DurationMap(Minimum, Maximum) select 0,30
          insert  DurationMap(Minimum, Maximum) select 30,60
          insert  DurationMap(Minimum, Maximum) select 60,120
          insert  DurationMap(Minimum, Maximum) select 120,180
          insert  DurationMap(Minimum, Maximum) select 180,240
          insert  DurationMap(Minimum, Maximum) select 240,300
          insert  DurationMap(Minimum, Maximum) select 300,360
          insert  DurationMap(Minimum, Maximum) select 360,420
          insert  DurationMap(Minimum, Maximum) select 420,480
          insert  DurationMap(Minimum, Maximum) select 480,540
          insert  DurationMap(Minimum, Maximum) select 540,600
          insert  DurationMap(Minimum) select 600
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多