Encuentra un número único de días

11

Deseo escribir una consulta SQL para encontrar el número de días hábiles únicos para cada empleado de la tabla 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' |
*---------------------------------------*

Rendimiento esperado:

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

He escrito la consulta sqlfiddle que me está dando el expectedresultado, pero por curiosidad, ¿hay una mejor manera de escribir esta consulta? ¿Puedo usar el calendario o la tabla de conteo?

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

Cualquier sugerencia seria genial.

celoso
fuente
para valores (1, 1, 'monday', 'wednesday'),(1, 2, 'monday', 'tuesday'),(1, 3, 'monday', 'tuesday');empid_1 ha trabajado 3 días distintos (lunes, martes, miércoles), el violín / consulta devuelve 4
lptr
1
@lptr es (1, 1, 'monday', 'wednesday'),(1, 2, 'monday', 'tuesday'),(1, 3, 'friday', 'friday');
celoso
3
Su consulta en realidad no funciona. Si cambia 1 2 'monday' 'tuesday'al 1 2 'monday' 'wednesday'resultado, aún debe ser de 4 días, pero devuelve 5
Nick

Respuestas:

5

Básicamente, necesita encontrar la intersección de los días trabajados por cada uno emp_iden cada uno taskcon todos los días de la semana, y luego contar los días distintos:

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

Salida:

emp_id  distinct_days
1       4
2       5

Demostración en SQLFiddle

Mella
fuente
No vi tu respuesta al escribir la mía. Ahora veo que estaba haciendo las cosas más complicadas de lo necesario. Me gusta tu solución
Thorsten Kettner
2
@ThorstenKettner sí - Al principio me empecé a bajar por el camino CTE recursiva mí mismo, pero utilizando una cuenta de joincon betweenla condición logra el mismo resultado con mayor facilidad ...
Nick
6

Un posible enfoque para simplificar el enunciado en la pregunta (violín) es utilizar el VALUESconstructor de valores de tabla y las uniones apropiadas:

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

Pero si desea contar los días distintos , la declaración es diferente. Es necesario encontrar todos los días entre el start_dayy el end_dayrango y contar los días distintos:

;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
fuente
Esta consulta (al igual que con la consulta original de OP) no funciona, si cambia 1 2 'monday' 'tuesday' al 1 2 'monday' 'wednesday' resultado aún debe ser de 4 días, pero devuelve 5.
Nick
@ Nick, lo siento, no puedo entender. Según las explicaciones de los OP, hay 2 días entre mondayy wednesday. ¿Me estoy perdiendo de algo?
Zhorov
cambie los datos de entrada como lo describí, y su consulta devuelve 5. Sin embargo, la respuesta aún debería ser 4 ya que solo hay 4 días únicos trabajados.
Nick
@ Nick, ahora entiendo tu punto. Pero si cambio los valores en el violín de los OP, el resultado será 5, no 4. Esta respuesta solo sugiere una declaración más simple. Gracias.
Zhorov
La consulta de OP también está mal. La respuesta correcta con esos datos es 4, ya que solo hay 4 días únicos.
Nick
2

Su consulta no es correcta Intente de lunes a martes con miércoles a jueves. Esto debería resultar en 4 días, pero su consulta devuelve 2 días. Su consulta ni siquiera detecta si dos rangos son adyacentes o se superponen o tampoco.

Una forma de resolver esto es escribir un CTE recursivo para obtener todos los días de un rango y luego contar días distintos.

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;

Demostración: http://sqlfiddle.com/#!18/4a5ac/16

(Como se puede ver, no pude aplicar el constructor de valores directamente como en with weekdays (day_name, day_number) as (values ('monday', 1), ...). No sé por qué. ¿Ese es SQL Server o yo? Bueno, con la selección adicional funciona :-)

Thorsten Kettner
fuente
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
fuente
3
Las respuestas de solo código casi siempre se pueden mejorar mediante la adición de alguna explicación de cómo y por qué funcionan.
Jason Aller
1
¡Bienvenido a Stack Overflow! Si bien este código puede resolver la pregunta, incluir una explicación de cómo y por qué esto resuelve el problema realmente ayudaría a mejorar la calidad de su publicación, y probablemente resultaría en más votos positivos. Recuerde que está respondiendo la pregunta para los lectores en el futuro, no solo la persona que pregunta ahora. Por favor, editar su respuesta para agregar explicaciones y dar una indicación de lo que se aplican limitaciones y supuestos. De la opinión
doble pitido
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
fuente
1
¿Le solicito que escriba una descripción de su código cómo funciona?
Suraj Kumar