How to split the time range into multiple rows - sql

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.

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

query to get data between date ranges and the padded data around

suppose this is my data in a table in the database
...
01/01/2016 00:00 367.2647688
01/06/2016 12:30 739.8067639 < INCLUDE THIS
01/01/2018 03:00 412.9686137
01/01/2018 03:30 150.6068046
01/01/2018 04:00 79.22204568
01/01/2018 04:30 648.702222
01/01/2018 09:00 75.41931365
01/01/2018 09:30 923.9435812
01/01/2018 10:00 342.9116004
02/01/2018 02:00 776.4855197 < INCLUDE THIS
08/04/2021 02:30 206.2066933
02/01/2022 03:00 852.9874735
02/01/2022 03:30 586.0818207
02/01/2022 04:00 363.5394613
02/01/2023 04:30 874.3073237
...
and this is my query to fetch data
SELECT * FROM MYTABLE WHERE [DATETIME] >= '2018/01/01 03:00' AND [DATETIME] < '2018/01/01 11:00'
I would also like the query to return one date before and after this range. so like the dates padded.
Now how can i do this efficiently. One way could be to get the dates in the ranges and then get all the data where they are less then min date and get the highest datetime of those and add to main range table also repeating this process for the max date. is there any other way?
You can use lead() and lag():
SELECT *
FROM (SELECT t.*,
LAG(DATETIME, 1, DATETIME) OVER (ORDER BY DATETIME) as PREV_DATETIME,
LEAD(DATETIME, 1, DATETIME) OVER (ORDER BY DATETIME) as NEXT_DATETIME
FROM MYTABLE t
) t
WHERE NEXT_DATETIME >= '2018-01-01 03:00:00' AND
PREV_DATETIME <= '2018-01-01 10:00:00'
Note: This uses default values to simplify the logic.
Here is a db<>fiddle. Based on the results you specified, I changed the last comparison to <= from <.
Lag and lead are window functions that are used to get preceding and succeeding value of any row within its partition. So when we want pad rows outside the range we can set offset parameter in both lag and lead functions.
SELECT t.Datetime,t.val FROM (SELECT T.*,LAG(Datetime,1,Datetime) over (order by Datetime) as lagdate,lead(Datetime,1,Datetime)over (order by Datetime) as leaddate FROM Mytable T)t
WHERE leaddate>= '2018/01/01 03:00' and lagdate<='2018/01/01 11:00'

Split date/time ranges into multiple rows

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

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

SQL - Order by time, then text on beginning

I have a query below :
SELECT DISTINCT TimeSched from tbl_schedule
It returns this result:
TimeSched
Rest Day
11:00 AM - 08:00 PM
No Schedule
09:00 AM - 06:00 PM
10:00 AM - 07:00 PM
When I use ORDER BY TimeSched, it returns this result:
TimeSched
09:00 AM - 06:00 PM
10:00 AM - 07:00 PM
11:00 AM - 08:00 PM
No Schedule
Rest Day
However, my desired result is I want 'Rest Day' and 'No Schedule' on the first and second row by default, then followed by the order of schedules in ascending order. As seen below :
TimeSched
Rest Day
No Schedule
09:00 AM - 06:00 PM
10:00 AM - 07:00 PM
11:00 AM - 08:00 PM
try this.
select * from timetable
order by
iif(timesched in ('No Schedule','Rest Day'),'01' + timesched,timesched)
Append a prefix for No Schedule and Restday with
01 when ordering.. so 01No will go 1, 01R will go next then your 09 to
24
How about simply doing:
SELECT DISTINCT TimeSched
FROM tbl_schedule
ORDER BY (CASE WHEN timesched LIKE '[a-zA-Z]%' THEN 1 ELSE 0 END),
timesched;
Try to make use of the below code :
DECLARE #TimeSched TABLE
(ID INT IDENTITY(1,1),TimeSched VARCHAR(20))
INSERT INTO #TimeSched
VALUES
('Rest Day'),
('11:00 AM - 08:00 PM'),
('No Schedule'),
('09:00 AM - 06:00 PM'),
('10:00 AM - 07:00 PM')
SELECT * FROM #TimeSched
ORDER BY
CASE
WHEN TimeSched ='Rest Day' THEN 1
WHEN TimeSched ='No Schedule' THEN 2
ELSE 3 END
Here is the answer to my question.
SELECT CASE TimeSched WHEN 'Restday' THEN 1 WHEN 'No Schedule' THEN 2 ELSE 3 END
AS TimeSked, TimeSched ORDER BY TimeSked, TimeSched