0

Essentially I'm trying to calculate the total amount of hours a user can have based on their shift and number of days entered.

Example: Start shift on 13th September 2020. I know this is Week 1, Sunday (based on other calculations). So I need to take the 2 from Sunday and then 3, 2, 1, 1, 1, 2 from week 2 in the ROTA table.

In total 7 days: Week 1 = Sunday Week 2 = Mon, Tues, Wed, Thurs, Fri, Sat

 ROTA table
+------+-----+-----+-----+-----+-----+-----+-----+
| WEEK | MON | TUE | WED | THU | FRI | SAT | SUN |
+------+-----+-----+-----+-----+-----+-----+-----+
|    1 |   2 |   2 |   2 |   2 |   2 |   2 |   2 |
|    2 |   3 |   2 |   1 |   1 |   1 |   2 |   1 |
|    3 |   1 |   2 |   1 |   1 |   1 |   1 |   1 |
|    4 |   1 |   1 |   1 |   1 |   2 |   1 |   1 |
|    5 |   1 |   1 |   2 |   1 |   1 |   1 |   1 |
+------+-----+-----+-----+-----+-----+-----+-----+

The numbers above are stored in a Shifts table. So for my 7 days it would be 2, 3, 2, 1, 1, 1, 2 = 51.5 total hours for the 7 days.

SHIFTS Table
+-------+-------+
| SHIFT | HOURS |
+-------+-------+
|     1 | 8.5   |
|     2 | 6     |
|     3 | 8     |
+-------+-------+

I'm doing a WHILE loop to get the Week and columns needed. So for the example above I would only need the SUN column from ROTA table. Next loop will give me MON - SAT.

At first I was trying to merge the two rows together, then I could do some sort of count. So there is 3x SHIFT 2, 1x SHIFT 3 and 3x SHIFT 1. Then I can get the total hours but not sure how to do that.

At the moment when my query has finished I end up with the below two rows:

LOOP 1:
+-----+
| SUN |
+-----+
|   2 |
+-----+

LOOP 2:
+-----+-----+-----+-----+-----+-----+
| MON | TUE | WED | THU | FRI | SAT |
+-----+-----+-----+-----+-----+-----+
|   3 |   2 |   1 |   1 |   1 |   2 |
+-----+-----+-----+-----+-----+-----+

I've stripped my query back a fair bit but this is the gist of it:

WHILE @cnt <= @totalDays
BEGIN

IF @dayOfWeek = 1 SET @columnList = 'SUN' ELSE 
IF @tempTotalDays >= 7 SET @columnList = 'MON, TUE, WED, THU, FRI, SAT, SUN' ELSE
IF @tempTotalDays = 6  SET @columnList = 'MON, TUE, WED, THU, FRI, SAT' ELSE
IF @tempTotalDays = 5  SET @columnList = 'MON, TUE, WED, THU, FRI' ELSE
IF @tempTotalDays = 4  SET @columnList = 'MON, TUE, WED, THU' ELSE
IF @tempTotalDays = 3  SET @columnList = 'MON, TUE, WED' ELSE
IF @tempTotalDays = 2  SET @columnList = 'MON, TUE' ELSE
IF @tempTotalDays = 1  SET @columnList = 'MON'

SET @sqlCommand = 'select '+ @columnList +' from dbo.ROTA
where WEEK = @rotaWeek'

EXEC sp_executesql @sqlCommand, N'@rotaWeek nvarchar(75), @rotaWeek = @rotaWeek

END;
GO

As you can see I'm almost there. I just don't know how to take my results and select the hours from the SHIFTS table. Any help would be appreciated.

Dereck
  • 703
  • 1
  • 7
  • 10
  • 3
    SQL is a set-based language. You don't need loops. In the worst case, you can convert all those checks to a single `CASE` clause in the `SELECT` part of a query – Panagiotis Kanavos Sep 20 '20 at 15:38
  • Thanks for the reply. If I was just using a SELECT, how would it work when I may need to use the same WEEK row multiple times? – Dereck Sep 20 '20 at 16:22
  • When would you have to consider a week multiple times? Do you mean that week #5 is followed by week #1 again and @totalDays can exceed 35 days? – Thorsten Kettner Sep 20 '20 at 21:01
  • Yes correct. There can be more than 35 days and after week #5 it goes back to week #1 – Dereck Sep 21 '20 at 00:36
  • Okay, that wasn't clear to me. I have updated my answer with a recursive query that does that. – Thorsten Kettner Sep 21 '20 at 06:16

1 Answers1

1

The ROTA table is very readable for us humans, but not so much for a DBMS that doesn't know that a week's Sunday is followed by the next week's Monday (or that we consider the values in the table adjacent values in the order mon-tue-wed-thu-fri-sat-sun at all).

You can transform the table into a readable form for the machine, giving the days numbers 1,2,3,4,5,6,7 for week #1, then 8,9,10,11,12,13,14 for week #2, etc. The formula to calculate a day number is then: day_number = day_of_week + (7 * (week - 1)).

The query:

with days as
(
  select 1 + (7 * (week - 1)) as daynum, mon as shift from rota
  union all
  select 2 + (7 * (week - 1)) as daynum, tue as shift from rota
  union all
  select 3 + (7 * (week - 1)) as daynum, wed as shift from rota
  union all
  select 4 + (7 * (week - 1)) as daynum, thu as shift from rota
  union all
  select 5 + (7 * (week - 1)) as daynum, fri as shift from rota
  union all
  select 6 + (7 * (week - 1)) as daynum, sat as shift from rota
  union all
  select 7 + (7 * (week - 1)) as daynum, sun as shift from rota
)
select sum(s.hours)
from days d
join shifts s on s.shift = d.shift
where d.daynum between @dayOfWeek + (7 * (@rotaWeek - 1))
                   and @dayOfWeek + (7 * (@rotaWeek - 1)) + @totalDays - 1;

Of course, if you changed your data model to match my ad-hoc days view, then the query would reduce to the mere last five lines of above query.

UPDATE:

In the request comments you say you want to continue week #5 with week #1 again. You can use a modulo operation to get from day #35 to day #1 (next_daynum = daynum % 35 + 1). But thus this becomes an iterative process where a ROTA week can even be used more than once in the calculation. Iterations are done with recursive queries in SQL:

with days as
(
  select 1 + (7 * (week - 1)) as daynum, mon as shift from rota
  union all
  select 2 + (7 * (week - 1)) as daynum, tue as shift from rota
  union all
  select 3 + (7 * (week - 1)) as daynum, wed as shift from rota
  union all
  select 4 + (7 * (week - 1)) as daynum, thu as shift from rota
  union all
  select 5 + (7 * (week - 1)) as daynum, fri as shift from rota
  union all
  select 6 + (7 * (week - 1)) as daynum, sat as shift from rota
  union all
  select 7 + (7 * (week - 1)) as daynum, sun as shift from rota
)
, cte (daynum, remaining, hours) as
(
  select d.daynum, @totalDays - 1, s.hours
  from days d
  join shifts s on s.shift = d.shift
  where d.daynum = @dayOfWeek + (7 * (@rotaWeek - 1))
  union all                     
  select d.daynum, cte.remaining - 1, cast(cte.hours + s.hours as decimal(5,1))
  from cte
  join days d on d.daynum = cte.daynum % 35 + 1
  join shifts s on s.shift = d.shift
  where cte.remaining >= 1
)
select max(hours)
from cte;

(Unfortunately, SQL Server requires the cast in the recursive CTE to match the column's exact data type.)

Demo: https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=351ef091ddb80acf27e209595e2d3f9e

Thorsten Kettner
  • 89,309
  • 7
  • 49
  • 73
  • Thank you for this. I will try and incorporate that into my original query to pull the exact shift hours. – Dereck Sep 21 '20 at 00:48