Semplificando (aliasing) istruzioni T-SQL CASE. Qualsiasi miglioramento possibile?

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

  •  11-09-2019
  •  | 
  •  

Domanda

Come si può vedere, questo fa schifo tempo grande. Qualsiasi alternativa? Ho provato con l'alias di colonna nella clausola group by senza alcun risultato.

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

EDIT: Ho veramente destinata a chiedere come avere una sola fonte caso, ma di casi le modifiche sono i benvenuti in ogni caso (anche se meno utile perché gli intervalli probabilmente saranno modificate e potrebbe anche essere generate automaticamente).

Come è stato considerato da alcune persone, callDuration è davvero un galleggiante in modo da alcune soluzioni elencate non sono validi per il mio caso d'uso, lasciando fuori i valori degli intervalli.

Lezioni:

  • cercare modelli nell'espressione caso per ridurre, se possibile, e 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
    
  • Utilizzare vista in linea di avere una sola fonte di 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
    
  • In alternativa, utilizzare espressioni di tabella comuni

       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
    
  • In alternativa, utilizzare una funzione definita dall'utente (nessun esempio finora :-))

  • In alternativa, utilizzare una tabella di ricerca e un join

    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
    

Grazie a tutti e sto avendo un momento molto difficile scegliere una risposta accettata, come molti coperti diverse parti della domanda (e io ero lì pensando che fosse una semplice domanda con una risposta diretta :-), mi spiace per la confusione).

È stato utile?

Soluzione

D: come ottenere un alias da utilizzare nella clausola GROUP BY

Un approccio è quello di utilizzare una vista inline. [EDIT] La risposta di Remus Ruşanu (+1!) Dà un esempio di una tabella espressione comune per ottenere la stessa cosa. [/ EDIT]

La vista in linea si ottiene una semplice "alias" per l'espressione complesso che si può quindi fare riferimento in una clausola GROUP BY in una query esterna:

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

Diamo disfare quello.

  • l'interno (rientrato) query viene chiamato e vista in linea (abbiamo dato che un d alias)
  • nella query esterna, possiamo fare riferimento al duration alias da d

Questo dovrebbe essere sufficiente per rispondere alla tua domanda. Se siete alla ricerca di un'espressione la sostituzione equivalente, quella da tekBlues ( 1 ) è la risposta giusta (funziona sul confine e per i non interi. )

Con l'espressione di ricambio dalla 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

(Questo dovrebbe essere sufficiente per rispondere alla tua domanda.)


[UPDATE:] campione funzione definita dall'utente (sostituzione di espressione linea 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)

NOTE:. tenere presente che la funzione definita dall'utente aggiungerà testa, e (ovviamente) aggiungere una dipendenza su un'altra oggetto database

Questa funzione esempio equivale all'espressione originale. L'espressione OP CASE non ha lacune, ma fa riferimento a ogni "breakpoint" due volte, preferisco testare solo il limite inferiore. (CASE restituisce quando una condizione è soddisfatta. Fare le prove in senso inverso permette caso non gestita (<= 0 o NULL) cadono attraverso senza prova, un ELSE NULL non è necessario, ma potrebbe essere aggiunto per completezza.

Dettagli aggiuntivi

(Assicuratevi di controllare le prestazioni e il piano di ottimizzazione, per assicurarsi che sia la stessa (o non significativamente peggiore) dell'originale. In passato, ho avuto problemi per ottenere predicati spinti nella vista in linea, doesn 't guardare come sarà un problema nel tuo caso).

vista memorizzato

Si noti che il in linea vista potrebbe anche essere immagazzinate come Vista la definizione nel database. Ma non c'è motivo per farlo, oltre a "nascondere" l'espressione complessa dal tuo estratto conto.

semplificando l'espressione complesso

Un altro modo per fare un'espressione complessa "semplice" è quello di utilizzare una funzione definita dall'utente. Ma una funzione definita dall'utente viene fornito con una propria serie di problemi (tra cui un decadimento delle prestazioni.)

Aggiungi database "ricerca" tabella

Alcune risposte raccomandare l'aggiunta di una tabella "di ricerca" per il database. Non vedo che questo è davvero necessario. Si potrebbe fare, naturalmente, e potrebbe avere un senso se si vuole essere in grado di ricavare diversi valori per duration da callDuration, al volo, senza dover modificare la query e senza che sia necessario eseguire istruzioni DDL (ad esempio per modificare una definizione di vista, o modificare una funzione definita dall'utente).

Con un join a una tabella di "ricerca", uno dei vantaggi è che si potrebbe fare la query restituiscono diversi insiemi di risultati semplicemente l'esecuzione di operazioni DML sul tavolo "di ricerca".

Ma lo stesso vantaggio può effettivamente essere un inconveniente pure.

Considerare attentamente se il beneficio in realtà supera il rovescio della medaglia. Prendere in considerazione l'impatto che la nuova tabella avrà il test di unità, come verificare il contenuto della tabella di ricerca sono validi e non modificati (sovrapposizioni? Eventuali lacune?), L'impatto sulla manutenzione in corso per il codice (a causa della complessità aggiuntivo).

alcune ipotesi GRANDI

Un sacco di risposte qui riportati sembrano supporre che callDuration è un tipo di dati INTEGER. Sembra che hanno trascurato la possibilità che non è un numero intero, ma forse mi mancava che Nugget nel qUestion.

E 'abbastanza semplice banco di prova per dimostrare che:

callDuration BETWEEN 0 AND 30

non equivalente a

callDuration > 0 AND callDuration < 30

Altri suggerimenti

C'è qualche motivo non lo si utilizza between? Le istruzioni case stesse non sembrano troppo male. Se davvero odio si può buttare tutto questo in una tabella e la mappa.

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

ecc ...

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

EDIT:. In alternativa, nel caso in cui i galleggianti vengono utilizzati e between diventa ingombrante

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

il caso può essere scritto in questo modo:

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

L'avere non è necessario, sostituirlo con un "dove callduration> 0"

Mi piace il tavolo risposta tradurre dato prima! questa è la soluzione migliore

È necessario spingere il CASE più in basso l'albero query in modo che la sua proiezione è visibile a GROUP BY. Questo può essere raggiungere in due modi:

  1. Utilizzare una tabella derivata (già Spencer, Adam e Jeremy ha mostrato come)
  2. utilizzare una tabella comune espressioni

    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
    

Entrambe le soluzioni sono equivalenti sotto ogni aspetto. Trovo CTE più leggibile, alcuni preferiscono tabelle derivate come più portabile.

Divide callDuration del 60:

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

Si noti che between è comprensivo dei limiti, e sto assumendo callDuration verrà trattato come un intero.


Aggiornamento:
Combinate questo con alcune delle altre risposte, e si può ottenere l'intera query riduce a questo:

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

Non testato:

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

Aggiungi tutti i casi in una variabile di tabella e fare un outer join

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

Ecco il mio colpo a esso. Tutti i componenti necessari può essere fatto in SQL dritto.

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

Ecco il codice SQL che ho usato per il test. (non ho la mia callmetatbl personali;)

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

L'output SQL è mostrato di seguito, come 2 record contate per ogni durata (bucket) da 1 a 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

Ecco i risultati del "foo" sub-query:

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

Saluti.

Cosa c'è di sbagliato in un User Defined Function qui? Si potrebbe sia visivamente ripulire il codice e centralizzare le funzionalità in quel modo. Performance-saggio, non riesco a vedere il colpo di essere troppo orribile a meno che non si sta facendo qualcosa di veramente ritardato entro detto UDF.

Crea una tabella di ricerca per duration
Utilizzando una tabella di guardare in alto si accelera la dichiarazione SELECT pure.

Ecco il risultato finale di come apparirà con tabella di ricerca.

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

Ecco la tabella di ricerca.

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
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top