Trouver un nombre unique de jours

11

Je souhaite écrire une requête SQL pour trouver le nombre de jours ouvrables uniques pour chaque employé de la table 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' |
*---------------------------------------*

Production attendue:

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

J'ai écrit la requête sqlfiddle qui me donne la expectedsortie mais par curiosité existe-t-il une meilleure façon d'écrire cette requête? Puis-je utiliser la table Calendrier ou 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

Toute suggestion sera appréciée.

zélé
la source
pour les valeurs (1, 1, 'monday', 'wednesday'),(1, 2, 'monday', 'tuesday'),(1, 3, 'monday', 'tuesday');empid_1 a fonctionné 3 jours distincts (lundi, mardi, mercredi), le violon / requête renvoie 4
lptr
1
@lptr c'est (1, 1, 'monday', 'wednesday'),(1, 2, 'monday', 'tuesday'),(1, 3, 'friday', 'friday');
zélé
3
Votre requête ne fonctionne pas réellement. Si vous changez 1 2 'monday' 'tuesday'pour 1 2 'monday' 'wednesday'le résultat devrait toujours être de 4 jours mais il retourne 5
Nick

Réponses:

5

Vous devez essentiellement trouver l'intersection des jours travaillés par chacun emp_idsur chacun taskavec tous les jours de la semaine, puis compter les jours distincts:

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

Production:

emp_id  distinct_days
1       4
2       5

Démo sur SQLFiddle

pseudo
la source
Je n'ai pas vu votre réponse en écrivant la mienne. Maintenant, je vois que je rendais les choses plus compliquées que nécessaire. J'aime ta solution.
Thorsten Kettner
2
@ThorstenKettner ouais - J'ai d'abord commencé moi-même le chemin CTE récursif mais j'ai réalisé en utilisant un joinavec betweencar la condition obtient plus facilement le même résultat ...
Nick
6

Une approche possible pour simplifier l'énoncé de la question (violon) consiste à utiliser VALUESle constructeur de valeur de table et les jointures appropriées:

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

Mais si vous voulez compter les jours distincts , la déclaration est différente. Vous devez trouver tous les jours entre la start_dayet end_dayportée et compter les jours distincts:

;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
Zhorov
la source
Cette requête (comme avec la requête originale des OP) ne fonctionne pas, si vous passez 1 2 'monday' 'tuesday' au 1 2 'monday' 'wednesday' résultat devrait toujours être de 4 jours mais il retourne 5.
Nick
@ Nick, désolé, je ne comprends pas. Sur la base des explications des PO, il y a 2 jours entre mondayet wednesday. Suis-je en train de manquer quelque chose?
Zhorov
modifiez les données d'entrée comme je l'ai décrit, et votre requête renvoie 5. Cependant, la réponse doit toujours être 4 car il ne reste que 4 jours uniques travaillés.
Nick
@ Nick, maintenant je comprends votre point. Mais si je change les valeurs dans le violon OPs, le résultat sera 5non 4. Cette réponse suggère simplement une déclaration plus simple. Merci.
Zhorov
La requête OP est également erronée. La bonne réponse avec ces données est 4, car il n'y a que 4 jours uniques.
Nick
2

Votre requête n'est pas correcte. Essayez du lundi au mardi et du mercredi au jeudi. Cela devrait aboutir à 4 jours, mais votre requête renvoie 2 jours. Votre requête ne détecte même pas si deux plages sont adjacentes ou se chevauchent ou aucune.

Une façon de résoudre ce problème consiste à écrire un CTE récursif pour obtenir tous les jours d'une plage, puis compter les jours distincts.

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;

Démo: http://sqlfiddle.com/#!18/4a5ac/16

(Comme on peut le voir, je ne pouvais pas appliquer le constructeur de valeurs directement comme dans with weekdays (day_name, day_number) as (values ('monday', 1), ...). Je ne sais pas pourquoi. Est-ce SQL Server ou moi? Eh bien, avec la sélection supplémentaire, cela fonctionne :-)

Thorsten Kettner
la source
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
Rahul Gossain
la source
3
Les réponses codées uniquement peuvent presque toujours être améliorées par l'ajout d'explications sur la façon dont elles fonctionnent et pourquoi.
Jason Aller
1
Bienvenue dans Stack Overflow! Bien que ce code puisse résoudre la question, y compris une explication de comment et pourquoi cela résout le problème aiderait vraiment à améliorer la qualité de votre message, et entraînerait probablement plus de votes positifs. N'oubliez pas que vous répondrez à la question des lecteurs à l'avenir, pas seulement à la personne qui pose la question maintenant. Veuillez modifier votre réponse pour ajouter des explications et donner une indication des limitations et hypothèses applicables. De l'avis
double-bip
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;
lptr
la source
1
Vous demander d'écrire une description de votre code comment cela fonctionne?
Suraj Kumar