Trova un numero unico di giorni


11

Vorrei scrivere una query SQL per trovare il numero di giorni lavorativi unici per ciascun dipendente dalla tabella times.

*---------------------------------------*
|emp_id  task_id  start_day   end_day   |
*---------------------------------------*
|  1        1     'monday'  'wednesday' |
|  1        2     'monday'  'tuesday'   |
|  1        3     'friday'  'friday'    |
|  2        1     'monday'  'friday'    |
|  2        1     'tuesday' 'wednesday' |
*---------------------------------------*

Uscita prevista:

*-------------------*
|emp_id  no_of_days |
*-------------------*
|  1        4       |
|  2        5       |
*-------------------*

Ho scritto la query sqlfiddle che mi sta dando l' expectedoutput ma per curiosità c'è un modo migliore per scrivere questa query? Posso usare la tabella Calendario o Tally?

with days_num as  
(
  select
    *,
    case 
      when start_day = 'monday' then 1
      when start_day = 'tuesday' then 2
      when start_day = 'wednesday' then 3
      when start_day = 'thursday' then 4
      when start_day = 'friday' then 5
    end as start_day_num,

    case 
      when end_day = 'monday' then 1
      when end_day = 'tuesday' then 2
      when end_day = 'wednesday' then 3
      when end_day = 'thursday' then 4
      when end_day = 'friday' then 5
    end as end_day_num

  from times
),
day_diff as
(
  select
    emp_id,
    case
      when  
        (end_day_num - start_day_num) = 0
      then
        1
      else
        (end_day_num - start_day_num)
    end as total_diff
  from days_num  
)

select emp_id,
  sum(total_diff) as uniq_working_days
from day_diff
group by
  emp_id

Qualsiasi suggerimento sarebbe molto apprezzato.


per i valori (1, 1, 'monday', 'wednesday'),(1, 2, 'monday', 'tuesday'),(1, 3, 'monday', 'tuesday');empid_1 ha funzionato 3 giorni distinti (lunedì, martedì, mercoledì), il violino / query restituisce 4
lptr

1
@lptr lo è (1, 1, 'monday', 'wednesday'),(1, 2, 'monday', 'tuesday'),(1, 3, 'friday', 'friday');
zelante

3
La tua query in realtà non funziona. Se si 1 2 'monday' 'tuesday'passa 1 2 'monday' 'wednesday'al risultato dovrebbe essere ancora 4 giorni ma restituisce 5
Nick

Risposte:


5

Devi fondamentalmente trovare l'intersezione dei giorni lavorati da ognuno emp_idsu ciascuno taskcon tutti i giorni della settimana, quindi contare i giorni distinti:

with days_num as (
  SELECT *
  FROM (
    VALUES ('monday', 1), ('tuesday', 2), ('wednesday', 3), ('thursday', 4), ('friday', 5)
  ) AS d (day, day_no)
),
emp_day_nums as (
  select emp_id, d1.day_no AS start_day_no, d2.day_no AS end_day_no
  from times t
  join days_num d1 on d1.day = t.start_day
  join days_num d2 on d2.day = t.end_day
)
select emp_id, count(distinct d.day_no) AS distinct_days
from emp_day_nums e
join days_num d on d.day_no between e.start_day_no and e.end_day_no
group by emp_id

Produzione:

emp_id  distinct_days
1       4
2       5

Demo su SQLFiddle


Non ho visto la tua risposta mentre scrivevo la mia. Ora vedo che stavo rendendo le cose più complicate del necessario. Mi piace la tua soluzione.
Thorsten Kettner il

2
@ThorstenKettner sì - Inizialmente ho iniziato da solo il percorso ricorsivo del CTE ma mi sono reso conto di usare un joincon betweenpoiché la condizione raggiunge lo stesso risultato più facilmente ...
Nick

6

Un possibile approccio per semplificare l'istruzione nella domanda (violino) è utilizzare il VALUEScostruttore del valore di tabella e i join appropriati:

SELECT 
   t.emp_id,
   SUM(CASE 
      WHEN d1.day_no = d2.day_no THEN 1
      ELSE d2.day_no - d1.day_no
   END) AS no_of_days
FROM times t
JOIN (VALUES ('monday', 1), ('tuesday', 2), ('wednesday', 3), ('thursday', 4), ('friday', 5)) d1 (day, day_no) 
   ON t.start_day = d1.day
JOIN (VALUES ('monday', 1), ('tuesday', 2), ('wednesday', 3), ('thursday', 4), ('friday', 5)) d2 (day, day_no) 
   ON t.end_day = d2.day
GROUP BY t.emp_id

Ma se vuoi contare i giorni distinti , l'affermazione è diversa. Hai bisogno di trovare tutti i giorni tra la start_daye end_daygamma e contare i giorni distinti:

;WITH daysCTE (day, day_no) AS (
   SELECT 'monday', 1 UNION ALL
   SELECT 'tuesday', 2 UNION ALL
   SELECT 'wednesday', 3 UNION ALL
   SELECT 'thursday', 4 UNION ALL
   SELECT 'friday', 5 
)
SELECT t.emp_id, COUNT(DISTINCT d3.day_no)
FROM times t
JOIN daysCTE d1 ON t.start_day = d1.day
JOIN daysCTE d2 ON t.end_day = d2.day
JOIN daysCTE d3 ON d3.day_no BETWEEN d1.day_no AND d2.day_no
GROUP BY t.emp_id

Questa query (come con PO query originale) non funziona, se si cambia 1 2 'monday' 'tuesday' per 1 2 'monday' 'wednesday' il risultato dovrebbe essere ancora 4 giorni ma restituisce 5.
Nick

@Nick, scusa, non riesco a capire. Sulla base delle spiegazioni dei PO, ci sono 2 giorni tra mondaye wednesday. Mi sto perdendo qualcosa?
Zhorov,

modifica i dati di input come ho descritto e la tua query restituisce 5. Tuttavia, la risposta dovrebbe essere ancora 4 in quanto sono stati ancora lavorati solo 4 giorni unici.
Nick

@Nick, ora capisco il tuo punto. Ma se cambio i valori nel violino dei PO, il risultato sarà 5no 4. Questa risposta suggerisce solo un'affermazione più semplice. Grazie.
Zhorov,

Anche la query dei PO è sbagliata. La risposta corretta con tali dati è 4, in quanto vi sono solo 4 giorni unici.
Nick

2

La tua query non è corretta. Prova da lunedì a martedì con mercoledì a giovedì. Ciò dovrebbe comportare 4 giorni, ma la query restituisce 2 giorni. La tua query non rileva nemmeno se due intervalli sono adiacenti o sovrapposti o nessuno dei due.

Un modo per risolverlo è scrivere un CTE ricorsivo per ottenere tutti i giorni da un intervallo e quindi contare giorni distinti.

with weekdays (day_name, day_number) as
(
  select * from (values ('monday', 1), ('tuesday', 2), ('wednesday', 3),
                        ('thursday', 4), ('friday', 5)) as t(x,y)
)
, emp_days(emp_id, day, last_day)
as
(
  select emp_id, wds.day_number, wde.day_number
  from times t
  join weekdays wds on wds.day_name = t.start_day
  join weekdays wde on wde.day_name = t.end_day
  union all
  select emp_id, day + 1, last_day
  from emp_days
  where day < last_day
)
select emp_id, count(distinct day)
from emp_days
group by emp_id
order by emp_id;

Demo: http://sqlfiddle.com/#!18/4a5ac/16

(Come si può vedere, non ho potuto applicare il costruttore di valori direttamente come in with weekdays (day_name, day_number) as (values ('monday', 1), ...). Non so perché. È SQL Server o me? Bene, con la selezione aggiuntiva funziona :-)


2
with cte as 
(Select id, start_day as day
   group by id, start_day
 union 
 Select id, end_day as day
   group by id, end_day
)

select id, count(day)
from cte
group by id

3
Le risposte solo al codice possono quasi sempre essere migliorate aggiungendo alcune spiegazioni su come e perché funzionano.
Jason Aller

1
Benvenuto in Stack Overflow! Sebbene questo codice possa risolvere la domanda, inclusa una spiegazione di come e perché questo risolva il problema, contribuirebbe davvero a migliorare la qualità del tuo post e probabilmente darebbe più voti positivi. Ricorda che stai rispondendo alla domanda per i lettori in futuro, non solo per la persona che chiede ora. Si prega di modificare la risposta per aggiungere spiegazioni e dare un'indicazione di ciò si applicano le limitazioni e le assunzioni. Dalla recensione
Doppio segnale acustico

1
declare @times table
(
  emp_id int,
  task_id int,
  start_day varchar(50),
  end_day varchar(50)
);

insert into @times(emp_id, task_id, start_day, end_day)
values
(1, 1, 'monday', 'wednesday'),
(1, 2, 'monday', 'tuesday'),
(1, 3, 'friday', 'friday'),
--
(2, 1, 'monday', 'friday'),
(2, 2, 'tuesday', 'wednesday'),
--
(3, 1, 'monday', 'wednesday'),
(3, 2, 'monday', 'tuesday'),
(3, 3, 'monday', 'tuesday');

--for sql 2019, APPROX_COUNT_DISTINCT() eliminates distinct sort (!!)...
-- ...with a clustered index on emp_id (to eliminate the hashed aggregation) the query cost gets 5 times cheaper ("overlooking" the increase in memory) !!??!!
/*
select t.emp_id, APPROX_COUNT_DISTINCT(v.val) as distinctweekdays
from
(
select *, .........
*/


select t.emp_id, count(distinct v.val) as distinctweekdays
from
(
select *, 
case start_day when 'monday' then 1
      when 'tuesday' then 2
      when 'wednesday' then 3
      when 'thursday' then 4
      when 'friday' then 5
    end as start_day_num,
case end_day when 'monday' then 1
      when 'tuesday' then 2
      when 'wednesday' then 3
      when 'thursday' then 4
      when 'friday' then 5
    end as end_day_num
from @times
) as t
join (values(1),(2), (3), (4), (5)) v(val) on v.val between t.start_day_num and t.end_day_num
group by t.emp_id;

1
Ti chiede di scrivere una descrizione del tuo codice come funziona?
Suraj Kumar
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.