I have table like shown below and i would like to capture all 5 week days in every week whether the employee worked or not. If employee only worked 3 days, then i want to show the hours for the 3 days that he/she worked and assign 0 for the 2 missing days :
UID DT HOURS_WORKED
Mike 07/4/16 5
Mike 07/5/16 8
Mike 07/7/16 4
here is the desired results for this scenario
UID DT HOURS_WORKED
Mike 07/4/16 5
Mike 07/5/16 8
Mike 06/6/16 0
Mike 07/7/16 4
Mike 07/8/16 0
so i want to put 0 when they skipped work for that day. I don't want to show weekends. Thanks for your help
select UID, DT, HOURS_WORKED from my table
I enhanced #vkp 's answer to make it more generic, ( if you are really picky we need to handle the edge cases where some days of the 1st week or last week can fall into different years )
Over here I have added the ability to change the first day of the week using the Datefirst setting . more on Datefirst MSDN : https://msdn.microsoft.com/en-us/library/ms181598.aspx
/* Setup Test Data
Drop Table #T1
Create Table #T1 ( UID Varchar(10), Dt Date, Hrs int )
insert into #T1 Values
( 'Mike' , GetDate() , 8 ) , -- Sat 07/23
( 'Mike' , GetDate()-1 , 8 ) ,
( 'Mike' , GetDate()-2 , 8 ) ,
( 'Mike' , GetDate()+3 , 8 ) -- Tue 07/26
( 'John' , GetDate() , 8 ) , -- Sat 07/23
( 'John' , GetDate()-1 , 8 ) ,
( 'John' , GetDate()-2 , 8 ) ,
( 'John' , GetDate()+3 , 8 )
insert into #T1 Values
( 'Mike' , GetDate() - 206 , 8 ) , --- One Date for Last Year 12/30 to Test Edge Case
-- select * , DatePart( WEEK, Dt) from #T1
*/
-- Create a Helper TV Function To get Dates for a Given Week in a Year
ALTER FUNCTION GetDates
(
#WK int ,
#yr varchar(5) = ''
)
RETURNS
#Table_Var TABLE
(
DD int,
Dt Date,
Wk int
)
AS
BEGIN
IF #yr = '' SET #yr = YEAR(Getdate()) -- If Year is Blank then Default to current year
Declare #LastDateOfYr Date = RTRIM(#YR)+'-12-31' -- Last Day of the year
Declare #LastDayOfYr Int = CAST(Datename(dy, #LastDateOfYr ) as int) -- No.of Days in the Year to Handle Leap Yrs
;WITH Dates as
(
-- SELECT 0 as DD , DATEADD(D, 0, #yr ) as Dt , DatePart( WEEK, DATEADD(D, 0 , #yr )) as Wk
SELECT Datepart( DAYOFYEAR,DateAdd(D, (#WK-2)*7, #yr) ) as DD , DATEADD(D, (#WK-2)*7, #yr ) as Dt , #WK-2 as Wk -- Initial values for the Recursive CTE.
UNION ALL
SELECT Dates.DD+1 as DD , DATEADD(D, Dates.DD, #yr ) , DatePart( WEEK,DATEADD(D, Dates.DD, #yr )) from Dates where Dates.DD <= #LastDayOfYr
AND Wk <= #WK + 1 -- Terminator for Recursion
)
INSERT INTO #Table_Var
SELECT
DD ,
Dt ,
Wk
FROM Dates as A
WHERE A.Wk = #WK
OPTION (MAXRECURSION 21) -- At any point we dont use CTE For more than 3 Weeks (one week actually). If the CTE is changed by using the commented out Initializer inside the CTE Above then this number has to change accordingly
RETURN
END
GO
Query :
SET DATEFIRST 1 -- Make Monday as First Day of Week. The default is Sunday.
Select B.* , A.* , ISNULL(T.Hrs,0) Hours_Worked
FROM
( SELECT Distinct UID,
DatePart( WEEK, Dt) as WK ,
DatePart( YEAR, Dt) as Yr
FROM #T1
) A
CROSS APPLY dbo.GetDates(A.WK, A.Yr ) B -- Helper Function Used to apply
LEFT OUTER JOIN #T1 T ON B.Dt = T.Dt
Construct a dates cte which will have all the dates in a week and perform an outer join on this table to show 0 when the employee doesn't work on a given weekday.
with x as (select cast('2016-07-01' as date) dt
union all
select dateadd(dd,1,dt) from x where dt < '2016-07-31')
select e.uid, t.dt, coalesce(e.hours_worked,0) hours_worked
from (select * from x where datepart(dw,dt) between 2 and 6) t
left join emp_table e on e.dt = t.dt
Related
I have Date time when engine has started working and how long was it working. but sometimes it can work more than 24 Hours.
if it worked for 28 Hours on the starting date i will have record
Name started_working Finished working hours_worked
obj-00123 07/02/2018 13:30 08/02/2018 17:30 28
I need to to have record that will show that engine has worked for 10:30 in 07 and 17:30 in 08.
Name started_working Finished working hours_worked
obj-00123 07/02/2018 13:30 07/02/2018 00:00 10:30
obj-00123 07/02/2018 13:30 08/02/2018 17:30 17:30
or something like that. I don't have any idea how can i get this done. can you give me some clues. i dont ask for writing code if its not too easy.
thank you
This might do the trick for you
--Using CTE to show sample data
;WITH cteX( Name,started_working,Finished_working)
AS
(
SELECT
'obj-00123','07/02/2018 13:30','08/02/2018 17:30' UNION ALL
SELECT 'obj-00155','07/02/2018 15:00','07/02/2018 22:30'
)
SELECT
X.Name
, X.started_working
, X.Finished_working
, HoursWorked = CONVERT(VARCHAR(12), DATEADD(minute, DATEDIFF(minute, X.started_working, X.Finished_working), 0), 114)
FROM
(
SELECT
T1.Name
,T1.started_working
,Finished_working = DATEADD(SECOND,0,DATEADD(DAY, DATEDIFF(DAY,-1,T1.started_working),0)) -- Dummy finish time # Midnight
FROM
cteX T1
WHERE
DATEDIFF(DAY,T1.started_working,T1.Finished_working) <> 0 --Create a dummy finish time #Midnight when start and end not on same day
UNION ALL
SELECT
T2.Name
,started_working = CASE WHEN DATEDIFF(DAY,T2.started_working,T2.Finished_working) <> 0
THEN DATEADD(DAY, DATEDIFF(DAY, 0, T2.Finished_working), 0) --Start # Midnight
ELSE T2.started_working
END
,T2.Finished_working
FROM
cteX T2
) X
ORDER BY
X.Name, X.started_working
OUTPUT
Name started_working Finished_working HoursWorked
obj-00123 2018-07-02 13:30:00.000 2018-07-03 00:00:00.000 10:30:00:000
obj-00123 2018-08-02 00:00:00.000 2018-08-02 17:30:00.000 17:30:00:000
obj-00155 2018-07-02 15:00:00.000 2018-07-02 22:30:00.000 07:30:00:000
According to your sample data working hours may be more than several days. In this case you need to use tally table or recursive CTE. I have used recursive CTE since it's easier to handle result fields. Also there are two columns in result named started_working and started_working2. started_working is from your expected output, but I believe you need started_working2 column
declare #T as table (
Name varchar(100)
, started_working datetime
, finished_working datetime
--, hours_worked int
)
insert into #T
values
('obj-00123', '20180207 13:30', '20180208 17:30')
, ('obj-00123', '20180208 19:00', '20180209 05:00')
, ('obj-00123', '20180209 19:00', '20180209 22:00')
, ('obj-00123', '20180210 19:00', '20180213 22:00')
;with rcte as (
select
*, started_working2 = started_working
, next_date = cast(dateadd(dd, 1, started_working) as date), 1 step
from
#T
union all
select
Name, started_working, finished_working
, cast(next_date as datetime)
, dateadd(dd, 1, next_date), step + 1
from
rcte
where
next_date < finished_working
)
select
Name, started_working, started_working2, finished_working
, right(replace(str(diff / 60), ' ', 0), 2) + ':' + right(replace(str(diff % 60), ' ', 0), 2) hours_worked
from (
select
Name, started_working
, case
when step = 1 then started_working
else started_working2
end started_working2
, case
when step = max(step) over (partition by Name, started_working)
then finished_working else next_date
end finished_working
from
rcte
) t
cross apply (select datediff(mi, started_working2, finished_working) diff) ca
I'd approach the solution something like this:
WITH dynamic_twelths_of_hr_table(datetime2_value) AS
(
SELECT '2017-01-01'
UNION ALL
SELECT DATEADD(MINUTE, 5, datetime2_value)
FROM dynamic_twelths_of_hr_table
WHERE DATEADD(MINUTE, 5, datetime2_value) <= '2019-01-01'
)
,twelths_hr_table AS
(
SELECT
DATEADD(DAY, DATEDIFF(DAY, 0, datetime2_value), 0) AS date_value
,datetime2_value
FROM dynamic_twelths_of_hr_table
)
,modified_source_table AS
(
SELECT
name
,objectid
,engine_start
,ISNULL(engine_stop, GETDATE()) AS engine_stop
,IIF(engine_start IS NULL OR engine_stop IS NULL, 1, 0) AS is_still_running
FROM [YOUR_SOURCE_TABLE]
)
SELECT
name
,objectid
,is_still_running
,date_value
,(COUNT(datetime2_value)/12.0) AS hours_run_on_this_day
FROM
modified_source_table
LEFT JOIN
twelths_hr_table AS tht
ON (tht.datetime2_value BETWEEN engine_start AND engine_stop)
GROUP BY
name, objectid, is_still_running, date_value
ORDER BY
name, objectid, is_still_running, date_value
Note I haven't tested this code so please excuse any small syntax errors.
I've also baked in an assumption about the range of dates to be considered (these can be widened, or made dynamic based on when the query runs), and it has a 5 minute resolution (based on the fact that, at a glance, I could only see one value in the engine_stop column that didn't fall on a 5-minute threshold - so I assume sub-5-minute precision is not required).
Basically what it does is expand each engine row out into 5-minute windows (twelths of an hour), and then simply groups these by day and counts the number of windows per day during which the engine was running.
For currently-running engines, it will calculate how long it has run so far. I trust you can tweak the code to your exact requirements.
thank you to all. this worked perfectly. it needed slight polishing and recursion needed to be set to 0.
But creating view is a trouble with CTE.
create view mroobjectenginerowkinghoursdeclare as
declare #T as table (
Name nvarchar(100)
, OBJECTID varchar(50)
, started_working datetime
,STOPFROM datetime
,STARTDATE datetime
,STOPDATE datetime
,MODIFIEDDATETIME datetime
,START_STOP int
,STARTDESCRIPTION nvarchar(300)
,STOPDESCRIPTION nvarchar(300)
,wattage nvarchar (50)
,purpose nvarchar(300)
,location nvarchar(300)
,finished_working datetime
,oldDiff int
)
insert into #T
select
NAME
,OBJECTID
,STOPTO
,STOPFROM
,STARTDATE
,STOPDATE
,MODIFIEDDATETIME
,START_STOP
,STARTDESCRIPTION
,STOPDESCRIPTION
,wattage
,purpose
,location
,next_stopfrom
,diff
FROM [MicrosoftDynamicsAX].[dbo].[mroobjectengineworkinghours]
;with rcte as (
select
*, started_working2 = started_working
, next_date = cast(dateadd(dd, 1, started_working) as date), 1 step
from
#T
union all
select
Name,OBJECTID, started_working,STOPFROM,STARTDATE,STOPDATE,MODIFIEDDATETIME,START_STOP,STARTDESCRIPTION
,STOPDESCRIPTION,wattage
,purpose
,location, finished_working,oldDiff
, cast(next_date as datetime)
, dateadd(dd, 1, next_date), step + 1
from
rcte
where
next_date < finished_working
)
select
Name,OBJECTID, started_working,STOPFROM,STARTDATE,STOPDATE,MODIFIEDDATETIME,START_STOP,STARTDESCRIPTION
,STOPDESCRIPTION,wattage
,purpose
,location,oldDiff, started_working2, finished_working
, right(replace(str(diff / 60), ' ', 0), 2) + ':' + right(replace(str(diff % 60), ' ', 0), 2) hours_worked
from (
select
Name,OBJECTID, started_working,STOPFROM,STARTDATE,STOPDATE,MODIFIEDDATETIME,START_STOP,STARTDESCRIPTION
,STOPDESCRIPTION,wattage
,purpose
,location,oldDiff
, case
when step = 1 then started_working
else started_working2
end started_working2
, case
when step = max(step) over (partition by Name, started_working)
then finished_working else next_date
end finished_working
from
rcte
) t
cross apply (select datediff(mi, started_working2, finished_working) diff) ca
OPTION (MAXRECURSION 0);
I have 2 day names as input (For Example: "Friday" and "Monday". I need to get need to get all dates from a table in between those Friday and Monday (Friday, Saturday, Sunday, Monday) . If it is "Tuesday" and "Thursday" i need to get (Tuesday, Wednesday and Thursday )
My table is
If days are friday and monday. My Output should be
I tried this
SELECT
EMPLID, DUR, DayName, TRC
FROM DayTable
WHERE
DayName BETWEEN 'FRIDAY' AND 'MONDAY'
ORDER BY DUR ASC
but not working, Help me to solve this. Thanks in Advance
Try this query:
DECLARE #From int,#To int
Create Table #Days(Id int, DayOfWeek Varchar(100))
Insert into #Days Values
(1,'Sunday'),
(2,'Monday'),
(3,'Tuesday'),
(4,'Wednesday'),
(5,'Thursday'),
(6,'Friday'),
(7,'Saturday')
Select #From = Id from #Days where DayOfWeek = 'Friday'
Select #To = Id from #Days where DayOfWeek = 'Monday'
Select T.EMPLID, T.DUR, T.DayName, T.TRC from DayTable T
Inner Join #Days D on T.DayName = D.DayOfWeek AND (D.Id <= #To Or D.Id >= #From)
Hope this helps!
Update
Here's the same solution in a table valued function:
create function dbo.DaysBetween (
#DayFrom nvarchar(16)
, #DayTo nvarchar(16)
) returns #results table ([DayName] nvarchar(16))
as
begin
declare #daynames table (id smallint not null, [dayname] nvarchar(16) not null)
insert #daynames(id, [dayname])
values (0, 'Monday'),(1, 'Tuesday'),(2, 'Wednesday'),(3, 'Thursday'),(4, 'Friday'),(5, 'Saturday'),(6, 'Sunday')
declare #dayFromInt smallint
, #dayToInt smallint
select #dayFromInt = id from #daynames where [dayname] = #DayFrom
if (#dayFromInt is null)
begin
--hacky trick from https://stackoverflow.com/a/4681815/361842
set #dayFromInt = cast(('Invalid Day From Name: ' + #DayFrom) as int)
return
end
select #dayToInt = id from #daynames where [dayname] = #DayTo
if (#dayToInt is null)
begin
--hacky trick from https://stackoverflow.com/a/4681815/361842
set #dayToInt = cast(('Invalid Day To Name: '+ #DayTo) as int)
return
end
insert #results ([dayname])
select [dayname]
from #daynames
where
(
(#dayFromInt <= #dayToInt) and (id between #dayFromInt and #dayToInt)
or
(#dayFromInt > #dayToInt) and (id >= #dayFromInt or id <= #dayToInt)
)
return
end
go
Here are some example scenarios:
select * from dbo.DaysBetween('Monday','Friday')
select * from dbo.DaysBetween('Friday','Monday')
select * from dbo.DaysBetween('Tuesday','Thursday')
select * from dbo.DaysBetween('Thursday','Tuesday')
select * from dbo.DaysBetween('Christmasday','Monday')
go --required to get this result after the above error
select * from dbo.DaysBetween('Monday','Holiday')
To use this in your query, you'd do:
SELECT EMPLID
, DUR
, DayName
, TRC
FROM DayTable
WHERE
[DayName] in
(
select [DayName]
from dbo.DaysBetween('Friday','Monday')
)
ORDER BY DUR ASC
You cannot use the between operator like that. Datename returns a string, and the between operator is tru for all daynames that lexigraphically falls between "Friday" and "Monday".
I would suggest using
DayName in ('FRIDAY','SATURDAY','SUNDAY', 'MONDAY' )
Or use
set datefirst 2
...
where datepart(weekday,DUR)>3
If you use the DATEPART function on DUR, you will get the integer representation for the weekday.
So updating your WHERE clause:
SELECT
EMPLID, DUR, DayName, TRC
FROM DayTable
WHERE
DATEPART(weekday, DUR) BETWEEN 2 AND 6
ORDER BY DUR ASC
Where datepart will give you the weekday integer: SUN -> 1, MON-> 2 ... SAT -> 7
https://learn.microsoft.com/pt-br/sql/t-sql/functions/datepart-transact-sql
If you know your sql server's setting start day of the week is Sunday, then you can try this query :
SELECT
EMPLID, DUR, DayName, TRC
FROM DayTable
WHERE
DATEPART(WEEKDAY, DUR) <= 2 or DATEPART(WEEKDAY, DUR) >= 6
ORDER BY DUR ASC
Where week index of Monday = 2 and Friday = 6
In my quest to construct a function that can calculate the date after x working days I came across this function:
ALTER FUNCTION [dbo].[AddBusinessDays] (#Date date,#n INT)
RETURNS DATE AS BEGIN
DECLARE #d INT;
SET #d=4-SIGN(#n)*(4-DATEPART(DW,#Date));
RETURN DATEADD(D,#n+((ABS(#n)+#d-2)/5)*2*SIGN(#n)-#d/7,#Date) END
This function works however I need to link it with my holiday table so that it can omit specific holidays in my country. When I run it with today's date (26-04-2017) I get this date after 20 working days 24-05-2017, so it omitted only the weekends. How do I modify it so that it also skips the holidays?
Apologies if I am sending too many requests for one problem. I am a beginner in SQL. Thanks
Instead of relying on hard to understand calculations wouldn't it be easier to explicitly have your working dates and select from there? For example:
DECLARE #NthWorkingDay INT = 33;
DECLARE #holidays TABLE
(
holiday DATE ,
[description] VARCHAR(500)
);
INSERT #holidays
( holiday, description )
VALUES ( '20170519', '...' ),
( '20170501', '...' ),
( '20170611', '...' ),
( '20170704', '...' );
DECLARE #sunday INT ,
#saturday INT;
-- 1/1/2000 is a known date - Saturday
SET #saturday = DATEPART(WEEKDAY, DATEFROMPARTS(2000, 1, 1));
SET #sunday = DATEPART(WEEKDAY, DATEFROMPARTS(2000, 1, 2));
WITH tally
AS ( SELECT TOP 5000
ROW_NUMBER() OVER ( ORDER BY t1.object_id ) AS N
FROM master.sys.all_columns t1
CROSS JOIN master.sys.all_columns t2
),
dates ( theDate )
AS ( SELECT DATEADD(DAY, N - 1, CAST(GETDATE() AS DATE))
FROM tally
),
workDates ( workingDay, workingDate )
AS ( SELECT ROW_NUMBER() OVER ( ORDER BY theDate ) ,
theDate
FROM dates
WHERE DATEPART(WEEKDAY, theDate) NOT IN ( #saturday, #sunday )
AND theDate NOT IN ( SELECT holiday
FROM #holidays )
)
SELECT workingDate
FROM workDates
WHERE workingDay = #NthWorkingDay;
I have a query that calculates the number working days within a month based on a table which stores all our public holidays.
The current output would show all working days, excluding public holidays and Saturday and Sunday, I would like to show each day of the month, but don't increment on a public holiday or Saturday or Sunday.
Is there a way to conditionally increment the row number?
Query is below:
DECLARE #startnum INT=0
DECLARE #endnum INT=365;
WITH gen AS
(
SELECT #startnum AS num
UNION ALL
SELECT num + 1
FROM gen
WHERE num + 1 <= #endnum
)
, holidays AS
(
SELECT CONVERT(DATE, transdate) AS HolidayDate
FROM WORKCALENDER w
WHERE w.CALENDARID = 'PubHoliday'
)
, allDays AS
(
SELECT DATEADD( d, num, CONVERT( DATE, '1 Jan 2016' ) ) AS DateOfYear
, DATENAME( dw, DATEADD( d, num, CONVERT( DATE, '1 Jan 2016' ))) AS [dayOfWeek]
FROM gen
)
select number = ROW_NUMBER() OVER ( ORDER BY DateOfYear )
, *
from allDays
LEFT OUTER JOIN holidays
ON allDays.DateOfYear = holidays.HolidayDate
WHERE holidays.HolidayDate IS NULL
AND allDays.dayOfWeek NOT IN ( 'Saturday', 'Sunday')
AND DateOfYear >= CONVERT( DATE, '1 ' + DATENAME( MONTH, GETDATE() ) + ' 2016' )
AND DateOfYear < CONVERT( DATE, '1 ' + DATENAME( MONTH, DATEADD( month, 1, GETDATE()) ) + ' 2016' )
option (maxrecursion 10000)
kind of pseudo code
select date, row_number() over (order by date) as num
from ( select date
from allDates
where month = x and weekday
exept
select date
from holidays
where month is x
) as t
union all
select date, null
from holidays
where month is x
order by date
You could use a windowed sum, see how the output of WorkdaySequenceInMonth is composed.
DECLARE #startDate DATE = '20160101'
, #numDays INT = 365
, #num INT = 0;
DECLARE #Holidays TABLE (Holiday DATE);
INSERT INTO #Holidays(Holiday)
VALUES ('20160101')
, ('20160115')
, ('20160714');
WITH nums AS
(
SELECT row_number() OVER (ORDER BY object_id) - 1 as num
FROM sys.columns
),
dateRange as
(
SELECT
DATEADD(DAY, num, #startDate) AS Dt
, num
FROM nums
WHERE num < #numDays
),
Parts AS
(
SELECT
R.Dt as [Date]
, Year(R.Dt) as [Year]
, Month(R.Dt) as [Month]
, Day(R.Dt) as [Day]
, Datename(weekday, R.Dt) as [Weekday]
, CASE WHEN H.Holiday IS NOT NULL
OR Datename(weekday, R.Dt) IN ('Saturday', 'Sunday')
THEN 0
ELSE 1
END AS IsWorkday
FROM dateRange R
LEFT JOIN #Holidays H ON R.Dt = H.Holiday
)
--
select
*
, sum(IsWorkday) over (PARTITION BY [Year],[month]
ORDER BY [Day]
ROWS UNBOUNDED PRECEDING) as WorkdaySequenceInMonth
from Parts
order by [Year], [Month]
Hi You can try this query, the initial part is the data generation, maybe you won't need it.
Then I generate a temp table with all the dates for the time period set in #StartYear, #EndYear
Then just simple queries to return the data
-- generate holidays table
select holiday
into #tempHolidays
from
(
select '20160101' as holiday
union all
select '20160201' as holiday
union all
select '20160205' as holiday
union all
select '20160301' as holiday
union all
select '20160309' as holiday
union all
select '20160315' as holiday
) as t
create table #tempCalendar (Date_temp date)
select * from
#tempHolidays
declare #startYear int , #endYear int, #i int, #dateStart datetime , #dateEnd datetime, #date datetime, #i = 0
Select #startYear = '2016'
,#endYear = '2016'
,#dateStart = (Select cast( (cast(#startYear as varchar(4)) +'0101') as datetime))
,#dateEnd = (Select cast( (cast(#startYear as varchar(4)) +'1231') as datetime))
,#date = #dateStart
--Insert dates of the period of time
while (#date <> #dateEnd)
begin
insert into #tempCalendar
Select #date
set #date = (select DATEADD(dd,1,#date))
end
-- Retrive Date list
Select Date_temp
from #tempCalendar
where Date_temp not in (Select holiday from #tempHolidays)
and datename(weekday,Date_temp) not in ('Saturday','Sunday')
--REtrieve sum of working days per month
select DATEPART(year,Date_temp) as year
,DATEPART(month,Date_temp) as Month
,Count(*) as CountOfWorkingDays
from #tempCalendar
where Date_temp not in (Select holiday from #tempHolidays)
and datename(weekday,Date_temp) not in ('Saturday','Sunday')
Group by DATEPART(year,Date_temp)
,DATEPART(month,Date_temp)
You should change #tempHolidays for your Holidays table, and use #StarYear and #EndYear as your time period.
Here's a simple demo that shows the use of the partition by clause to keep contiguity in your sequencing for non-holidays
IF OBJECT_ID('tempdb.dbo.#dates') IS NOT null
DROP TABLE #dates;
CREATE TABLE #dates (d DATE);
IF OBJECT_ID('tempdb.dbo.#holidays') IS NOT null
DROP TABLE #holidays;
CREATE TABLE #holidays (d DATE);
INSERT INTO [#holidays]
( [d] )
VALUES
('2016-12-25'),
('2017-12-25'),
('2018-12-25');
INSERT INTO [#dates]
( [d] )
SELECT TOP 1000 DATEADD(DAY, n, '2015-12-31')
FROM [Util].dbo.[Numbers] AS [n];
WITH holidays AS (
SELECT d.*, CASE WHEN h.d IS NULL THEN 0 ELSE 1 END AS [IsHoliday]
FROM [#dates] AS [d]
LEFT JOIN [#holidays] AS [h]
ON [d].[d] = [h].[d]
)
SELECT d, ROW_NUMBER() OVER (PARTITION BY [holidays].[IsHoliday] ORDER BY d)
FROM [holidays]
ORDER BY d;
And please forgive my marking only Christmas as a holiday!
How to create a sql query to return the number of occurrences of sick days.
Where one occurrence is defined as consecutive days out sick. It would also be considered one occurrence if the a weekend fell within that time period or a holiday fell in that time period.
Examples of what is consider one occurrence:
A person is out sick Monday and Tuesday.
A person is out sick on a Friday and the following Monday.
A person is out sick Thursday, Friday is a Holiday, and Monday they are sick.
For these examples it would be considered three occurrences.
There is a table that contains the sick days (date) (one row for every sick day) and a table that contains the observed holiday dates.
To simplify the tables and fields:
tbl_emp
empid
empname
tbl_sick
empid
sickdate
tbl_holiday
holiday
Logically, this should work, but it probably isn't the most elegant solution.
After creating a bit of sample data, I gather all of the 'suspect' days including known sick days, known holidays and weekends that are adjacent to either a sick day or a holiday. Then I identify the start and end of each group of consecutive days and for each employee count the start dates of ranges that contain a sick day.
/***** SAMPLE DATA *****/
declare #sick table (
empid int,
sick datetime
)
declare #holiday table (
holiday datetime
)
/* Example 1 */
insert into #sick values (1,'2010/01/04'); /* Mon */
insert into #sick values (1,'2010/01/05'); /* Tue */
/* Example 2 */
insert into #sick values (1,'2010/01/15'); /* Fri */
insert into #sick values (1,'2010/01/18'); /* Mon */
/* Example 3 */
insert into #sick values (1,'2010/01/21'); /* Thu */
insert into #holiday values('2010/01/22'); /* Fri */
insert into #sick values (1,'2010/01/25'); /* Mon */
/* Extra Examples */
insert into #sick values (3,'2010/01/08');
insert into #sick values (2,'2010/01/08');
insert into #holiday values ('2010/01/11');
insert into #sick values (3,'2010/01/20');
insert into #sick values (3,'2010/01/21');
/* Extra Holiday */
insert into #holiday values ('2010/02/05');
/***** SAMPLE DATA *****/
/* First a CTE to gather all of the 'suspect' days together
including known sick days, known holidays and weekends
that are adjacent to either a sick day or a holiday */
with suspectdays as (
/* Start with all Sick days */
select
empid,
sick dt,
'sick' [type]
from
#sick
/* Add all Saturdays following a sick Friday */
union
select
empid,
DATEADD(day,1,sick) dt,
'weekend' [type]
from
#sick
where
(DATEPART(WEEKDAY,sick) + ##DATEFIRST) % 7 = 6
/* Add all Sundays following a sick Friday */
union
select
empid,
DATEADD(day,2,sick) dt,
'weekend' [type]
from
#sick
where
(DATEPART(WEEKDAY,sick) + ##DATEFIRST) % 7 = 6
/* Add all Sundays preceding a sick Monday */
union
select
empid,
DATEADD(day,-1,sick) dt,
'weekend' [type]
from
#sick
where
(DATEPART(WEEKDAY,sick) + ##DATEFIRST) % 7 = 2
/* Add all Saturdays preceding a sick Monday */
union
select
empid,
DATEADD(day,-2,sick) dt,
'weekend' [type]
from
#sick
where
(DATEPART(WEEKDAY,sick) + ##DATEFIRST) % 7 = 2
/* Add all Holidays */
union
select
empid,
holiday dt,
'holiday' [type]
from
#holiday,
(select distinct empid from #sick) as a
/* Add all Saturdays following a holiday Friday */
union
select
empid,
DATEADD(day,1,holiday) dt,
'weekend' [type]
from
#holiday,
(select distinct empid from #sick) as a
where
(DATEPART(WEEKDAY,holiday) + ##DATEFIRST) % 7 = 6
/* Add all Sundays following a holiday Friday */
union
select
empid,
DATEADD(day,2,holiday) dt,
'weekend' [type]
from
#holiday,
(select distinct empid from #sick) as a
where
(DATEPART(WEEKDAY,holiday) + ##DATEFIRST) % 7 = 6
/* Add all Sundays preceding a holiday Monday */
union
select
empid,
DATEADD(day,-1,holiday) dt,
'weekend' [type]
from
#holiday,
(select distinct empid from #sick) as a
where
(DATEPART(WEEKDAY,holiday) + ##DATEFIRST) % 7 = 2
/* Add all Saturdays preceding a holiday Monday */
union
select
empid,
DATEADD(day,-2,holiday) dt,
'weekend' [type]
from
#holiday,
(select distinct empid from #sick) as a
where
(DATEPART(WEEKDAY,holiday) + ##DATEFIRST) % 7 = 2
),
/* Now a CTE to identify the start and end of each
group of consecutive days for each employee */
suspectranges as (
select distinct
sd.empid,
( select
max(dt)
from
suspectdays
where
empid = sd.empid and
DATEADD(day,-1,dt) not in (select dt from suspectdays where empid = sd.empid) and
dt <= sd.dt
) rangeStart,
( select
min(dt)
from
suspectdays
where
empid = sd.empid and
DATEADD(day,1,dt) not in (select dt from suspectdays where empid = sd.empid) and
dt >= sd.dt
) rangeEnd
from
suspectdays sd
)
/* For each employee count the start dates of ranges that contain a sick day */
select
empid,
COUNT(rangeStart) SickIncidents
from
suspectranges sr
where
exists (select * from suspectdays where dt between sr.rangeStart and sr.rangeEnd and empid=sr.empid and type='sick')
group by
empid
For the sample data I created, here's the result.
empid SickIncidents
----------- -------------
1 3
2 1
3 2
If i had a little more time, i would try to get a query out for you. But this really reminds me of a sql Challenge that is close enough to your issue.
Different Approaches
Explanation of Approaches
You are probably going to have to end up with a form of recursion or a tricky joining method to figure out if a sequence of days are what you prescribe. Hopes this helps you in your quest for the query.
Here's my attempt. Quite a lot of this query would lend itself to precalculation though rather than doing it all every time. Particularly having a table of working days with an incrementing sequence number.
--Base Tables
WITH tbl_holiday AS
(
SELECT CAST(2010-05-27 AS DATETIME) AS holidate UNION ALL
SELECT CAST(2010-05-28 AS DATETIME)
),
tbl_emp AS
(
SELECT 1 AS empid, 'Bob' AS empname UNION ALL
SELECT 2 AS empid, 'Dave' AS empname
),
tbl_sick AS
(
SELECT 1 AS empid, '2010-04-01' as sickdate UNION ALL
SELECT 1, '2010-04-02' UNION ALL
SELECT 1, '2010-04-09'
),
--Calculated Tables
tbl_WorkingDays AS
(
SELECT dateadd(day,number,'2010-01-01') AS workdate
FROM master.dbo.spt_values
WHERE Type='P' AND number <= DATEDIFF(day,'2010-01-01',getdate())
AND (##datefirst + datepart(weekday, dateadd(day,number,'2010-01-01'))) % 7 not in (0, 1)
EXCEPT
SELECT * FROM tbl_holiday
),
tbl_NumberedWorkingDays AS
(
SELECT ROW_NUMBER() OVER (ORDER BY workdate) AS N,
workdate
FROM tbl_WorkingDays
)
SELECT e.empid, e.empname, COUNT(nwd.N) AS Absences
FROM tbl_emp e
LEFT JOIN tbl_sick s ON e.empid = s.empid
LEFT JOIN tbl_NumberedWorkingDays nwd ON nwd.workdate = s.SickDate
WHERE (s.empid IS NULL) OR (nwd.N = 1) OR
NOT EXISTS (SELECT * FROM tbl_sick s2 WHERE s.empid = s2.empid AND sickdate = (SELECT
workdate FROM tbl_NumberedWorkingDays WHERE N = nwd.N-1))
GROUP BY e.empid, e.empname
First, the simplest way to do analysis against dates is to have a calendar table which is a sequential list of dates. The advantage is that it makes it easy to indicate which days are not workdays (for whatever reason) and which days are holidays.
Create Table dbo.Calendar (
[Date] Date not null primary key clustered
, IsHoliday bit not null default(0)
, IsWorkday bit not null default(1)
, Constraint CK_Calendar Check ( Case
When IsHoliday = 1 And IsWorkDay <> 1 Then 0
Else 1
End = 1 )
)
The only additional restriction I made here is that if something is a holiday, then it must also be marked as not being a workday. Now let's fill our calendar table:
;With Numbers As
(
Select Row_Number() Over( Order By C1.object_id ) As Value
From sys.columns As C1
Cross Join sys.columns As C2
)
, CalendarItems As
(
Select N.Value, DateAdd(d, N.Value, '2000-01-01')As [Date]
From Numbers As N
Where DateAdd(d, N.Value, '2000-01-01') <= '2100-01-01'
)
Insert Calendar( [Date], IsWorkDay )
Select [Date], Case When DatePart(dw, [Date]) In(1,7) Then 0 Else 1 End As IsWorkDay
From CalendarItems
I'm using a CTE to generate a sequential list of integers which I can then use to populate my table. Often, it is useful to have this table be static. In addition, I've marked days that are on Sunday or Saturday as not being workdays. In the above query, I arbitrarily filled my calendar table with dates from the year 2000 to 2100 however you can easily expand the range if you wish.
We can even update the table to account for your tbl_holiday table:
Update Calendar
Set IsHoliday = 1
From Calendar
Join tbl_Holiday
On tbl_Holiday.holiday = Calendar.[Date]
Finally, we have our query to get the number of occurrances:
;With WorkDayNums As
(
Select C1.[Date]
, Row_Number() Over ( Order By C1.[Date] ) As Seq
From dbo.Calendar As C1
Where C1.IsWorkDay = 1
)
Select S.empid, Count(*) As SickOccurances
From WorkDayNums As WDN
Join (
Select Min(S1.sickdate) MinSickDate, Max(S1.sickdate) As MaxSickDate
From tbl_sick As S1
) As MinMax
On WDN.[Date] Between MinMax.MinSickDate And MinMax.MaxSickDate
Join tbl_sick As S
On S.sickdate = WDN.[Date]
Where Exists (
Select 1
From WorkDayNums As WDN2
Join tbl_sick As S2
On S2.sickdate= WDN2.[Date]
Where WDN2.Seq = WDN.Seq + 1
)
Group By S.empid
What I did here is to create a sequence for each workday. So if Friday was 1, Monday would be 2 barring any holidays. With this, I can easily see if the next workday was a sick day.
You did not make it clear in your OP about whether more than two sequential days counted as a single occurrence or multiple occurrences. In this approach, each two day combination would count as a single occurrence. So if someone where out Monday, Tuesday and Wednesday, the above query should count that as two occurrences.
Just came across this old question and thought I might be able to offer an elegant solution.
Assuming you have a utility function in your database similar to
create function dbo.Fn_Number (#Start int, #N int)
returns #Number table
(
N int not null primary key
)
as
begin
declare #i int
set #i = #Start
while #i <= #N
begin
insert into #Number values (#i)
set #i = #i + 1
end
return
end
go
And using JC's example data you can execute the following
/* We need to consider the following range */
declare #From datetime
declare #To datetime
select #From = min(sick), #To = max(sick)
from #sick
/* Ignoring holidays and Saturday & Sunday create a table of workdays
for the given range */
declare #Workdays table (workday datetime not null primary key)
insert into #Workdays
select dateadd(day, n.N, #From) as Workdays
from dbo.Fn_Number(0, datediff(day, #From, #To) - 1) n
where -- ignore Saturday and Sunday
datepart(weekday, dateadd(day, n.N, #From)) not in (1, 7) and
-- ignore holidays
dateadd(day, n.N, #From) not in (select holiday from #holiday)
The core of the solution is here. It is saying, by empid, get be a count of the days sick, but ignore any days where the preceding workday was also a sick day (because we want don't want to count that twice)
select empid, count(*) as sick_events
from #sick s
where
not exists
( -- make sure there wasn't a sick day prior to this one for this employee
select *
from #sick sa
where sa.empid = s.empid and sa.sick < s.sick and
not exists
( -- and if there was ensure that there wasn't an intervening workday
select *
from #Workdays w
where w.workday < s.Sick and w.workday > sa.Sick
)
)
group by empid