Split date/time ranges into multiple rows - sql

I'm trying to split the date/time ranges in my dataset into multiple rows by hour in SQL Server. My current dataset looks like:
EmployeeCode StartDateTime EndDateTime
843578 2017-05-14 8:30 AM 2017-05-14 3:36 PM
587123 2017-05-14 9:00 AM 2017-05-14 6:28 PM
.....
And my current code seems like it's not catching both Date and Time element in the dataset. I want something like this as my final result.
EmployeeCode StartDateTime EndDateTime
843578 2017-05-14 8:30 AM 2017-05-14 9:00 PM
843578 2017-05-14 9:00 AM 2017-05-14 10:00 AM
843578 2017-05-14 10:00 AM 2017-05-14 11:00 AM
843578 2017-05-14 11:00 AM 2017-05-14 12:00 PM
843578 2017-05-14 12:00 PM 2017-05-14 01:00 PM
843578 2017-05-14 01:00 PM 2017-05-14 02:00 PM
843578 2017-05-14 02:00 PM 2017-05-14 03:00 PM
843578 2017-05-14 03:00 PM 2017-05-14 03:36 PM
587123 2017-05-14 9:00 AM 2017-05-14 10:00 AM
...............
I want to capture both date and time information and expand it to the rows by hour. Also, if an entry starts at 8:30 I want to split it as a separate unit such as 8:30 to 9:00 and the same logic is applied to the end time. (e.g. 3:00 - 3:36) My current code is not complete and I'd appreciate how I should rewrite the code to achieve what I mentioned above.
Select EmployeeCode, StartDateTime, EndDateTime
,StartTime = case when N=datepart(hour,StartDateTime) then StartTime else TimeFromParts(N,0,0,0,0)
end
,EndTime = case when N=datepart(hour,EndDateTime) then EndTime else
TimeFromParts(N+1,0,0,0,0) end
From mytable T
Join ( values (0),(1),(2),(3),(4),(5),(6)
,(7),(8),(9),(10),(11),(12),(13)
,(14),(15),(16),(17),(18),(19),(20)
,(21),(22),(23)
) B(N)
on N between datepart(hour,StartDateTime) and datepart(hour,EndDateTime)

This is a bit messy, but using a few table constructors, you can create a tally and time ranges. Then you use a CASE expression to return the relevant values:
SELECT YT.EmployeeCode,
CASE WHEN YT.StartDateTime > DT.StartDateTime THEN YT.StartDateTime ELSE DT.StartDateTime END AS StartDateTime,
CASE WHEN YT.EndDateTime < DT.EndDateTime THEN YT.EndDateTime ELSE DT.EndDateTime END AS StartDateTime
FROM (VALUES(843578,CONVERT(datetime2(0),'2017-05-14T08:30:00'),CONVERT(datetime2(0),'2017-05-14T15:36:00')),
(587123,CONVERT(datetime2(0),'2017-05-14T09:00:00'),CONVERT(datetime2(0),'2017-05-14T18:28:00')))YT(EmployeeCode,StartDateTime,EndDateTime)
CROSS APPLY (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14),(15),(16),(17),(18),(19),(20),(21),(22),(23))T(I)
CROSS APPLY (VALUES(DATEADD(HOUR,T.I,CONVERT(time(0),'00:00:00')),DATEADD(HOUR,T.I+1,CONVERT(time(0),'00:00:00'))))V(StartTime,EndTime)
CROSS APPLY (VALUES(DATETIMEFROMPARTS(YEAR(YT.StartDateTime),MONTH(YT.StartDateTime),DAY(YT.StartDateTime),DATEPART(HOUR,V.StartTime),DATEPART(MINUTE,V.StartTime),0,0),
DATETIMEFROMPARTS(YEAR(YT.StartDateTime),MONTH(YT.StartDateTime),DAY(YT.StartDateTime),DATEPART(HOUR,V.EndTime),DATEPART(MINUTE,V.EndTime),0,0)))DT(StartDateTime,EndDateTime)
WHERE YT.StartDateTime <= DT.EndDateTime
AND YT.EndDateTime >= DT.StartDateTime;
From the comments, it looks(?) like the OP is using a Procedure to get the data, not a table as alluded to in the question, as they say that they can't use a FROM. You'll need to create a table, and then INSERT that data into it and reference it in the FROM:
CREATE TABLE #t (EmployeeCode int,
StartDatetime datetime2(0),
EndDateTime datetime2(0));
INSERT INTO #t
EXEC dbo.YourProc {Parameters?};
--Then prior solution:
SELECT YT.EmployeeCode,
...
FROM #T YT
CROSS APPLY (VALUES(0),(1)....

Does this do what you need?
Select
EmployeeCode
, final.startDateTime
, final.endDateTime
from
myTable as employee
cross apply ( Select
startDate = convert(date, startDateTime)
, startHour = datepart(hour, startDateTime)
, endDate = convert(date, endDateTime)
, endHour = datepart(hour, endDateTime)
) as times
cross apply (Select
roundedStart = dateadd(hour, startHour, convert(datetime, startDate))
, roundedEnd = dateAdd(hour, endHour + 1, convert(datetime, endDate))
) as rounding
cross apply (Select
[hours] = datediff(hour,roundedStart, roundedEnd)
) as diff
cross apply (Select
increment = ROW_NUMBER() over (order by (select NULL))
from string_split(replicate(' ',diff.[hours] - 1),' ')
) as repeater
cross apply ( Select
startIncrement = dateadd(hour, repeater.increment - 1, roundedStart)
, endInrement = dateadd(hour, repeater.increment, roundedStart)
) as timeIncrements
cross apply (Select
startDateTime = max(startDT)
, endDateTime = min(endDT)
from (values (employee.startDateTime, employee.endDateTime),
(startIncrement, endInrement) ) s (startDT, endDT)
) as final

This seems like a good place to use a recursive CTE:
with cte as (
select employeecode, startdatetime as starthour,
dateadd(hour, datepart(hour, startdatetime) + 1, convert(datetime, convert(date, startdatetime))) as endhour,
enddatetime
from t
union all
select employeecode, endhour,
(case when dateadd(hour, 1, endhour) > enddatetime then enddatetime else dateadd(hour, 1, endhour) end),
enddatetime
from cte
where endhour < enddatetime
)
select *
from cte;
Here is a db<>fiddle.
If you can have more than 100 hours in a period (which seems unlikely because the periods appear to be on the same day), then you need option (maxrecursion 0).

Related

Time until midnight between two date and time columns

I need help with my query to calculate the time until midnight between two date and time columns
break down by day
This is the main table:
ID
Start_Time
End_time
DateDiff
32221
01-01-2022 13:10:00
01-03-2022 13:10:00
2880
My query:
SELECT
start_time.ID,
start_time.Date_Time AS Start_time,
end_time.Date_Time AS End_time,
DATEDIFF(minute, start_time.Date_Time, end_time.Date_Time) AS DateDiff
FROM
Main
what I need is similar to this:
ID
Date_start
End_time
DateDiff
32221
01-01-2022 13:10:00
01-01-2022 23:59:59
654
32221
01-02-2022 00:00:00
01-02-2022 23:59:59
1440
32221
01-03-2022 00:00:00
01-03-2022 13:10:00
781
how i can do that?
You can loop through the times, always adding the time untill midnight, untill your 'start_time + 1 day' is bigger than your end_time.
The below code can be run directly in SQL (mind the date notation, my SQL is in united states notation, so if yours is in Europe this will give you back results for 3 months instead of 3 days);
DECLARE #start_time datetime2 = '01/01/2022 13:00:00';
DECLARE #end_time datetime2 = '03/01/2022 14:00:00';
DECLARE #daily_end_time datetime2=NULL;
DECLARE #Table Table (start_time datetime2, end_time datetime2, diff nvarchar(8));
DECLARE #diff_minutes_start int = DATEDIFF(MINUTE,#start_time,DateDiff(day,0,dateadd(day,1,#start_time)));
DECLARE #diff_minutes_end int = DATEDIFF(minute,#end_time,DateDiff(day,0,dateadd(day,1,#end_time)))
SET #daily_end_time = DATEADD(mi,#diff_minutes_start,#start_time)
WHILE #daily_end_time < #end_time
BEGIN
INSERT INTO #Table (start_time,end_time,diff)
VALUES (
#start_time,
CASE WHEN DATEADD(day,1,#daily_end_time) > #end_time THEN #end_time ELSE
#daily_end_time END,
CASE WHEN DATEADD(day,1,#daily_end_time) > #end_time THEN #diff_minutes_end ELSE
#diff_minutes_start END )
SET #daily_end_time = DATEADD(mi,#diff_minutes_start,#start_time)
SET #start_time = DATEADD(mi,1,#daily_end_time);
select #diff_minutes_start =
DATEDIFF(MINUTE,#start_time,DateDiff(day,0,dateadd(day,1,#start_time)));
select #diff_minutes_end = DATEDIFF(minute,#end_time,DateDiff(day,0,dateadd(day,1,#end_time)))
END
SELECT * FROM #Table
And the results:
You may use a recursive CTE as the following:
With CTE As
(
Select ID, Start_Time, End_time, DATEADD(Second, -1, DATEADD(Day, DATEDIFF(Day,0, Start_Time), 1)) et
From main
Union All
Select C.ID, DATEADD(Second, 1, C.et), C.End_time, DATEADD(Day, 1, C.et)
From CTE C Join main T
On C.ID = T.ID
Where DATEADD(Second, 1, C.et) <= C.End_time
)
Select ID, Start_Time,
Case When End_Time <= et Then End_Time Else et End As End_Time,
DATEDIFF(Minute, Start_Time, DATEADD(Second, 1, Case When End_Time <= et Then End_Time Else et End)) As [DateDiff]
From CTE
Order By ID, Start_Time
See a demo with extended data sample from db<>fiddle.
You can also solve this with a tally table, using the expanded (to show different cases) sample data
ID
StartTime
EndTime
32221
2022-01-01 13:10:00
2022-01-03 13:10:00
32222
2022-02-02 10:10:00
2022-02-02 17:10:00
32223
2022-03-03 19:10:00
2022-03-04 08:10:00
32224
2022-04-04 19:10:00
2022-04-08 08:10:00
and the code
with cteSampleData as ( --Enter some sample data, include spans of 0, 1, and >1 days
SELECT * --Note that we need CONVERT to make sure the dates are treated as datetime not string!
FROM (VALUES(32221, CONVERT(datetime2(0), '01-01-2022 13:10:00'), CONVERT(datetime2(0), '01-03-2022 13:10:00') )
, (32222, '02-02-2022 10:10:00', '02-02-2022 17:10:00')
, (32223, '03-03-2022 19:10:00', '03-04-2022 08:10:00')
, (32224, '04-04-2022 19:10:00', '04-08-2022 08:10:00')
) as Samp(ID, StartTime, EndTime)
), cteWithControl as ( --Add some fields to make testing cledarer - you could do this as part of a subsequent step instead
SELECT *
, CONVERT(date, StartTime) as StartDate , CONVERT(date, EndTime) as EndDate
, DATEDIFF(day, StartTime , EndTime) as DiffDays
--, DATEDIFF(day, CONVERT(date, StartTime) , CONVERT(date, EndTime)) as DiffDays
FROM cteSampleData
), cteTally as ( --Get a list of integers to represent days, assume nothing lasts longer than a year
SELECT top 365 ROW_NUMBER() over (ORDER BY name) as Tally
FROM sys.objects --just a table we know has over 300 rows, look up tally tables for other generation methods
)--The real work begins below, partition the data into "same day" and "multi-day" spans
, cteSet as (
SELECT ID, StartTime, EndTime, DiffDays, 1 as DayNumber
FROM cteWithControl WHERE DiffDays = 0
UNION ALL
SELECT ID --For multi-day, cross with the tally table and treat first and last days special
, CASE WHEN T.Tally = 1 THEN StartTime --For the first day the start time is the real time
ELSE DATEADD (day, T.Tally - 1, startdate) END as StartTime --Otherwise it's the start of the day
, CASE WHEN T.Tally = DiffDays + 1 THEN EndTime --For the last day the end is the real end
ELSE DATEADD (second, -1, CONVERT(DATETIME2(0), DATEADD (day, T.Tally, startdate)))
END as EndTime --otherwise 1 second less than the next day
, DiffDays, Tally as DayNumber
FROM cteWithControl as D CROSS JOIN cteTally as T
WHERE DiffDays > 0 AND T.Tally <= D.DiffDays + 1
)--Now we display the results and calculate the length (in minutes) of each span
SELECT *
, DATEDIFF(MINUTE, StartTime, EndTime) as DateDiff
FROM cteSet
ORDER BY ID, DayNumber
we get the output
ID
StartTime
EndTime
DiffDays
DayNumber
DateDiff
32221
2022-01-01 13:10:00
2022-01-01 23:59:59
2
1
649
32221
2022-01-02 00:00:00
2022-01-02 23:59:59
2
2
1439
32221
2022-01-03 00:00:00
2022-01-03 13:10:00
2
3
790
32222
2022-02-02 10:10:00
2022-02-02 17:10:00
0
1
420
32223
2022-03-03 19:10:00
2022-03-03 23:59:59
1
1
289
32223
2022-03-04 00:00:00
2022-03-04 08:10:00
1
2
490
32224
2022-04-04 19:10:00
2022-04-04 23:59:59
4
1
289
32224
2022-04-05 00:00:00
2022-04-05 23:59:59
4
2
1439
32224
2022-04-06 00:00:00
2022-04-06 23:59:59
4
3
1439
32224
2022-04-07 00:00:00
2022-04-07 23:59:59
4
4
1439
32224
2022-04-08 00:00:00
2022-04-08 08:10:00
4
5
490

Insert date and time in SQL

i want to generate and insert date with time to table but dont quite know how.
I need to insert date for every day to three years ahead with time
01.01.2023 07:00
01.01.2023 08:00
01.01.2023 09:00
02.01.2023 07:00
02.01.2023 08:00
02.01.2023 09:00
03.01.2023 07:00 and so on
Recursive approach:
DECLARE
#StartDate DATE,
#EndDate DATE
SELECT
#StartDate = '2023-01-01',
#EndDate = DATEADD(YEAR, 3, #StartDate);
WITH DatesCTE(RecDate) AS (
SELECT #StartDate
UNION ALL
SELECT DATEADD(DAY, 1, RecDate) FROM DatesCTE WHERE RecDate < #EndDate
)
SELECT CAST(RecDate AS VARCHAR(10)) + ' 07:00' AS dt FROM DatesCTE
UNION ALL
SELECT CAST(RecDate AS VARCHAR(10)) + ' 08:00' AS dt FROM DatesCTE
UNION ALL
SELECT CAST(RecDate AS VARCHAR(10)) + ' 09:00' AS dt FROM DatesCTE
ORDER BY dt ASC
OPTION (MAXRECURSION 0)

How to split the time range into multiple rows

I want to split the date/time ranges into multiple rows by hour in SQL Server but have some issues. My current dataset looks like this:
EmployeeCode StartDateTime EndDateTime
843578 2017-05-14 8:30 AM 2017-05-14 11:36 PM
587123 2017-05-14 22:00 PM 2017-05-15 01:28 AM
And I want something like this as my result table. Note that I want to treat a block less than an hour as one independent row as well. (For example 8:30AM - 9:00AM as one row.)
EmployeeCode StartDateTime EndDateTime
843578 2017-05-14 8:30 AM 2017-05-14 9:00 PM
843578 2017-05-14 9:00 AM 2017-05-14 10:00 AM
843578 2017-05-14 10:00 AM 2017-05-14 11:00 AM
843578 2017-05-14 11:00 AM 2017-05-14 11:36 AM
587123 2017-05-14 22:00 PM 2017-05-14 23:00 PM
587123 2017-05-14 23:00 PM 2017-05-15 00:00 AM
587123 2017-05-15 00:00 AM 2017-05-15 01:00 AM
587123 2017-05-15 01:00 AM 2017-05-15 01:28 AM
My current code only splits the date/time range that is within the same day. For example, the time range for Employee 587123 stops the spliting at 22:00 - 23:00 and doesn't work for the time range in next day.
How do I update my code to capture data after midnight? (The last three rows in the sample result table.)
Here's my current code
SELECT YT.EmployeeCode,
CASE WHEN YT.StartDateTime > DT.StartDateTime THEN YT.StartDateTime ELSE DT.StartDateTime END AS StartDateTime,
CASE WHEN YT.EndDateTime < DT.EndDateTime THEN YT.EndDateTime ELSE DT.EndDateTime END AS StartDateTime
FROM (VALUES(843578,CONVERT(datetime2(0),'2017-05-14T08:30:00'),CONVERT(datetime2(0),'2017-05-14T15:36:00')),
(587123,CONVERT(datetime2(0),'2017-05-14T09:00:00'),CONVERT(datetime2(0),'2017-05-14T18:28:00')))YT(EmployeeCode,StartDateTime,EndDateTime)
CROSS APPLY (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14),(15),(16),(17),(18),(19),(20),(21),(22),(23))T(I)
CROSS APPLY (VALUES(DATEADD(HOUR,T.I,CONVERT(time(0),'00:00:00')),DATEADD(HOUR,T.I+1,CONVERT(time(0),'00:00:00'))))V(StartTime,EndTime)
CROSS APPLY (VALUES(DATETIMEFROMPARTS(YEAR(YT.StartDateTime),MONTH(YT.StartDateTime),DAY(YT.StartDateTime),DATEPART(HOUR,V.StartTime),DATEPART(MINUTE,V.StartTime),0,0),
DATETIMEFROMPARTS(YEAR(YT.StartDateTime),MONTH(YT.StartDateTime),DAY(YT.StartDateTime),DATEPART(HOUR,V.EndTime),DATEPART(MINUTE,V.EndTime),0,0)))DT(StartDateTime,EndDateTime)
WHERE YT.StartDateTime <= DT.EndDateTime
AND YT.EndDateTime >= DT.StartDateTime;
The current code looks too complicated so if you know better way to do this, please let me know.
I'd appreciate any help on this.
Here is a recursive CTE solution:
with cte as (
select
employeecode,
startdatetime,
dateadd(hour, 1, datetimefromparts(year(startdatetime), month(startdatetime), day(startdatetime), datepart(hour, startdatetime), 0, 0, 0)) enddatetime
enddatetime maxdatetime
from mytable
union all
select employeecode, enddatetime, dateadd(hour, 1, enddatetime), maxdatetime
from cte
where enddatetime < maxdatetime
)
select employeecode, startdatetime,
case when enddatetime < maxdatetime then enddatetime else maxdatetime end as enddatetime
from cte
Basically, the anchor of the CTE performs computes the end of the first range, using datetimefrompart(). Then we iteratively generate the following ranges, until the maximum date time is reached. We can then display the results with the outer query, while adjusting the end date of the last range.
I would approach this using a recursive CTE:
with cte as (
select t.EmployeeCode, t.StartDateTime as startdt,
dateadd(hour, datepart(hour, t.startdatetime) + 1, convert(datetime, convert(date, t.StartDateTime))) as enddt,
t.endDateTime, 1 as lev
from t
union all
select cte.employeecode, enddt,
(case when dateadd(hour, 1, enddt) < enddatetime then dateadd(hour, 1, enddt) else enddatetime end),
enddatetime, lev + 1
from cte
where enddt < enddatetime
)
select *
from cte
order by employeecode, startdt;
Here is a db<>fiddle.
If you might have spans of more than 100 hours, then you need option (maxrecursion 0) for the query.

Find Overlap In Time Records

I have a table in a SQL Server 2012 database that logs events, with columns StartDate and EndDate. I need to aggregate all of the records within a certain time period, and determine the duration of time where any events were active, not counting overlapping durations. For example, if my table looks like this:
id StartDate EndDate
=======================================================
1 2017-08-28 12:00:00 PM 2017-08-28 12:01:00 PM
2 2017-08-28 1:15:00 PM 2017-08-28 1:17:00 PM
3 2017-08-28 1:16:00 PM 2017-08-28 1:20:00 PM
4 2017-08-28 1:30:00 PM 2017-08-28 1:35:00 PM
And my time period to search was from 2017-08-28 12:00:00 PM to 2017-08-28 2:00:00 PM, then my desired output should be:
Duration of Events Active = 00:11:00
I have been able to aggregate the records and get a total duration (basically just EndDate - StartDate), but I cannot figure out how to exclude overlapping time. Any help would be appreciated. Thanks!
How about a CTE and aggregation? This can be done with a sub-query too.
declare #table table (id int, StartDate datetime, EndDate datetime)
insert into #table
values
( 1,'2017-08-28 12:00:00 PM','2017-08-28 12:01:00 PM'),
(2,'2017-08-28 1:15:00 PM','2017-08-28 1:17:00 PM'),
(3,'2017-08-28 1:16:00 PM','2017-08-28 1:20:00 PM'),
(4,'2017-08-28 1:30:00 PM','2017-08-28 1:35:00 PM')
declare #StartDate datetime = '2017-08-28 12:00:00'
declare #EndDate datetime = '2017-08-28 14:00:00'
;with cte as(
select
id
,StartDate = case when StartDate < lag(EndDate) over (order by id) then lag(EndDate) over (order by id) else StartDate end
,EndDate
from
#table
where
StartDate >= #StartDate
and EndDate <= #EndDate),
cte2 as(
select
Dur = datediff(second, StartDate, EndDate)
from cte)
select
Dur = convert(varchar, dateadd(ms, sum(Dur) * 1000, 0), 114)
from
cte2

Query should be changed

I have one query this query display will be based on the day which day the hours is there based on the week it will display.
In this hours table my start date and end date starts from 2014-01-06 2014-01-12 one week..
for eg:
declare #Hours as Table ( EmpId Int, Monday Time, Tuesday Time, Wednesday Time,
Thursday Time, Friday Time, Saturday Time, Sunday Time, StartDate Date, EndDate Date );
insert into #Hours
( EmpId, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday, StartDate, EndDate )
values
( 1, '08:00', '00:00', '00:00', '00:00', '00:00', '00:00', '00:00', '2014-01-06 00:00:00.000', '2014-01-12 00:00:00.000' );
select * from #Hours;
with Week as (
-- Build a table of all of the dates in the work week.
select StartDate as WorkDate, EndDate
from #Hours
union all
select DateAdd( day, 1, WorkDate ), EndDate
from Week
where WorkDate < EndDate ),
DaysHours as (
-- Build a table of the hours assigned to each date.
select EmpId,
case DatePart( weekday, W.WorkDate )
when 1 then H.Monday
when 2 then H.Tuesday
when 3 then H.Wednesday
when 4 then H.Thursday
when 5 then H.Friday
when 6 then H.Saturday
when 7 then H.Sunday
end as Hours,
WorkDate as StartDate, WorkDate as EndDate
from Week as W inner join
#Hours as H on H.StartDate <= W.WorkDate and W.WorkDate <= H.EndDate )
-- Output the non-zero hours.
select EmpId, Hours,StartDate as StartDate, EndDate
from DaysHours
where Hours <> Cast( '00:00' as Time );
need the output like this:
EmpId Hours StartDate EndDate
----------- ---------------- ---------- ----------
1 08:00:00.0000000 2014-01-06 2014-01-06
because monday only we have the value.but my output like this
EmpId Hours StartDate EndDate
----------- ---------------- ---------- ----------
1 08:00:00.0000000 2014-01-12 2014-01-12
it should work based on which day the value is there.
Can any body alter in this query itself.
Thanks,
After execution got
EmpId Hours StartDate EndDate
1 08:00:00.0000000 2014-01-06 2014-01-06
MS SQL Server 2008 R2
Try this query:
SELECT x.EmpId, ca.Hours, ca.CurrentDate AS StartDate, ca.CurrentDate AS EndDate
FROM #Hours x
CROSS APPLY
(
SELECT y.Hours, DATEADD(DAY, y.NumOfDays, x.StartDate)
FROM
(
SELECT x.Monday, 0
UNION ALL SELECT x.Tuesday, 1
UNION ALL SELECT x.Wednesday, 2
UNION ALL SELECT x.Thursday, 3
UNION ALL SELECT x.Friday, 4
UNION ALL SELECT x.Saturday, 5
UNION ALL SELECT x.Sunday, 6
) y (Hours, NumOfDays)
WHERE y.Hours <> '00:00:00.0000000'
) ca (Hours, CurrentDate);