distribute accumulated working hours through days - sql

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);

Related

How to get Last day day and Today Data based on time limit

I have sample data here:
ID Val dt
1 Mohan 2017-10-13 13:02:49.493
2 Manasa 2017-10-12 20:02:49.493
3 maren 2017-10-13 18:02:49.493
When I run the statement today at 2 PM, it should give the result set of the last day 6 PM after Data, and today data up to this time and when I run the statement today at 7 PM, it should give today's data after 6 PM.
If I ran it at Today at 2 PM, the result will be like this
ID Val dt
1 Mohan 2017-10-13 13:02:49.493
2 Manasa 2017-10-12 20:02:49.493
If I ran at Today at 7 PM:
ID Val dt
3 maren 2017-10-13 18:02:49.493
Basic thing, if I ran before 6 PM it should give last day data before 6 PM and if i ran after 6 PM it should give today Data after 6 PM. I have tried with DATE DIFF conditions but am not able to justify the result - can any one please suggest?
Table Script
Declare #tab table
(ID INT,Val Varchar(10),dt datetime)
Insert into #tab (ID,val,dt)
values (1,'Mohan','2017-10-13 13:02:49.493'),
(2,'Manasa','2017-10-12 20:02:49.493'),
(3,'maren','2017-10-13 18:02:49.493')
You can achieve this by creating two dates that simulate your day "window", i.e. 6pm yesterday and 6pm today, adjusting the concept of yesterday/today depending on whether the current time is before or after 6pm currently. You then simply select your data where dt is between those two dates (or use <= and > or whatever you need to include or exclude the relevant rows) e.g....
DECLARE #t table (ID int,Val varchar(30),dt datetime2(3))
INSERT #t(ID,Val,dt)
VALUES(1,'Mohan' ,'2017-10-13T19:02:49.493'),
(2,'Manasa','2017-10-12T20:02:49.493'),
(3,'maren' ,'2017-10-13T07:02:49.493');
DECLARE #now datetime2 = dateadd(HOUR,12,sysdatetime());
SELECT *,
#now,
x.Today6pm,
x.Yesterday6pm
FROM #t AS t
CROSS APPLY (VALUES(DATEADD(day, DATEDIFF(day,'19000101',cast(#now AS date))-(CASE WHEN cast(#now AS time) < timefromparts(18,00,00,0,0) THEN 1 ELSE 0 END), CAST(timefromparts(18,00,00,0,0) AS DATETIME2(7))),
DATEADD(day, DATEDIFF(day,'19000101',cast(#now AS date))+(CASE WHEN cast(#now AS time) > timefromparts(18,00,00,0,0) THEN 1 ELSE 0 END), CAST(timefromparts(18,00,00,0,0) AS DATETIME2(7))))) x(Yesterday6pm,Today6pm)
WHERE dt BETWEEN x.Yesterday6pm AND x.Today6pm
The CROSS APPLY here is to simplfy the code otherwise you could put those functions directly in the WHERE predicate.
the variable #now is simply used for testing to ensure the adjustment works rather than waiting until 6:01pm this evening. Obviously in your code just replace #now with sysdatetime().
Note: if your dates are datetime then adding a date and time together "works" but it does not work for datetime2. The above approach will work for both datetime and datetime2 so is more resilient.
DECLARE #T TABLE
(
ID INT,
Val VARCHAR(50) ,
dt DATETIME
)
INSERT INTO #T
VALUES
(1, 'Mohan ', '2017-10-13 13:02:49.493'),
(2, 'Manasa ', '2017-10-12 20:02:49.493'),
(3, 'maren ', '2017-10-13 18:02:49.493')
DECLARE #CURRENT datetime = '13 oct 2017 18:00'
SELECT
*
FROM #T
WHERE
(
CAST(#CURRENT as time) BETWEEN '00:00' AND '18:00' AND
dt BETWEEN DATEADD(day,-1,DATEADD(hh,18,CAST(CAST(#CURRENT as date) as datetime))) AND DATEADD(hh,18,CAST(CAST(#CURRENT as date) as datetime))
) OR
(
CAST(#CURRENT as time) NOT BETWEEN '00:00' AND '18:00' AND
dt >=DATEADD(hh,18,CAST(CAST(#CURRENT as date) as datetime))
)
You can try this script.
SELECT * FROM MyTable WHERE
( (CAST(GETDATE() AS TIME) < '18:00')
AND dt < DATEADD(HOUR,18, CAST(CAST(GETDATE() AS DATE) AS DATETIME) ) )
OR
(( CAST(GETDATE() AS TIME) >= '18:00' )
AND dt >= DATEADD(HOUR,18, CAST(CAST(GETDATE() AS DATE) AS DATETIME) ) )
Try this:
DECLARE #Now DATETIME, #FromDate DATETIME
SET #Now = GETDATE()
SET #FromDate = DATEADD( HOUR, 18, DATEADD( DAY, DATEDIFF( DAY, 0, #Now )
- ( CASE WHEN DATEPART( HOUR, #Now ) < 18 THEN 1 ELSE 0 END ), 0 ))
SELECT #Now, #FromDate
SELECT *
FROM #tab
WHERE #FromDate < dt AND dt <= #Now
Result:
Now FromDate
----------------------- -----------------------
2017-10-13 19:36:40.963 2017-10-13 18:00:00.000
ID Val dt
----------- ---------- -----------------------
3 maren 2017-10-13 18:02:49.493
Explanation:
#FromDate is calculated as follows:
( CASE WHEN DATEPART( HOUR, #Now ) < 18 THEN 1 ELSE 0 END ) - if hour is less than 18 (6PM) then return 1
DATEADD( HOUR, 18, DATEADD( DAY, DATEDIFF( DAY, 0, #Now ) - ... , 0 )) - return "6PM date", which is either yesterday or today depending on the result of the CASE expression above
You could try this:
SELECT * FROM [yourTable] WHERE
(dt between CONCAT(CAST(GETDATE() AS DATE), ' ', '18:00:00.000') AND
CONCAT(CAST(GETDATE() AS DATE), ' ', '23:59:59.999')
AND HOUR(GETDATE()) > 18)
OR
(dt between CONCAT(CAST(dt AS DATE) - INTERVAL 1 DAY, ' ', '18:00:00.000')
AND GETDATE()
AND HOUR(GETDATE()) < 18)

Conditional Count On Row_Number

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!

calculating the weekly hours worked and excluding the weekends using SQL

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

Eliminate and reduce overlapping data ranges using SQL

i got a dataset in SQL Server Management Studio. The data looks like the following. i have a identifier for each people userID, date of the record, start timestartime and finish time endtime.
UserID date startime endtime
1 20110203 09:30 09:35
1 20110203 09:31 09:38
1 20110203 10:03 10:05
1 20110203 10:04:00 10:35:00
2 20110203 11:02 11:05
For each people, i want check if there is any overlapping time. If there is, I want to keep the smallest startime and largest endtime. if no overlapping time, I keep the original data. In addition, I want to calculate the duration of maxi endtime and smallest startime.
The result I want should looks like the following. Can anyone teach me how to code this please.
UserID date startime endtime diff
1 20110203 09:30 09:38 00:08
1 20110203 10:03 10:35 00:02
2 20110203 11:02 11:05 00:03
It seems that SELECT with CTE needs to recursively merge undetermined number of rows. In that case I would prefer safe CURSOR based solution:
DECLARE #t TABLE
(
UserId int,
[Date] date,
StartTime time,
EndTime time
);
INSERT INTO #t VALUES
(1, '2011-02-03', '09:30:00', '09:35:00'),
(1, '2011-02-03', '09:31:00', '09:38:00'),
(1, '2011-02-03', '09:36:00', '09:41:00'),
(1, '2011-02-03', '09:40:00', '09:45:00'),
(1, '2011-02-03', '09:42:00', '09:43:00'),
(1, '2011-02-03', '10:03:00', '10:05:00'),
(2, '2011-02-03', '11:02:00', '11:05:00'),
(1, '2011-02-03', '12:00:00', '12:05:00'),
(1, '2011-02-03', '12:04:00', '12:06:00');
------------------
DECLARE #result TABLE
(
UserId int,
[Date] date,
StartTime time,
EndTime time
)
DECLARE cur CURSOR FOR
SELECT UserId, [Date], StartTime, EndTime
FROM #t
ORDER BY UserId, [Date], StartTime;
DECLARE #UserId int
DECLARE #Date date
DECLARE #StartTime time
DECLARE #EndTime time
DECLARE #LastUserId int
DECLARE #LastDate date
DECLARE #LastStartTime time
DECLARE #LastEndTime time
OPEN cur
FETCH NEXT FROM cur INTO #UserId, #Date, #StartTime, #EndTime
SET #LastUserId = #UserId
SET #LastDate = #Date
SET #LastStartTime = #StartTime
SET #LastEndTime = #EndTime
WHILE ##FETCH_STATUS = 0
BEGIN
IF #UserId = #LastUserId AND #Date = #LastDate AND #StartTime <= #LastEndTime
SET #LastEndTime = CASE WHEN #LastEndTime > #EndTime THEN #LastEndTime ELSE #EndTime END
ELSE
BEGIN
INSERT #result(UserId, [Date], StartTime, EndTime) VALUES (#LastUserId, #LastDate, #LastStartTime, #LastEndTime)
SET #LastUserId = #UserId
SET #LastDate = #Date
SET #LastStartTime = #StartTime
SET #LastEndTime = #EndTime
END
FETCH NEXT FROM cur INTO #UserId, #Date, #StartTime, #EndTime
END
INSERT #result(UserId, [Date], StartTime, EndTime) VALUES (#LastUserId, #LastDate, #LastStartTime, #LastEndTime)
CLOSE cur
DEALLOCATE cur
SELECT UserId,
[Date],
StartTime,
EndTime,
CAST(DATEADD(second,DATEDIFF(second,StartTime,EndTime),'2000-01-01') AS time) Diff
FROM #result
which returns
1 2011-02-03 09:30:00.0000000 09:45:00.0000000 00:15:00.0000000
1 2011-02-03 10:03:00.0000000 10:05:00.0000000 00:02:00.0000000
1 2011-02-03 12:00:00.0000000 12:06:00.0000000 00:06:00.0000000
2 2011-02-03 11:02:00.0000000 11:05:00.0000000 00:03:00.0000000
Following a redesigned Version of my previous cte Approach. However, it will still have Problems if there are multiple records for the same user with identical start time... didn't have time to fix that one, but as far as I understood this is not possible in the described process!?
--
-- This part is temporary and has to be replaced by your tables
-- There several more records included now
-- There is still a glitch if the starttime is identical for two records - but as far as I understood, this is not possible in the described case?
--
declare #t table (userid int, date int, starttime time, endtime time);
insert into #t values (1, 20110203, '09:30:00', '09:35:00'), (1, 20110203, '09:31:00', '09:38:00'), (1, 20110203, '09:36:00', '09:41:00'), (1, 20110203, '10:03:00', '10:05:00'),(1, 20110203, '10:04:00', '10:35:00'),
(2, 20110203, '11:02:00', '11:05:00'), (2, 20110203, '11:03:00', '11:20:00'), (2, 20110203, '11:04:00', '11:35:00'), (2, 20110203, '13:02:00', '13:05:00'), (2, 20110203, '13:04:00', '13:15:00');
--
-- First cte: selects all start and endtimes and their - if existing - "overlaps"; recursive cte
--
WITH cte AS(
SELECT 1 lvl, a.userid
,CASE WHEN a.starttime <= ISNULL(b.starttime, a.starttime) THEN a.starttime ELSE b.starttime END AS starttime
,CASE WHEN a.endtime >= ISNULL(b.endtime, a.endtime) THEN a.endtime ELSE b.endtime END AS endtime
FROM #t as a
LEFT OUTER JOIN #t AS b ON b.userid = a.userid
AND b.starttime < a.starttime
AND b.endtime > a.starttime
UNION ALL
select a.lvl+1, a.userid
,CASE WHEN a.starttime <= ISNULL(b.starttime, a.starttime) THEN a.starttime ELSE b.starttime END AS xStart
,CASE WHEN a.endtime >= ISNULL(b.endtime, a.endtime) THEN a.endtime ELSE b.endtime END AS xEnd
from cte as a
INNER JOIN #t AS b ON b.userid = a.userid
AND b.starttime < a.starttime
AND b.endtime > a.starttime
),
--
-- Second cte: get the max. lvl result per user from the recursive cte
--
cteUserMaxLvl AS (
SELECT userid, max(lvl) AS MaxLvl
FROM cte
GROUP BY userid
),
--
-- third cte: get the rows matching the max.lvl; their timespan should cover the desired min. start and max. end
--
cteNoMoreOverlap AS(
SELECT a.userid, starttime, endtime
FROM cte AS a
JOIN cteUserMaxLvl AS b ON a.userid = b.userid AND a.lvl = b.MaxLvl
)
--
-- Select the rows from the "No more overlap" cte
--
SELECT userid, starttime, endtime
FROM cteNoMoreOverlap
UNION ALL
--
-- And finally select all rows, which are not covered by the previously selected timespan
--
SELECT a.userid, min(a.starttime) AS starttime, max(a.endtime) AS endtime
FROM cte AS a
JOIN cteNoMoreOverlap AS b ON a.userid = b.userid AND a.starttime NOT BETWEEN b.starttime AND b.endtime
GROUP BY a.userid
order by userid, starttime, endtime
I believe when you say overlapping time, you are saying within the same hour on the same day. If that is what you mean, following solution might work. Attached is the screenshot of my results.
CREATE TABLE #OverlappingDates
(
UserID INT
, [date] DATE
, starttime VARCHAR(5)
, endtime VARCHAR(5)
);
INSERT INTO #OverlappingDates
( UserID, date, starttime, endtime )
VALUES ( 1 -- UserID - int
, '20110203' -- date - date
, '09:30' -- starttime - time
, '09:35' -- endtime - time
),
( 1 -- UserID - int
, '20110203' -- date - date
, '09:31' -- starttime - time
, '09:38' -- endtime - time
),
( 1 -- UserID - int
, '20110203' -- date - date
, '10:03' -- starttime - time
, '10:05' -- endtime - time
),
( 2 -- UserID - int
, '20110203' -- date - date
, '11:02' -- starttime - time
, '11:05' -- endtime - time
),
( 2 -- UserID - int
, '20110203' -- date - date
, '11:05' -- starttime - time
, '11:15' -- endtime - time
),
( 2 -- UserID - int
, '20110203' -- date - date
, '11:05' -- starttime - time
, '12:00' -- endtime - time
);
WITH cte
AS ( SELECT UserID
, date
, MIN(starttime) AS StartTime
, MAX(endtime) AS EndTime
FROM #OverlappingDates
GROUP BY UserID
, date
, LEFT(starttime, 2)
, LEFT(endtime, 2)
)
SELECT cte.UserID
, cte.date
, cte.StartTime
, cte.EndTime
, ( RIGHT('0'
+ CAST(( DATEDIFF(SECOND,
( CAST(CONCAT(( CAST(cte.[date] AS VARCHAR(10)) ),
' ', cte.StartTime) AS DATETIME) ),
( CAST(CONCAT(( CAST(cte.[date] AS VARCHAR(10)) ),
' ', cte.EndTime) AS DATETIME) )) )
/ 3600 AS VARCHAR(2)), 2) + ':' + RIGHT('0'
+ CAST(( ( DATEDIFF(SECOND,
( CAST(CONCAT(( CAST(cte.[date] AS VARCHAR(10)) ),
' ',
cte.StartTime) AS DATETIME) ),
( CAST(CONCAT(( CAST(cte.[date] AS VARCHAR(10)) ),
' ', cte.EndTime) AS DATETIME) )) )
/ 60 ) % 60 AS VARCHAR(2)),
2) ) AS Diff
FROM cte;

Get every hour for a time range

So what I am trying to is generate all the hours that are inside a specific time range.
So given the range 11 AM to 2:00 PM, I would get:
11:00 AM
12:00 PM
1:00 PM
2:00 PM
I am trying to avoid having to store every specific hour a store might be open and just store the range (I need to compare the hours against other times)
Thanks
No loops, recursive CTEs or numbers table required.
DECLARE
#start TIME(0) = '11:00 AM',
#end TIME(0) = '2:00 PM';
WITH x(n) AS
(
SELECT TOP (DATEDIFF(HOUR, #start, #end) + 1)
rn = ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_columns ORDER BY [object_id]
)
SELECT t = DATEADD(HOUR, n-1, #start) FROM x ORDER BY t;
You could use a recursive CTE. This would generate the hours between 11 and 14:
;with Hours as
(
select 11 as hr
union all
select hr + 1
from Hours
where hr < 14
)
select *
from Hours
Live example at SQL Fiddle.
If you have a numbers table (click the link to create one if you don't)...
create table test(
startTime time
, endTime time
)
insert into test
select '11:00', '14:00'
select
dateadd(hh, n.n, t.startTime) as times
from test t
inner join Numbers n
-- assuming your numbers start at 1 rather than 0
on n.n-1 <= datediff(hh, t.startTime, t.endTime)
If this is specialized, you can create an hours table with just 24 values.
create table HoursInADay(
[hours] time not null
, constraint PK_HoursInADay primary key ([hours])
)
-- insert
insert into HoursInADay select '1:00'
insert into HoursInADay select '2:00'
insert into HoursInADay select '3:00'
insert into HoursInADay select '4:00'
insert into HoursInADay select '5:00'
insert into HoursInADay select '6:00'
insert into HoursInADay select '7:00'
...
select
h.[hours]
from test t
inner join HoursInADay h
on h.[hours] between t.startTime and t.endTime
The easiest way I can think of to do this is to have only 1 permanent table with a list of all hours; 24 entries total.
Create table dbo.Hours (Hourly_Time Time NOT NULL)
Insert into dbo.Hours ...
Then, given times A & B:
select * from dbo.Hours where Hourly_Time<=A and Hourly_Time>=B
#Andomar Thanks a lot, you helped me, there is my add above your code.
*----------------------------
create view vw_hoursalot as
with Hours as
(
select DATEADD(
dd, 0, DATEDIFF(
dd, 0, DATEADD (
year , -5 , getDate()
)
)
) as dtHr
union all
select DATEADD (minute , 30 , dtHr )
from Hours
where dtHr < DATEADD(
dd, 0, DATEDIFF(
dd, 0, DATEADD (
year , +5 , getDate()
)
)
)
)
select * from Hours
----------------------------
select * from vw_hoursalot option (maxrecursion 0)
----------------------------*