Simplificación (aliasing) de declaraciones CASE de T-SQL.¿Alguna mejora posible?

StackOverflow https://stackoverflow.com/questions/951836

  •  11-09-2019
  •  | 
  •  

Pregunta

Como puede ver, esto apesta mucho.¿Alguna alternativa?Intenté usar el alias de columna en la cláusula group by sin éxito.

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

EDITAR:Realmente quise preguntar cómo tener una única fuente de caso, pero las modificaciones de caso son bienvenidas de todos modos (aunque menos útiles porque los intervalos probablemente se modificarán e incluso podrían generarse automáticamente).

Como algunas personas han considerado, callDuration es de hecho un valor flotante, por lo que algunas soluciones enumeradas no son válidas para mi caso de uso, al dejar valores fuera de los intervalos.

Lecciones:

  • Busque patrones en la expresión case para reducirla si es posible y vale la pena.

     case
        when callDuration > 0 AND callDuration < 30 then 1
        when callDuration > 600 then 12
        else floor(callDuration/60) + 2  end
     end as duration
    
  • Utilice vistas en línea para tener una única fuente del caso

    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
    
  • O use expresiones de tabla comunes

       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
    
  • O utilice una función definida por el usuario (no hay ningún ejemplo hasta ahora :-))

  • O utilice una tabla de búsqueda y una unión

    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
    

Gracias a todos y me está costando mucho elegir una respuesta aceptada, ya que muchas cubrían diferentes partes de la pregunta (y yo estaba allí pensando que era una pregunta simple con una respuesta directa :-), perdón por la confusión).

¿Fue útil?

Solución

Q: cómo conseguir un alias para utilizar en la cláusula GROUP BY

Un enfoque consiste en utilizar una vista en línea. [EDIT] La respuesta de Remus Ruşanu (1!) Da un ejemplo de una expresión de tabla común para lograr la misma cosa. [/ EDIT]

La vista en línea que usted consigue un simple "alias" para el complejo de expresión que luego se puede hacer referencia en una cláusula GROUP BY en una consulta externa:

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

Vamos a descomprimir eso.

  • el (sangría) consulta interna y se llama Ver incluido (que se da una d alias)
  • en la consulta externa, podemos hacer referencia a la duration alias de d

Esto debería ser suficiente para responder a su pregunta. Si lo que buscas es una expresión de reemplazo equivalente, el de tekBlues ( 1 ) es la respuesta correcta (funciona en el límite y para los no enteros. )

Con la expresión de reemplazo de 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

(Esto debería ser suficiente para responder a su pregunta.)


[UPDATE:] muestra usuario función definida (un reemplazo para expresión en línea 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)

NOTAS:. tenga en cuenta que el usuario función definida añadirá gastos generales, y (por supuesto) agregar una dependencia en otro objeto de base de datos

Esta función ejemplo es equivalente a la expresión original. La expresión OP caso no tiene ningún lagunas, pero hace referencia a cada "punto de ruptura" dos veces, prefiero probar sólo el límite inferior. (CASE devuelve cuando se satisface una condición. Hacer las pruebas en sentido inverso permite que el caso no controlada (<= 0 o NULL) caiga a través sin prueba, un ELSE NULL no es necesario, pero podría ser añadido para la integridad.

DETALLES ADICIONALES

(Asegúrese de revisar el desempeño y el plan optimizador, para asegurarse de que es lo mismo que (o no significativamente peor que) el original. En el pasado, he tenido problemas para conseguir predicados empujados a la vista en línea, doesn 'mirada de t que va a ser un problema en su caso.)

vista almacenado

Tenga en cuenta que el línea vista podría también ser almacenados como definición de la vista en la base de datos. Pero no hay razón para hacerlo, aparte de "ocultar" el complejo de expresión a partir de su estado de cuenta.

simplificar el complejo de expresión

Otra forma de hacer una expresión compleja "simple" es el uso de una función definida por el usuario. Pero una función definida por el usuario viene con su propio conjunto de problemas (incluyendo el rendimiento sea menor.)

añadir la base de datos de "búsqueda" tabla

Algunas respuestas recomienda añadir una tabla de "búsqueda" de la base de datos. No veo que esto es realmente necesario. Se podría hacer, por supuesto, y podría tener sentido si usted quiere ser capaz de derivar valores diferentes para duration de callDuration, sobre la marcha, sin tener que modificar su consulta y sin tener que ejecutar cualesquiera instrucciones DDL (por ejemplo, para alterar una definición de la vista, o modificar un usuario función definida).

Con una combinación de una tabla de "búsqueda", una de las ventajas es que se puede realizar la consulta devuelven diferentes conjuntos de resultados con sólo realizar operaciones de DML en la tabla de "búsqueda".

Pero esa misma ventaja puede ser en realidad un inconveniente también.

Considere cuidadosamente si el beneficio es mayor que en realidad la baja. Considere el impacto que la nueva tabla tendrá en la unidad de pruebas, cómo verificar el contenido de la tabla de búsqueda son válidos y no ha cambiado (solapamientos? Alguna falla?), Impacto en el mantenimiento continuo al código (debido a la complejidad adicional).

algunos supuestos BIG

Muchas de las respuestas dadas aquí parecen asumir que callDuration es un tipo de datos entero. Parece que han pasado por alto la posibilidad de que no es un entero, pero tal vez se perdió esa pepita en el question.

Es bastante simple caso de prueba para demostrar que:

callDuration BETWEEN 0 AND 30

es no equivalente a

callDuration > 0 AND callDuration < 30

Otros consejos

¿Hay alguna razón usted no está usando between? Las declaraciones de casos ellos mismos no se ven muy mal. Si realmente odias que podría lanzar todo esto en una mesa y asignarla.

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

etc ...

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

EDIT:. O bien, en un caso en el que los flotadores están siendo utilizados y between vuelve muy complicada

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

el caso se puede escribir así:

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

No es necesario, sustituirlo por el tener una "Duración de la llamada donde> 0"

Me gusta la mesa respuesta traducir dado antes! esa es la mejor solución

Es necesario empujar el CASO más abajo en el árbol de consulta para que su proyección es visible para el GROUP BY. Esto se puede lograr de dos maneras:

  1. Utilizar una tabla derivada (ya Spencer, Adán y Jeremy mostraron cómo)
  2. utilizar una tabla común expresiones

    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
    

Las dos soluciones son equivalentes en todos los aspectos. Encuentro CTE más fácil de leer, algunos prefieren las tablas derivadas como más portátil.

Divide callDuration por 60:

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

Tenga en cuenta que between es inclusivo de los límites, y estoy asumiendo Duración de la llamada será tratado como un entero.


Actualización:
Combine esto con algunas de las otras respuestas, y se puede obtener toda la consulta reduce a esto:

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
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

No comprobado:

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

Añadir todos los casos en una variable de tabla y hacer una combinación externa

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

Aquí está mi oportunidad.Todos los componentes que necesita se pueden realizar en SQL directo.

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

Aquí está el SQL que utilicé para las pruebas. (No tengo mi propio callmetatbl personal;)

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

La salida de SQL se muestra a continuación, como 2 registros contados para cada duración (depósito) del 1 al 12.

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

Estos son los resultados de la subconsulta "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

Salud.

¿Qué tiene de malo aquí una función definida por el usuario?Podrías limpiar visualmente el código y centralizar la funcionalidad de esa manera.En cuanto al rendimiento, no veo que el golpe sea demasiado horrible a menos que estés haciendo algo realmente retrasado dentro de dicha UDF.

Crear una tabla de consulta para duration
El uso de una tabla de consulta acelerará la declaración SELECT también.

Este es el resultado final de cómo se verá con tabla de consulta.

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

Aquí está la tabla de consulta.

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
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top