Finden Sie eine eindeutige Anzahl von Tagen

11

Ich möchte eine SQL-Abfrage schreiben, um die Anzahl der eindeutigen Arbeitstage für jeden Mitarbeiter aus der Tabelle zu ermitteln 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' |
*---------------------------------------*

Erwartete Ausgabe:

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

Ich habe die Abfrage sqlfiddle geschrieben, die mir die expectedAusgabe gibt, aber aus Neugier gibt es eine bessere Möglichkeit, diese Abfrage zu schreiben? Kann ich eine Kalender- oder Tally-Tabelle verwenden?

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

Irgendwelche Vorschläge wären toll.

eifrig
quelle
Für Werte (1, 1, 'monday', 'wednesday'),(1, 2, 'monday', 'tuesday'),(1, 3, 'monday', 'tuesday');empid_1 hat 3 verschiedene Tage gearbeitet (Montag, Dienstag, Mittwoch), die Geige / Abfrage gibt 4
lptr
1
@lptr es ist (1, 1, 'monday', 'wednesday'),(1, 2, 'monday', 'tuesday'),(1, 3, 'friday', 'friday');
eifrig
3
Ihre Abfrage funktioniert nicht wirklich. Wenn Sie 1 2 'monday' 'tuesday'zum 1 2 'monday' 'wednesday'Ergebnis wechseln , sollten es noch 4 Tage sein, aber es gibt 5 zurück
Nick

Antworten:

5

Sie müssen im Grunde den Schnittpunkt der von jedem emp_idan jedem taskTag mit allen Wochentagen bearbeiteten Tage finden und dann die verschiedenen Tage zählen:

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

Ausgabe:

emp_id  distinct_days
1       4
2       5

Demo auf SQLFiddle

Nick
quelle
Ich habe deine Antwort nicht gesehen, als ich meine geschrieben habe. Jetzt sehe ich, dass ich die Dinge komplizierter als nötig gemacht habe. Ich mag deine Lösung.
Thorsten Kettner
2
@ThorstenKettner Ja - Ich habe zunächst selbst den rekursiven CTE-Pfad eingeschlagen, aber festgestellt, dass die Verwendung von a joinmit betweenals Bedingung das gleiche Ergebnis leichter erzielt ...
Nick
6

Ein möglicher Ansatz zur Vereinfachung der Aussage in der Frage (Geige) ist die Verwendung VALUES Tabellenwertkonstruktor und geeignete Verknüpfungen zu verwenden:

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

Wenn Sie jedoch die verschiedenen Tage zählen möchten , ist die Aussage anders. Sie müssen alle Tage zwischen den finden start_dayund end_dayBereich und die unterschiedlichen Tage zählen:

;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
quelle
Diese Abfrage (wie bei der ursprünglichen Abfrage des OP) funktioniert nicht, wenn Sie 1 2 'monday' 'tuesday' zum 1 2 'monday' 'wednesday' Ergebnis wechseln , sollte es noch 4 Tage dauern, aber es wird 5 zurückgegeben.
Nick
@ Nick, sorry, ich kann nicht verstehen. Basierend auf den Erklärungen der OP liegen zwischen mondayund 2 Tage wednesday. Vermisse ich etwas
Zhorov
Ändern Sie die Eingabedaten wie beschrieben, und Ihre Abfrage gibt 5 zurück. Die Antwort sollte jedoch immer noch 4 sein, da nur noch 4 eindeutige Arbeitstage vorhanden sind.
Nick
@ Nick, jetzt verstehe ich deinen Punkt. Aber wenn ich die Werte in der OP-Geige ändere, ist das Ergebnis 5nicht 4. Diese Antwort deutet nur auf eine einfachere Aussage hin. Vielen Dank.
Zhorov
Die OP-Abfrage ist ebenfalls falsch. Die richtige Antwort mit diesen Daten ist 4, da es nur 4 eindeutige Tage gibt.
Nick
2

Ihre Anfrage ist nicht korrekt. Versuchen Sie es von Montag bis Dienstag mit Mittwoch bis Donnerstag. Dies sollte 4 Tage dauern, aber Ihre Anfrage gibt 2 Tage zurück. Ihre Abfrage erkennt nicht einmal, ob zwei Bereiche benachbart oder überlappend sind oder keiner.

Eine Möglichkeit, dies zu lösen, besteht darin, einen rekursiven CTE zu schreiben, um alle Tage aus einem Bereich abzurufen und dann verschiedene Tage zu zählen.

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

(Wie zu sehen ist, konnte ich den Wertekonstruktor nicht direkt wie in anwenden with weekdays (day_name, day_number) as (values ('monday', 1), ...). Ich weiß nicht warum. Ist das SQL Server oder ich? Nun, mit der zusätzlichen Auswahl funktioniert es :-)

Thorsten Kettner
quelle
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
quelle
3
Nur-Code-Antworten können fast immer verbessert werden, indem erklärt wird, wie und warum sie funktionieren.
Jason Aller
1
Willkommen bei Stack Overflow! Während dieser Code die Frage lösen kann, einschließlich einer Erklärung, wie und warum dies das Problem löst, würde dies wirklich dazu beitragen, die Qualität Ihres Beitrags zu verbessern, und wahrscheinlich zu mehr Up-Votes führen. Denken Sie daran, dass Sie in Zukunft die Frage für die Leser beantworten, nicht nur für die Person, die jetzt fragt. Bitte bearbeiten Sie Ihre Antwort, um Erklärungen hinzuzufügen und anzugeben, welche Einschränkungen und Annahmen gelten. Aus der Überprüfung
doppelter Piepton
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
quelle
1
Fordern Sie Sie auf, eine Beschreibung Ihres Codes zu schreiben, wie er funktioniert?
Suraj Kumar