SQL query to include time segments with no counts - sql

I am working in SQL Server 2014. I have table that records 'counts' and a timestamp of the count. The counting period is a two hour block that can start at any quarter hour. In the example data below, the count starts at 16:00 and goes through 18:00. The counting block could have started at 01:30 and stopped at 03:30.
Timestamp Count
16:00:31 1
16:00:42 1
16:16:04 1
16:16:06 1
16:45:10 1
16:45:31 1
16:45:32 1
17:16:45 1
17:16:52 1
17:16:53 1
17:33:19 1
17:34:01 1
17:45:03 1
17:46:08 1
I have a query which sums the counts over 15 minute intervals within the two hour block:
SELECT
FORMAT(DATEPART(HOUR, [Timestamp]), '0#') + ':' + FORMAT(DATEPART(MINUTE, [TimeStamp]) / 15 * 15, '0#') AS QtrHrBeg
, COUNT(*) AS CountTotal
FROM
[Sandbox].[trippetoe].[SURVEYCOUNTS]
GROUP BY
DATEPART(HOUR, [TIMESTAMP])
, (DATEPART(MINUTE, [TIMESTAMP]) / 15 * 15)
which results in this:
QtrHrBeg Count
16:00 2
16:15 2
16:45 3
17:15 3
17:30 2
17:45 2
I'd like to include 15 minute intervals where there are no counts - in this example the quarter hours beginning at 16:30 and 17:00, like below:
QtrHrBeg Count
16:00 2
16:15 2
16:30 0
16:45 3
17:00 0
17:15 3
17:30 2
17:45 2
How can i do that?

See below.
Begin by creating a time table of all intervals for the day, then restricting that to the intervals for the 2 hour window you want.
Then left join that to the sum of your data table, pushing 0 where the join returns null.
DECLARE #Data TABLE ([TimeStamp] TIME, [Count] INT)
INSERT INTO #Data ([TimeStamp],[Count])
VALUES ('16:00:31',1),
('16:00:42',1),
('16:16:04',1),
('16:16:06',1),
('16:45:10',1),
('16:45:31',1),
('16:45:32',1),
('17:16:45',1),
('17:16:52',1),
('17:16:53',1),
('17:33:19',1),
('17:34:01',1),
('17:45:03',1),
('17:46:08',1)
;with AllIntervals AS
(
SELECT CONVERT(TIME,'00:00:00') AS Interval
UNION ALL
SELECT DATEADD(MINUTE,15,Interval)
FROM AllIntervals
WHERE Interval<'23:45:00'
), MyIntervals AS
(
SELECT CONVERT(VARCHAR(5),Interval,108) AS Interval
FROM AllIntervals
WHERE Interval >= (SELECT MIN(CONVERT(TIME,DATEADD(minute,(DATEDIFF(minute,0,[TimeStamp])/15)*15,0))) FROM #Data)
AND Interval < DATEADD(HOUR,2,(SELECT MIN(CONVERT(TIME,DATEADD(minute,(DATEDIFF(minute,0,[TimeStamp])/15)*15,0))) FROM #Data))
)
SELECT M.Interval, ISNULL(I.[Count],0)
FROM MyIntervals M
LEFT JOIN (SELECT CONVERT(TIME,DATEADD(minute,(DATEDIFF(minute,0,[TimeStamp])/15)*15,0)) AS Interval, SUM([Count]) AS Count
FROM #Data
GROUP BY CONVERT(TIME,DATEADD(minute,(DATEDIFF(minute,0,[TimeStamp])/15)*15,0))) I
ON M.Interval=I.Interval

You can use the following
Find the minimum date and the maximum date in the data you are going to work on , then round these two values to the nearest 15
Split the segment into 15 minutes intervals
Left join your data with the result came out and apply group by the StartTime and I used format in order to show the time formatting only
The benefit of this approach is that it works on specific interval and will not take any time interval outside of your data ranges.
with initial as(
select dateadd(minute, datediff(minute,0,min([Time])) / 15 * 15, 0) as MinTime,
dateadd(minute, datediff(minute,0,max([Time])) / 15 * 15, 0) as MaxTime
from data
), times as(
select StartTime = MinTime,
EndTime =dateadd(millisecond,-1,dateadd(minute,15,MinTime)),
MaxTime
from initial
union all
select dateadd(millisecond,1,EndTime),
dateadd(minute,15,EndTime),
MaxTime
from times
where EndTime<MaxTime
)
select format(t.StartTime,'HH:mm') as [Time],isnull(sum(d.[Count]),0) as [Count]
from times t
left join data d on d.[Time] between t.StartTime and t.EndTime
group by t.StartTime
Here is the output
Time Count
16:00 2
16:15 2
16:30 0
16:45 3
17:00 0
17:15 3
17:30 2
17:45 2
Here a working demo
Hope this will help you
EDIT
I changed the usage of second to millisecond based on the comment from #HABO, it will solve the case where there is some times like 16:59:59

Related

Split current date into hourly intervals and get count of production

How can I split the current date into hourly intervals like 00:00 - 01:00 for 24 hours and based on that I need to get the count of production which is another column.
This is the code for date column and count column which I wanted to group by hour interval.
select count(*),order_start_time_T
from UDA_Order UDA INNER JOIN WORK_Order WO ON WO.order_key = UDA.object_key
where order_state = 'BOOKED' OR order_state = 'CLOSED'
GROUP BY order_start_time_T
this returns me
Count order_start_time_T
2 2019-07-02 10:54:27.000
7 2019-07-02 10:55:27.000
1 2019-07-02 11:51:58.000
1 2019-07-02 11:58:41.000
1 2019-07-02 12:19:13.000
The result I expect is
Count Hour interval till 24 hours for current day
2 00:00 - 01:00
7 01:00 - 02:00
1 02:00 - 03:00
1 03:00 - 04:00
1 04:00 - 05:00
1 05:00 - 06:00
and so on till 24 hours for current day.
You need to use the DATEPART function that returns the part of the date that you need, which is hrs in your case.
select count(*), CAST(order_start_time_T AS DATE) StartDate, DATEPART(HOUR, order_start_time_T) StartHr
from UDA_Order UDA INNER JOIN WORK_Order WO ON WO.order_key = UDA.object_key
where order_state = 'BOOKED' OR order_state = 'CLOSED'
GROUP BY CAST(order_start_time_T AS DATE), DATEPART(HOUR, order_start_time_T)
But this will not return the results as you wish.
It will returns it like this (for example):
Count StartDate StartHr
2 2019-07-02 10
7 2019-07-02 11
1 2019-07-02 12
I would try with a helper table, which would hold a start hour (h1 column) and end hour (h2 column). I used a temporary table, but it can be a standard table or a table variable. Column display is just for display purposes.
First of all I populate the table with start and end hour, starting from 0.
Secondly, I use DATEPART to identify an hour of an order (order_start_time_T) and check, in which period that hour depends to.
h1 h2 display
--- --- ---
0 1 00:00 - 01:00
1 2 01:00 - 02:00
....
23 24 23:00 - 24:00
Query:
-- Populate time table
if object_id('tempdb..#t') is not null drop table #t
create table #t (
h1 tinyint,
h2 tinyint,
display varchar(30)
);
declare #i tinyint =0
while #i<24 begin
insert into #t (h1, h2, display) values(#i, #i+1
, case when #i<10 then '0' else '' end+cast(#i as varchar)
+':00 - ' + case when #i<9 then '0' else '' end+ cast(#i+1 as varchar)+':00')
set #i = #i + 1
end
-- Group per period
select count(*) [Count], t.display
from UDA_Order UDA INNER JOIN WORK_Order WO ON WO.order_key = UDA.object_key
JOIN #t t ON datepart(hour, order_start_time_T) between t.h1 and t.h2
where order_state = 'BOOKED' OR order_state = 'CLOSED'
GROUP BY t.display

Compare date in multiple rows and calculate the downtime

I'm trying to calculate the downtime for a train from a service record, below is a sample scenario
There can be multiple jobs running simultaneous for a train which can overlap at times
For:
Job_number 1 the date diff between the work start and end date is 360 Minute
Job_number 2 the date diff between the work start and end date is 60 Minute but this overlap with Job_number 1 so we shouldn't consider this
Job_number 3 the date diff between the work start and end date is 45 Minute but this partially overlap with Job_number 1 so we should consider only 10 Minute
So the actual down time should be 360 Minute (Job 1) + 0 Minute (Job 2) + 10 Minute (Job 3) = 370 Minute
My desired output is :-
I'm having 20 trains as of now for which I need to calculate the downtime as above
How do I do this?
Sample Data script:
CREATE TABLE [dbo].[tb_ServiceMemo](
[Job_Number] [nvarchar](500) NULL,
[Train_Number] [nvarchar](500) NULL,
[Work_Start_Date] [datetime] NULL,
[Work_Completed_Date] [datetime] NULL
) ON [PRIMARY]
INSERT INTO [dbo].[tb_ServiceMemo]
VALUES (1,1,'01-08-2018 12:35','01-08-18 18:35'),
(2,1,'01-08-2018 14:20','01-08-18 15:20'),
(3,1,'01-08-2018 18:00','01-08-18 18:45')
This is a gaps-and-islands problem, but it is tricky because it has start and end times.
The idea for the solution is to determine when an outage starts. What is the characteristic? Well, the period starts at a time where there is no overlap with preceding work. The tricky part is that more than one "work" effort could start at the same time (although your data does not show this).
Once you know the time when an outage starts, you can use a cumulative sum to assign a group to each record and then simply aggregate by that group (and other information).
The following query should do what you want:
with starts as (
select sm.*,
(case when exists (select 1
from tb_ServiceMemo sm2
where sm2.Train_Number = sm.Train_Number and
sm2.Work_Start_Date < sm.Work_Start_Date and
sm2.Work_Completed_Date >= sm.Work_Start_Date
)
then 0 else 1
end) as isstart
from tb_ServiceMemo sm
)
select Train_Number, min(Work_Start_Date) as outage_start_date, max(Work_Completed_Date) as outage_end_date,
datediff(minute, min(Work_Start_Date), max(Work_Completed_Date))
from (select s.*, sum(isstart) over (partition by Train_Number order by Work_Start_Date) as grp
from starts s
) s
group by Train_Number, grp;
In this db<>fiddle, I added a few more rows to show how the code works in different scenarios.
This is a Gaps and Islands in Sequences problem.
You can try to use recursive CTE, get the minute during every row.
then use every MAX and MIN DateTime to calculate the result.
;WITH CTE AS (
SELECT [Train_Number], [Work_Start_Date] ,[Work_Completed_Date]
FROM [tb_ServiceMemo]
UNION ALL
SELECT [Train_Number], DATEADD(minute,1,[Work_Start_Date]) ,[Work_Completed_Date]
FROM CTE
WHERE DATEADD(minute,1,[Work_Start_Date]) <= [Work_Completed_Date]
),CTE2 AS (
SELECT DISTINCT Train_Number,
Work_Start_Date,
MAX(Work_Completed_Date) OVER(PARTITION BY Train_Number ORDER BY Work_Completed_Date DESC) MAX_Time
FROM CTE
),CTE_RESULT AS (
SELECT *,datediff(mi,MAX_Time,Work_Start_Date) - row_number() over(PARTITION BY Train_Number ORDER BY Work_Start_Date) grp
FROM CTE2
)
SELECT Train_Number,sum(time_diff)
FROM (
SELECT Train_Number,DATEDIFF(MI,MIN(Work_Start_Date),MAX(Work_Start_Date)) time_diff
FROM CTE_RESULT
GROUP BY Train_Number,grp
)t1
GROUP BY Train_Number
option ( MaxRecursion 0 );
sqlfiddle
This is the infamous gaps and islands problem with dates. The following is a solution that uses a recursive CTE. It might be a little tough to understand if you aren't used to working with them, I commented all parts that might need clarifying.
I also added a few more examples to contemplate different scenarios, such as different days on periods and overlapping times exactly at the start/end.
Example setup:
IF OBJECT_ID('tempdb..#tb_ServiceMemo') IS NOT NULL
DROP TABLE #tb_ServiceMemo
CREATE TABLE #tb_ServiceMemo(
Job_Number INT, -- This is an INT not VARCHAR!! (even the name says so)
Train_Number INT, -- This one also!!
Work_Start_Date DATETIME,
Work_Completed_Date DATETIME)
INSERT INTO #tb_ServiceMemo (
Job_Number,
Train_Number,
Work_Start_Date,
Work_Completed_Date)
VALUES
-- Total time train 1: 6h 10m (370m)
(1,1,'2018-08-01 12:35','2018-08-01 18:35'), -- Make sure to write date literals in ISO format (yyyy-MM-dd) to avoid multiple interpretations
(2,1,'2018-08-01 14:20','2018-08-01 15:20'),
(3,1,'2018-08-01 18:00','2018-08-01 18:45'),
-- Total time train 2: 2h (120m)
(4,2,'2018-08-01 12:00','2018-08-01 12:10'),
(5,2,'2018-08-01 12:15','2018-08-01 12:20'),
(6,2,'2018-08-01 13:15','2018-08-01 13:45'),
(9,2,'2018-08-01 13:45','2018-08-01 15:00'),
-- Total time train 3: 3h 45m (225m)
(7,3,'2018-08-01 23:30','2018-08-02 00:30'),
(8,3,'2018-08-02 00:15','2018-08-02 03:15'),
-- Total time train 4: 2d 8h 15m (3375m)
(10,4,'2018-08-01 23:00','2018-08-03 23:00'),
(11,4,'2018-08-02 00:15','2018-08-04 07:15')
The solution:
;WITH TimeLapses AS
(
-- Recursive Anchor: Find the minimum Jobs for each train that doesn't overlap with previous Jobs
SELECT
InitialJobNumber = T.Job_Number,
JobNumber = T.Job_Number,
TrainNumber = T.Train_Number,
IntervalStart = T.Work_Start_Date,
IntervalEnd = T.Work_Completed_Date,
JobExtensionPath = CONVERT(VARCHAR(MAX), T.Job_Number), -- Will store the chained jobs together for clarity
RecursionLevel = 1
FROM
#tb_ServiceMemo AS T
WHERE
NOT EXISTS (
SELECT
'Job doesn''t overlap with previous Jobs (by train)'
FROM
#tb_ServiceMemo AS S
WHERE
S.Train_Number = T.Train_Number AND
S.Job_Number < T.Job_Number AND
S.Work_Completed_Date >= T.Work_Start_Date AND -- Conditions for the periods to overlap
S.Work_Start_Date <= T.Work_Completed_Date)
UNION ALL
-- Recursive Union: Chain overlapping Jobs by train and keep intervals boundaries (min & max)
SELECT
InitialJobNumber = L.InitialJobNumber,
JobNumber = T.Job_Number,
TrainNumber = L.TrainNumber,
IntervalStart = CASE -- Minimum of both starts
WHEN L.IntervalStart <= T.Work_Start_Date THEN L.IntervalStart
ELSE T.Work_Start_Date END,
IntervalEnd = CASE -- Maximum of both ends
WHEN L.IntervalEnd >= T.Work_Completed_Date THEN L.IntervalEnd
ELSE T.Work_Completed_Date END,
JobExtensionPath = L.JobExtensionPath + '->' + CONVERT(VARCHAR(MAX), T.Job_Number),
RecursionLevel = L.RecursionLevel + 1
FROM
TimeLapses AS L -- Recursive CTE!
INNER JOIN #tb_ServiceMemo AS T ON
L.TrainNumber = T.Train_Number AND
T.Work_Completed_Date >= L.IntervalStart AND -- Conditions for the periods to overlap
T.Work_Start_Date <= L.IntervalEnd
WHERE
L.JobNumber < T.Job_Number -- Prevent joining in both directions (that would be "<>") to avoid infinite loops
),
MaxRecursionLevelByTrain AS
(
/*
Max recursion level will hold the longest interval for each train, as there might be recursive paths that skips some jobs. For example: Train 1's job 1 will
join with Job 2 and Job 3 on the first recursive level, then Job 2 will join with Job 3 on the next recursion. The higher the recursion level the more Jobs we
are taking into account for the longest interval.
We also need to group by InitialJobNumber as there might be different, idependent gaps for each train.
*/
SELECT
TrainNumber = T.TrainNumber,
InitialJobNumber = T.InitialJobNumber,
MaxRecursionLevel = MAX(T.RecursionLevel)
FROM
TimeLapses AS T
GROUP BY
T.TrainNumber,
T.InitialJobNumber
),
ExpandedLapses AS
(
SELECT
TrainNumber = T.TrainNumber,
InitialJobNumber = M.InitialJobNumber,
IntervalStart = T.IntervalStart,
IntervalEnd = T.IntervalEnd,
DownTime = DATEDIFF(MINUTE, T.IntervalStart, T.IntervalEnd),
JobExtensionPath = T.JobExtensionPath,
RecursionLevel = T.RecursionLevel
FROM
MaxRecursionLevelByTrain AS M
INNER JOIN TimeLapses AS T ON
M.TrainNumber = T.TrainNumber AND
M.MaxRecursionLevel = T.RecursionLevel AND
M.InitialJobNumber = T.InitialJobNumber
)
SELECT
TrainNumber = E.TrainNumber,
TotalDownTime = SUM(DownTime)
FROM
ExpandedLapses AS E
GROUP BY
E.TrainNumber
And these are the partial results from each CTE, so you can see each step:
TimeLapses:
InitialJobNumber JobNumber TrainNumber IntervalStart IntervalEnd JobExtensionPath RecursionLevel
1 1 1 2018-08-01 12:35:00.000 2018-08-01 18:35:00.000 1 1
1 2 1 2018-08-01 12:35:00.000 2018-08-01 18:35:00.000 1->2 2
1 3 1 2018-08-01 12:35:00.000 2018-08-01 18:45:00.000 1->3 2
1 3 1 2018-08-01 12:35:00.000 2018-08-01 18:45:00.000 1->2->3 3
4 4 2 2018-08-01 12:00:00.000 2018-08-01 12:10:00.000 4 1
5 5 2 2018-08-01 12:15:00.000 2018-08-01 12:20:00.000 5 1
6 6 2 2018-08-01 13:15:00.000 2018-08-01 13:45:00.000 6 1
6 9 2 2018-08-01 13:15:00.000 2018-08-01 15:00:00.000 6->9 2
7 8 3 2018-08-01 23:30:00.000 2018-08-02 03:15:00.000 7->8 2
7 7 3 2018-08-01 23:30:00.000 2018-08-02 00:30:00.000 7 1
10 10 4 2018-08-01 23:00:00.000 2018-08-03 23:00:00.000 10 1
10 11 4 2018-08-01 23:00:00.000 2018-08-04 07:15:00.000 10->11 2
MaxRecursionLevelByTrain:
TrainNumber InitialJobNumber MaxRecursionLevel
1 1 3
2 4 1
2 5 1
2 6 2
3 7 2
4 10 2
ExtendedLapses:
TrainNumber InitialJobNumber IntervalStart IntervalEnd DownTime JobExtensionPath RecursionLevel
1 1 2018-08-01 12:35:00.000 2018-08-01 18:45:00.000 370 1->2->3 3
2 4 2018-08-01 12:00:00.000 2018-08-01 12:10:00.000 10 4 1
2 5 2018-08-01 12:15:00.000 2018-08-01 12:20:00.000 5 5 1
2 6 2018-08-01 13:15:00.000 2018-08-01 15:00:00.000 105 6->9 2
3 7 2018-08-01 23:30:00.000 2018-08-02 03:15:00.000 225 7->8 2
4 10 2018-08-01 23:00:00.000 2018-08-04 07:15:00.000 3375 10->11 2
Final Result:
TrainNumber TotalDownTime
1 370
2 120
3 225
4 3375
A few things worth mentioning:
While this solution will definitely be faster than using a cursor, it might not be the best one available, specially if you have a huge dataset (more than 100k records). There is room for improving performance.
You might benefit from a index on #tb_ServiceMemo (Train_Number, Job_Number, Work_Start_Date) to speed up the query.
You might need to add OPTION (MAXRECURSION N) at the end of the SELECT statement, being N the max recursion level you want to try. Default is 100, so if there are more than 100 periods that chain together for a particular train, an error message will pop up. You can use 0 as N for unlimited.
Make sure that every end time is higher than the start time, and that the job numbers don't repeat, at least by each train.
Can you try this one ? I added other test case to besure but I think it's OK. I also think there is more simple
INSERT INTO [dbo].[tb_ServiceMemo]
SELECT 1, 1, CONVERT(DATETIME, '2018-08-01 09:35:00', 120), CONVERT(DATETIME, '2018-08-01 12:45:00', 120) union
SELECT 2, 1, CONVERT(DATETIME, '2018-08-01 12:35:00', 120), CONVERT(DATETIME, '2018-08-01 18:35:00', 120) union
SELECT 3, 1, CONVERT(DATETIME, '2018-08-01 14:20:00', 120), CONVERT(DATETIME, '2018-08-01 15:20:00', 120) union
SELECT 4, 1, CONVERT(DATETIME, '2018-08-01 18:00:00', 120), CONVERT(DATETIME, '2018-08-01 18:45:00', 120) union
SELECT 5, 1, CONVERT(DATETIME, '2018-08-01 19:00:00', 120), CONVERT(DATETIME, '2018-08-01 19:45:00', 120)
SELECT [Train_Number], SUM(DATEDIFF(MINUTE, T.[Work_Start_Date], T.Work_Completed_Date)) as Delay
FROM (
SELECT
[Job_Number],
[Train_Number],
CASE
WHEN EXISTS(SELECT * FROM [tb_ServiceMemo] T3 WHERE T1.[Work_Start_Date] BETWEEN T3.[Work_Start_Date] AND T3.[Work_Completed_Date] AND T1.[Job_Number] <> T3.[Job_Number] AND T1.Train_Number = T3.Train_Number)
THEN (SELECT MAX(T3.[Work_Completed_Date]) FROM [tb_ServiceMemo] T3 WHERE T1.[Work_Start_Date] BETWEEN T3.[Work_Start_Date] AND T3.[Work_Completed_Date] AND T1.[Job_Number] <> T3.[Job_Number] AND T1.Train_Number = T3.Train_Number)
ELSE [Work_Start_Date] END as [Work_Start_Date],
[Work_Completed_Date]
FROM [tb_ServiceMemo] T1
WHERE NOT EXISTS( -- To kick off the ignored case
SELECT T2.*
FROM [tb_ServiceMemo] T2
WHERE T2.[Work_Start_Date] < T1.[Work_Start_Date] AND T2.[Work_Completed_Date] > T1.[Work_Completed_Date]
)
) as T
GROUP BY [Train_Number]
The idea is to :
ignore the result contained into another
rewrite the start date value of each rown if she is contained into another

SQL how to split one row to multiple between two time?

I have a row like:
EMPID INTIME OUTTIME JOBCODE
1 4:00 5:00 ABC
2 5:00 8:00 ABC
Expected Output:
EMPID TIMEID JOBCODE MINUTE
1 16 ABC 15
1 17 ABC 15
1 18 ABC 15
1 19 ABC 15
TIMEID FOR 4:00 is 16 and increase with 15 minutes threshold.
I have tried with below query but it generates a single row.
SELECT
TO_NUMBER(SUBSTR(TO_CHAR(INTIME,'HH24:MI:SS'),0,2)* 4) + ROUND(TO_NUMBER((SUBSTR(TO_CHAR(INTIME,'HH24:MI:SS'),4,2)))/15,0) AS TIMEID,
EMPID,
JOBCODE,
MINUTE FROM MYTABLE;
Split 1 row like that as below:
with
x as
(select 1 Empid, to_date('01-JAN-1900 '||'4:00','DD-MON-YYYY HH24:MI') intime, to_date('01-JAN-1900 '||'5:12','DD-MON-YYYY HH24:MI') outtime from dual)
Select Empid,intime,(newtime-nvl(lag(newtime) over (order by newtime),intime))*1440 Required_Intervals,outtime,newtime
from (
select EmpId,InTime,Case When Outtime > INTIME+(15*rownum/1440) Then INTIME+(15*rownum/1440) Else Outtime End newTime,OUTTIME
from x
connect by rownum <= CEIL(((outtime-intime)*1440)/15))
1440 = 24 (hours per day) * 60 (minutes per hour) = total minutes per day
Please note it will work for only 1 row. For multiple rows as well you can create a query using multiset & connect by level if required.
The query also accounts for when the out-time is not in intervals of 15.

SQL Server : Gap / Island, datetime, contiguous block 365 day block

I have a table that looks like this:-
tblMeterReadings
id meter period_start period_end amount
1 1 2014-01-01 00:00 2014-01-01 00:29:59 100.3
2 1 2014-01-01 00:30 2014-01-01 00:59:59 50.5
3 1 2014-01-01 01:00 2014-01-01 01:29:59 70.7
4 1 2014-01-01 01:30 2014-01-01 01:59:59 900.1
5 1 2014-01-01 02:00 2014-01-01 02:29:59 400.0
6 1 2014-01-01 02:30 2014-01-01 02:59:59 200.3
7 1 2014-01-01 03:00 2014-01-01 03:29:59 100.8
8 1 2014-01-01 03:30 2014-01-01 03:59:59 140.3
This is a tiny "contiguous block" from '2014-01-01 00:00' to '2014-01-01 3:59:59'.
In the real table there are "contiguous blocks" of years in length.
I need to find the the period_start and period_end of the most recent CONTINUOUS 365 COMPLETE DAYs (fileterd by meter column).
When I say COMPLETE DAYs I mean a day that has entries spanning 00:00 to 23:59.
When I say CONTINUOUS I mean there must be no days missing.
I would like to select all the rows that make up this block of CONTINUOUS COMPLETE DAYs.
I also need an output like:
block_start block_end total_amount_for_block
2013-02-26 00:00 2014-02-26 23:59:59 1034234.5
This is beyond me, so if someone can solve... I will be very impressed.
Since your granularity is 1 second, you need to expand your periods into all the date/times between the start and end at 1 second intervals. To do this you need to cross join with a numbers table (The numbers table is generated on the fly by ranking object ids from an arbitrary system view, I have limited it to TOP 86400 since this is the number of seconds in a day, and you have stated your time periods never span more than one day):
WITH Numbers AS
( SELECT TOP (86400)
Number = ROW_NUMBER() OVER(ORDER BY a.object_id) - 1
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
ORDER BY a.object_id
)
SELECT r.ID, r.meter, dt.[DateTime]
FROM tblMeterReadings r
CROSS JOIN Numbers n
OUTER APPLY
( SELECT [DateTime] = DATEADD(SECOND, n.Number, r.period_start)
) dt
WHERE dt.[DateTime] <= r.Period_End;
You then have your continuous range in which to perform the normal gaps and islands grouping:
WITH Numbers AS
( SELECT TOP (86400)
Number = ROW_NUMBER() OVER(ORDER BY a.object_id) - 1
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
ORDER BY a.object_id
), Grouped AS
( SELECT r.meter,
Amount = CASE WHEN Number = 1 THEN r.Amount ELSE 0 END,
dt.[DateTime],
GroupingSet = DATEADD(SECOND,
-DENSE_RANK() OVER(PARTITION BY r.Meter
ORDER BY dt.[DateTime]),
dt.[DateTime])
FROM tblMeterReadings r
CROSS JOIN Numbers n
OUTER APPLY
( SELECT [DateTime] = DATEADD(SECOND, n.Number, r.period_start)
) dt
WHERE dt.[DateTime] <= r.Period_End
)
SELECT meter,
PeriodStart = MIN([DateTime]),
PeriodEnd = MAX([DateTime]),
Amount = SUM(Amount)
FROM Grouped
GROUP BY meter, GroupingSet
HAVING DATEADD(YEAR, 1, MIN([DateTime])) < MAX([DateTime]);
N.B. Since the join to Number causes amounts to be duplicated, it is necessary to set all duplicates to 0 using CASE WHEN Number = 1 THEN r.Amount ELSE 0 END, i.e only include the amount for the first row for each ID
Removing the Having clause for your sample data will give:
meter | PeriodStart | PeriodEnd | Amount
------+---------------------+---------------------+----------
1 | 2014-01-01 00:00:00 | 2014-01-01 03:59:59 | 1963
Example on SQL Fiddle
You could try this:
Select MIN(period_start) as "block start"
, MAX(period_end) as "block end"
, SUM(amount) as "total amount"
FROM YourTable
GROUP BY datepart(year, period_start)
, datepart(month, period_start)
, datepart(day, period_start)
, datepart(year, period_end)
, datepart(month, period_end)
, datepart(day, period_end)
Having datepart(year, period_start) = datepart(year, period_end)
AND datepart(month, period_start) = datepart(month, period_end)
AND datepart(day, period_start) = datepart(day, period_end)
AND datepart(hour, MIN(period_start)) = 0
AND datepart(minute,MIN(period_start)) = 0
AND datepart(hour, MAX(period_end)) = 23
AND datepart(minute,MIN(period_end)) = 59

SQL Server : Create Timetable

I have a table that records the start and end of a day schedule:
ScheduleID VenueID Date StartTime EndTime
1 1 14/07/2013 10:30 16:30
2 1 15/07/2013 09:30 16:30
3 2 16/07/2013 09:00 16:00
I need to create a table that self populates 30 minute time slots based in the start and end time so it looks something like this:
TimetableID VenueID Date Time_Slots Client1ID Client2ID
1 1 14/07/2013 10:30 45 Null
2 1 14/07/2013 11:00 67 78
3 1 14/07/2013 11:30 104 Null
4 1 14/07/2013 12:00 112 56
etc
Any help on how I generate the second table so that I can simple add the Client1ID and Clinet2ID would be greatly appreciated.
Thanks
Not sure what you mean be 'self populating table' and you don't say where the clientid's come from (so i'll leave that to you). But you can get a result set of 30 minute time slots with something like this
WITH cte AS
(
SELECT id, venue, schedule_date,
start_time time_slots, end_time
FROM schedule
UNION ALL
SELECT schedule.id, schedule.venue, schedule.schedule_date,
DATEADD(MINUTE, 30, cte.time_slots) time_slots,
schedule.end_time
FROM schedule
INNER JOIN cte
ON cte.id = schedule.id
AND cte.time_slots < schedule.end_time
)
SELECT ROW_NUMBER() OVER(ORDER BY id, time_slots),
venue,
schedule_date,
time_slots
FROM cte
ORDER BY id, time_slots
demo