SQL Server Tracking Scheduled Shifts when the day varies - sql

I'm not even sure where to start in solving this one, I need to query production data from our MS SQL 2012 Db that has a datetime stamp based on the shift it was recorded in, the tricky part is that we run 4 12hr shifts in a pattern of 2 on, 2 off, 3 on, 2 off i.e. here's the first 3wks of Jan 2013:
S M T W T F S
1 2 3 4 5
C/D A/B A/B C/D C/D
6 7 8 9 10 11 12
C/D A/B A/B C/D C/D A/B A/B
13 14 15 16 17 18 19
A/B C/D C/D A/B A/B C/D C/D
A&C are morning shifts running 7:00-19:00 and B&D are night running 19:00-7:00 I'm fairly new to SQL and haven't had to do anything like this yet, I know I can easily determine the data for the 1/1/2013 AM shift with:
select *
from Line05
where L05Time BETWEEN '01/01/2013 07:00' AND '01/01/2013 19:00'
for example but I'm not sure how I can tie that to C Shift.

The following cranks out a table of shifts. It isn't exactly clear what you want to do, but you should be able to reverse engineer determining the shift from the date/time of an event using some of the calculations shown here.
EDIT: Corrected case to handle to 2/2/3/2 pattern.
; with Samples as (
-- Start at the beginning of 2013.
select Cast( '01-01-2013 00:00' as DateTime ) as Sample
union all
-- Add hours up to the desired end date.
select DateAdd( hour, 1, Sample )
from Samples
where Sample <= '2013-01-30'
),
ExtendedSamples as (
-- Calculate the number of days since the beginning of the first shift on 1/1/2013.
select Sample, DateDiff( hour, '01-01-2013 07:00', Sample ) / 24 as Days
from Samples ),
Shifts as (
-- Calculate the shifts for each day.
select *,
case when ( Days + 1 ) % 9 in ( 0, 1, 4, 5 ) then 'C/D' else 'A/B' end as Shifts
from ExtendedSamples )
select *,
case when DatePart( hour, Sample ) between 7 and 18 then Substring( Shifts, 1, 1 ) else Substring( Shifts, 3, 1 ) end as Shift
from Shifts
option ( maxrecursion 0 )

I'm sure it will get even tricker when there is a holiday or a shutdown occurs. You are thinking like a programmer, you are thinking there is an algorithm that can determine your answer. Instead, if would advise that you think like a data-guy. There should be a source of information somewhere that has the answer you seek. Ask the person who sets up the schedule if he knows. There should be a table somewere that tells you what shifts are assigned to what time slots. Use the data it contains to get your answer.

Related

How to select and group fortnightly in postgreql

I am trying to group the rows in a table fortnightly, but can't seem to work out how to do it - especially, as the date_part function does not have a 'fortnight' keyword argument.
This is what I have so far:
CREATE TABLE foo(
dt DATE NOT NULL,
f1 REAL NOT NULL,
f2 REAL NOT NULL,
f3 REAL NOT NULL,
f4 REAL NOT NULL
);
SELECT AVG((f1+f2+f3+f4)/4) as fld_avg FROM
(
SELECT date_part('year', dt) AS year_part,
date_part('fortnight', dt) AS fortnight_part,
f1, f2, f3, f4
FROM foo
WHERE dt >= date_trunc('day', NOW() - '3 month')
) foo
GROUP BY year_part, fortnight_part
How may I rewrite (or modify) the query above so as to group data fortnightly?
Basic idea
What we need to do, is take intervals of 14 consecutive days and map them to unique buckets and then group by those buckets. These buckets can of any type, int, char, timstamp, as long as we have unique value.
Division
A simple way to accomplish this is division. Divide by 14 days and truncate the result to date precision.
For example, we can extract the number of seconds since 1970-01-01, the UNIX epoch, and divide by the number of seconds in a fortnight: 14 * 24 * 60 * 60 = 14 * 86400 = 1209600. (I'll use Vao Tsun's example data)
WITH c(d) AS (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
SELECT (EXTRACT(EPOCH FROM d)::int/86400)/14 fortnight FROM c
which yields fortnights since 1970-01-01 (a Thursday):
fortnight
-----------
1251
1252
1254
1254
(4 rows)
The integer values we get, represent the number of fortnights since 1970-01-01, but we don't have to care about this. The important thing is, that it uniquely identifies a fortnight.
Due to 1970-01-01 being a Thursday, all fortnights will start at a Thursday. We might want to vary the starting point of our fortnight to a different day of the week (e.g. Monday) by adding:
WITH c(d) AS (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
SELECT (EXTRACT(EPOCH FROM d)::int/86400 + 4)/14 fortnight FROM c
By adding four days to Thursday we end up at Monday.
If you rather want fortnights with respect to the beginning of the year, instead of some arbitrary absolute date, such as 1970-01-01, we can use the day of the year instead:
WITH c(d) AS (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
SELECT EXTRACT(year FROM d) * 26 + EXTRACT(doy FROM d)::int/14 AS fortnight FROM c;
which yields
fortnight
-----------
52467
52468
52469
52470
(4 rows)
We need to multiply the extracted year by 26, because there are 26.1… fortnights in a year.
Truncation
Instead of division another approach is truncation. We map each day of a specific fortnight to the first timestamp of that fortnight.
WITH c(d) AS (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
SELECT d - make_interval(secs => EXTRACT(EPOCH FROM d)::int % (86400 * 14)) AS fortnight FROM c;
which yields
fortnight
---------------------
2017-12-14 00:00:00
2017-12-28 00:00:00
2018-01-25 00:00:00
2018-01-25 00:00:00
(4 rows)
This might seems a bit more complicated, but has some benefits. The result is still a date/time type and other code does not need to worry about the fact, that we used fortnights.
Again, instead of absolute fortnights, we can calculate this with respect to the beginning of the year:
WITH c(d) AS (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
SELECT d - make_interval(days => EXTRACT(dow FROM d)::int % 14) AS fortnight FROM c;
which yields
fortnight
---------------------
2017-12-17 00:00:00
2017-12-31 00:00:00
2018-01-21 00:00:00
2018-01-28 00:00:00
(4 rows)
The result is of type timestamp, you might want to have date instead. This can be addressed by casting:
(d - make_interval(days => EXTRACT(dow FROM d)::int % 14))::date
or subtracting int instead of interval from date:
d - (EXTRACT(dow FROM d)::int % 14)
There are much more possibilities. With this scheme, we can calculate the fortnight or any other interval with respect to the beginning of the month, some arbitrary date, etc.
update
fortnight is a two week period - one even the other odd. eg week 1 and 2, 3 and 4, 5 and 6.
closer: 2 is even, mod(2,2)=0 and 1 is odd, mod(1,2)=1
4 is even, mod(4,2)=0 and 3 is odd, mod(3,2)=1
6 is even, mod(6,2)=0 and 5 is odd, mod(5,2)=1
thus you can make an assumption that each one week's in year consecutive number divided by two reminder is 1, and each next one weeks number/2 reminders 0
The general idea is - using the sequential number of week in a year. To avoid Jan 1st to be first and Dec31 (possible be the 53rd - and thus two odds in a row), I use IW
week number of ISO 8601 week-numbering year (01-53; the first Thursday
of the year is in week 1)
then I assume that if one week number will be odd, next will be even, so we divide all the time in parts of two weeks - even+odd.
SQL Example:
o=# with c(d) as (values('2017.12.21'::date),('2017.12.31'),('2018.01.26'),('2018.02.01'))
select d,to_char(d,'IW'),right(to_char(d,'IW'),1)::int,mod(right(to_char(d,'IW'),1)::int, 2) from c;
d | to_char | right | mod
------------+---------+-------+-----
2017-12-21 | 51 | 1 | 1
2017-12-31 | 52 | 2 | 0
2018-01-26 | 04 | 4 | 0
2018-02-01 | 05 | 5 | 1
(4 rows)
mod is either 0 or 1 - group by this column
https://www.postgresql.org/docs/current/static/functions-math.html
https://www.postgresql.org/docs/current/static/functions-formatting.html
Of course you would need to add outer join on generate_series if you want data without gaps...
I post another answer to explain how I was wrong and why my "smart-n-neat"
way failed...
the schema build and queries are at:
https://www.db-fiddle.com/f/j5i2Td8CvxCVXQQYePKzCe/0
the first (and correct) query:
select distinct w2, avg(c) over (partition by w2)
from d
join generate_series('2016.11.28'::date,'2017.02.23'::date,'2 weeks'::interval) w2
on gs >= w2 and gs < w2 + '2 weeks'::interval
order by w2;
Is a long, simple and correct approach. with idea is to join on two weeks interval. It's working, reliable and all good.
Now the second query:
select distinct div(to_char(gs,'IW')::int,2), min(gs) over w, avg(c) over w
from d
window w as (partition by div(to_char(gs,'IW')::int,2))
order by min;
Is much shorter, neater and smarter, yet has a huge limitation and is unusable. Here's why:
My approach splits next to last two-weeks-interval to two parts: last week of 2016 and first week of 2017, thus dividing the result by half. If you multiply a sum of averages for those two weeks by a half, the result for both queries will match. Alas introducing CASE WHEN logic for the edge year weeks makes neat solution a heavy and overhead. And thus the very point is lost.
TL;DR the neat and lightweight solution works only on interval of one year, farther then two weeks from end or start of the year and lastly if our fortnightly interval starts from Monday.
Now the idea behind lightweight solution: round(2/2, 0)=1 and round(3/2, 0)=1, so you can divide year in intervals of two weeks and use it for grouping by.
Also I deliberately took not this New Year switch, because this 2018 Jan 1 is Monday, so IW is same as WW - which usually is not the case.
Lastly my first answer with odd and even weeks is not viable at all. It divides year not in two-weeks interval, but rather in two parts - for even and odd weeks... I deceived myself with "something close" idea and worked on the reminder, while I should do the opposite the whole value of division...

SQL splitting time when over midnight

I am after some guidance on the best way to get useful information out of our MIS database
Scenario:- I want to check staff utilisation by a variable period that I can drill down into. This needs to then be split into days so I can assess over a 24 hour period what was done
The table is huge and has loads of columns we need to calculate, so ideally I need to split the records that span 2 days into 2
The table has a datetimeformat field that has user [starttime], it then has a separate field that has [duration] which is in decimal hours.
So an example would be:
ID StartTime Duration Qty username
1 2016-11-24 23:00:00 2.00 1000 Joe Bloggs
In the example above Joe starts at 11pm and works till 1 am, so what I need is to somehow split this record in my query to put anything before midnight as 1 record and anything after into another This example is pretty simple as it is half/half but some might start at 10pm and finish at 6pm so I would need 2 hours and 6 hours.
Not sure on the best way to do this, my initial thoughts was to create a cte where a start time is in 1 day and if the starttime + duration was in the next day then split the record.
Not sure if there is an easier way or if anyone has had to do this before.
Any help appreciated
#Joe has the right idea, here is pseudo-SQL
SELECT ID,StartTime,Duration,Qty,username
WHERE TRUNCATE(StartTime,DAY) = TRUNCATE(StartTime + Duration hours ,DAY)
UNION
SELECT ID,StartTime, TRUNCATE(StartTime,DAY) + 1 days - StartTime hours ,Qty,username
WHERE TRUNCATE(StartTime,DAY) < TRUNCATE(StartTime+Duration hours,DAY)
UNION
SELECT ID,TRUNCATE(StartTime+Duration hour,DAY),StartTime + duration hours - DATE(StartTime+Duration),Qty,username
WHERE TRUNCATE(StartTime,DAY) < TRUNCATE(StartTime+Duration hours,DAY)
Where TRUNCATE(timestamp,DAY) truncates a timestamp to YYYY-MM-DD 00:00:00
You can multiply rows with join. Make Tally table, simple table with numbers 1, 2, 3... and do a join. I will use table starting at zero here:
CREATE TABLE Tally0 (Number INT IDENTITY(0,1) PRIMARY KEY NOT NULL);
GO
INSERT INTO Tally0 DEFAULT VALUES;
GO 10000
Now the harders part is conversion between dates and numerics:
;WITH
tmp1 AS (SELECT *,
DATEDIFF(SECOND, CONVERT(DATE, StartTime), StartTime)/3600.0
+ DATEPART(NANOSECOND, StartTime)/(3600*1000000000.0) AS startingHours
FROM Record),
tmp2 AS (SELECT *,
startingHours + Duration AS endingHours,
(startingHours + Duration)/24.0 AS endingDays
FROM tmp1)
SELECT *,
CASE WHEN Number = 0 THEN StartTime
ELSE DATEADD(DAY, Number, CONVERT(DATE, StartTime))
END AS StartTime2,
CASE WHEN Number = 0 AND 1 < endingDays THEN 24 - startingHours
WHEN Number = 0 THEN Duration
WHEN Number + 1 < endingDays THEN 24
ELSE endingHours - Number * 24
END AS Duration2
FROM tmp2
JOIN Tally0 ON Number < endingDays

Adding intervals as part of an SQL statement

Very long shot here as Im not sure this is possible - if it is then I definitely owe you guys.
I have a table with start_date, end_date and hours. Consider I have one line of start_date 1 Jan 2015, end_date of 31 Dec 2015 and hours as 120.
I want to produce a result of 12 lines with 10 hours in each line and the month
so something like
|Jan 15|10|
|Feb 15|10|
...
|Dec 15|10|
I want ten because I am dividing 120 hours by the number of months (12). 120 / 12 = 10
Can I do this at all? Or anything similar? I suspect I may have to do ETL in code rather than in SQL. Has anyone got any ideas on how I could anything to get closer to what I want?
Thanks
You just need a numbers tables. If you had one, you could do:
select add_months(start_date, n.n - 1),
val / (months_between(start_date, end_date) + 1)
from t join
numbers n
on add_months(start_date, n.n - 1) <= end_date;
You can create a numbers table in various ways. An easy way is something like this:
with numbers as (
select rownum as n
from t
where rownum <= 100
)

Oracle week calculation issue

I am using Oracle's to_char() function to convert a date to a week number (1-53):
select pat_id,
pat_enc_csn_id,
contact_date,
to_char(contact_date,'ww') week,
...
the 'ww' switch gives me these values for dates in January of this year:
Date Week
1-Jan-10 1
2-Jan-10 1
3-Jan-10 1
4-Jan-10 1
5-Jan-10 1
6-Jan-10 1
7-Jan-10 1
8-Jan-10 2
9-Jan-10 2
10-Jan-10 2
11-Jan-10 2
12-Jan-10 2
a quick look at the calendar indicates that these values should be:
Date Week
1-Jan-10 1
2-Jan-10 1
3-Jan-10 2
4-Jan-10 2
5-Jan-10 2
6-Jan-10 2
7-Jan-10 2
8-Jan-10 2
9-Jan-10 2
10-Jan-10 3
11-Jan-10 3
12-Jan-10 3
if I use the 'iw' switch instead of 'ww', the outcome is less desirable:
Date Week
1-Jan-10 53
2-Jan-10 53
3-Jan-10 53
4-Jan-10 1
5-Jan-10 1
6-Jan-10 1
7-Jan-10 1
8-Jan-10 1
9-Jan-10 1
10-Jan-10 1
11-Jan-10 2
12-Jan-10 2
Is there another Oracle function that will calculate weeks as I would expect or do I need to write my own?
EDIT
I'm trying to match the logic used by Crystal Reports. Each full week starts on a Sunday; the first week of the year starts on whichever day is represented by January 1st (e.g. in 2010, January 1st is a Friday).
When using IW, Oracle follows the ISO 8601 standard regarding week numbers (see http://en.wikipedia.org/wiki/ISO_8601). That is the same standard than the one we generally use in Europe here.
Your problem is also mentioned on the Oracle forum: http://forums.oracle.com/forums/thread.jspa?threadID=947291 and http://forums.oracle.com/forums/message.jspa?messageID=3318715#3318715. Maybe you can find a solution there.
I know this is old, but still a common question.
This should give you the correct results in the smallest amount of effort:
select pat_id,
pat_enc_csn_id,
contact_date,
to_char(contact_date + 1,'IW') week,
...
Since it looks like you are using your own special definition of the week number you'll need to write your own function.
It might be helpful that NLS_TERRITORY affects the day with which a week starts as used by the D Format Model
see also:
http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/sql_elements004.htm#SQLRF00210
and
http://www.adp-gmbh.ch/ora/sql/to_char.html
Based on this question, How do I calculate the week number given a date?, I wrote the following Oracle logic:
CASE
--if [date field]'s day-of-week (e.g. Monday) is earlier than 1/1/YYYY's day-of-week
WHEN to_char(to_date('01/01/' || to_char([date field],'YYYY'),'mm/dd/yyyy'), 'D') - to_char([date field], 'D') > 1 THEN
--adjust the week
trunc(to_char([date field], 'DDD') / 7) + 1 + 1 --'+ 1 + 1' used for clarity
ELSE trunc(to_char([date field], 'DDD') / 7) + 1
END calendar_week

How can I get a table of dates from the first day of the month, two months ago, to yesterday?

I have a situation that I would normally solve by creating a feeder table (for example, every date between five years ago and a hundred years into the future) for querying but, unfortunately, this particular job disallows creation of such a table.
So I'm opening this up to the SO community. Today is Jan 29, 2010. What query could I run that would give a table with a single date column with values ranging from Nov 1, 2009 through Jan 28, 2010 inclusive? On Feb 1, it should give me every date from Dec 1, 2009 through Jan 31, 2010.
I'm using DB2 but I'm happy to see any other solutions on the off-chance they may provide a clue.
I know I can select CURRENT DATE from sysibm.sysdummy1 (or dual for Oracle bods) but I'm not sure how to immediately select a date range without a physical backing table.
This just does sequential days between two dates, but I've posted to show you can eliminate the recursive error by supplying a limit.
with temp (level, seqdate) as
(select 1, date('2008-01-01')
from sysibm.sysdummy1
union all
select level, seqdate + level days
from temp
where level < 1000
and seqdate + 1 days < Date('2008-02-01')
)
select seqdate as CalendarDay
from temp
order by seqdate
Update from pax:
This answer actually put me on the right track. You can get rid of the warning by introducing a variable that's limited by a constant. The query above didn't have it quite right (and got the dates wrong, which I'll forgive) but, since it pointed me to the problem solution, it wins the prize.
The code below was the final working version (sans warning):
WITH DATERANGE(LEVEL,DT) AS (
SELECT 1, CURRENT DATE + (1 - DAY(CURRENT DATE)) DAYS - 2 MONTHS
FROM SYSIBM.SYSDUMMY1
UNION ALL SELECT LEVEL + 1, DT + 1 DAY
FROM DATERANGE
WHERE LEVEL < 1000 AND DT < CURRENT DATE - 1 DAY
) SELECT DT FROM DATERANGE;
which outputs, when run on the 2nd of February:
----------
DT
----------
2009-12-01
2009-12-02
2009-12-03
: : : :
2010-01-30
2010-01-31
2010-02-01
DSNE610I NUMBER OF ROWS DISPLAYED IS 63
DSNE616I STATEMENT EXECUTION WAS SUCCESSFUL.
just an idea (not even sure how you'd do this), but let's say you knew how many days you wanted. Like 45 days. If you could get a select to list 1-45 you could do date arithmetic to subtract that number from your reference data (ie today).
This kind of works (in MySQL):
set #i = 0;
SELECT #i:=#i+1 as myrow, ADDDATE(CURDATE(), -#i)
FROM some_table
LIMIT 10;
The trick i have is how to get the 10 to be dynamic. If you can do that then you can use a query like this to get the right number to limit.
SELECT DATEDIFF(DATE_SUB(CURDATE(), INTERVAL 1 DAY),
DATE_SUB(LAST_DAY(CURDATE()), INTERVAL 2 MONTH))
FROM dual;
I haven't used DB2 before but in SQL Server you could do something like the following. Note that you need a table with at least the number of rows as you need days.
SELECT TOP 45
DATEADD(d, ROW_NUMBER() OVER(ORDER BY [Field]) * -1, GETDATE())
FROM
[Table]