T-SQL - Minutes per hour between two datetimes - sql-server

I have to following data:
| tid | startdate | enddate |
| 1 | 2016-12-26 12:30 | 2016-12-26 15:30 |
| 2 | 2016-12-26 13:15 | 2016-12-26 15:15 |
I would like to create a result with the hour number and then the amount of minutes the date time falls within that hour.
Example result:
| tid | hour | minutes_in |
| 1 | 12 | 30 |
| 1 | 13 | 60 |
| 1 | 14 | 60 |
| 1 | 15 | 30 |
| 2 | 13 | 45 |
| 2 | 14 | 60 |
| 2 | 15 | 15 |
Any suggestions?

First You need a numbers table to get your hours from 0 - 23, which can be fairly easily created on the fly with a table value constructor:
SELECT N
FROM (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)
) n (N);
Then you can join this to your original data to split rows out into the number required. Then you just need a case expression to apply the correct logic for calculating the minutes:
WITH Numbers (Number) AS
( SELECT N
FROM (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)
) n (N)
), SampleData (tid, StartDate, EndDate) AS
( SELECT tid, CONVERT(DATETIME2, StartDate), CONVERT(DATETIME2, EndDate)
FROM (VALUES
(1, '2016-12-26 12:30', '2016-12-26 15:30'),
(2, '2016-12-26 13:15', '2016-12-26 15:15')
) d (tid, StartDate, EndDate)
)
SELECT d.tid,
[Hour] = n.Number,
Minutes_in = CASE
-- SPECIAL CASE: START HOUR = END HOUR
WHEN DATEPART(HOUR, d.StartDate) = DATEPART(HOUR, d.EndDate)
THEN DATEDIFF(MINUTE, d.StartDate, d.EndDate)
-- FULL HOURS IN BETWEEN START AND END
WHEN n.Number > DATEPART(HOUR, d.StartDate)
AND n.Number < DATEPART(HOUR, d.EndDate) THEN 60
-- START HOUR
WHEN n.Number = DATEPART(HOUR, d.StartDate)
THEN 60 - DATEPART(MINUTE, d.StartDate)
-- END HOUR
WHEN n.Number = DATEPART(HOUR, d.EndDate)
THEN DATEPART(MINUTE, d.EndDate)
END
FROM SampleData d
INNER JOIN Numbers n
ON n.Number >= DATEPART(HOUR, d.StartDate)
AND n.Number <= DATEPART(HOUR, d.EndDate);
ADDENDUM
If you need to span days, then you could alter the logic slightly, generate a larger set of numbers to cover more hours difference, then rather than joining on the hour of the day, join the numbers on the hours difference from the start datetime to the end datetime:
SELECT *
FROM SampleData d
INNER JOIN Numbers n
ON n.Number <= DATEDIFF(HOUR, d.StartDate, d.EndDate)
This means where the range crosses over days, then there is no issue, the hours just keep incrementing. e.g.
WITH Numbers (Number) AS
( SELECT ROW_NUMBER() OVER(ORDER BY N1.N) - 1
FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N1(N)
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N2 (N)
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N3 (N)
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N4 (N)
), SampleData (tid, StartDate, EndDate) AS
( SELECT tid, CONVERT(DATETIME2, StartDate), CONVERT(DATETIME2, EndDate)
FROM (VALUES
(1, '2016-12-26 12:30', '2016-12-26 15:30'),
(2, '2016-12-26 13:15', '2016-12-26 15:15'),
(3, '2016-12-26 13:15', '2016-12-27 15:15')
) d (tid, StartDate, EndDate)
)
SELECT d.tid,
[Date] = CONVERT(DATE, d.StartDate),
[Hour] = CONVERT(TIME(0), DATEADD(HOUR, DATEPART(HOUR, d.StartDate) + n.Number, 0)),
Minutes_in = CASE
-- SPECIAL CASE: START HOUR = END HOUR
WHEN DATEPART(HOUR, d.StartDate) = DATEPART(HOUR, d.EndDate)
AND DATEDIFF(DAY, d.StartDate, d.EndDate) = 0
THEN DATEDIFF(MINUTE, d.StartDate, d.EndDate)
-- START HOUR
WHEN n.Number = 0
THEN 60 - DATEPART(MINUTE, d.StartDate)
-- END HOUR
WHEN n.Number = DATEDIFF(HOUR, d.StartDate, d.EndDate)
THEN DATEPART(MINUTE, d.EndDate)
-- FULL HOURS IN BETWEEN START AND END
ELSE 60
END
FROM SampleData d
INNER JOIN Numbers n
ON n.Number <= DATEDIFF(HOUR, d.StartDate, d.EndDate)
ORDER BY d.tid, n.Number;

Method -I
You can achieve this with a UDF (Another Simplest Way)
Lets build schema of your provided data
CREATE TABLE #TAB ( TID INT, STARTDATE DATETIME, ENDDATE DATETIME)
INSERT INTO #TAB
SELECT 1,'2016-12-26 12:30','2016-12-26 15:30'
UNION ALL
SELECT 2,'2016-12-26 13:15','2016-12-26 15:15'
Create one UDF to generate values between from and To
ALTER FUNCTION [dbo].[FN_GENERATE] (#FROM_NBR INT, #TO_NBR INT)
RETURNS
#RESULT TABLE(HR INT)
AS
BEGIN
;WITH CTE AS
(
SELECT #FROM_NBR AS FROM_NBR,#TO_NBR AS TO_NBR
UNION ALL
SELECT FROM_NBR+1 ,TO_NBR FROM CTE WHERE FROM_NBR<TO_NBR
)
INSERT INTO #RESULT
SELECT FROM_NBR FROM CTE
RETURN
END
Now query for data by calling the function.
;WITH CTE AS (
SELECT TID,STARTDATE,ENDDATE,DATEPART(HH,STARTDATE) FROM_HR , DATEPART(HH,ENDDATE) TO_HR FROM #TAB T
)
SELECT C1.TID,F.HR, COALESCE(DATEPART(MINUTE,FRM_HR_MINUTS.STARTDATE),DATEPART(MINUTE,TO_HR_MINUTS.ENDDATE),60 )
FROM CTE C1
CROSS APPLY
(
SELECT * FROM DBO.[FN_GENERATE] (C1.FROM_HR, C1.TO_HR)
)AS F
LEFT JOIN CTE FRM_HR_MINUTS ON C1.TID= FRM_HR_MINUTS.TID AND DATEPART(HH,FRM_HR_MINUTS.STARTDATE)= F.HR
LEFT JOIN CTE TO_HR_MINUTS ON C1.TID= TO_HR_MINUTS.TID AND DATEPART(HH,TO_HR_MINUTS.ENDDATE)= F.HR
Edit :
Method - II
Without using UDF & using MASTER.DBO.SPT_VALUES
;WITH CTE AS (
--PREPARING START HR, END HR, START_MIN, END_MIN FROM #TAB
SELECT TID,STARTDATE,ENDDATE
,DATEPART(HH,STARTDATE) FROM_HR
, DATEPART(HH,ENDDATE) TO_HR
, DATEPART(MINUTE, STARTDATE) AS STARTMIN
, DATEPART(MINUTE, ENDDATE) ENDMIN
FROM #TAB T
)
SELECT TID
, NUMBER AS HRS
--if Outer APply produce Null Display Minutes from CTE else 60 Mins
, CASE ISNULL(OA.FRM_MINS, C1.STARTMIN) + ISNULL(TO_MINS,C1.ENDMIN)
WHEN 0
THEN 60
ELSE ISNULL(OA.FRM_MINS, C1.STARTMIN) + ISNULL(TO_MINS,C1.ENDMIN)
END AS MINS
FROM CTE C1
OUTER APPLY --JOINING NUMBERS BETWEEN FROM_HR & TO_HR using MASTER.DBO.SPT_VALUES
(
SELECT NUMBER
--IF FROM_HR matched NULL Else 0
, CASE C1.FROM_HR WHEN NUMBER THEN NULL ELSE 0 END AS FRM_MINS
--IF TO_HR matched NULL Else 0
,CASE C1.TO_HR WHEN NUMBER THEN NULL ELSE 0 END AS TO_MINS
FROM MASTER.DBO.SPT_VALUES
WHERE [type]='P' AND number>0 AND number BETWEEN FROM_HR AND TO_HR
)AS OA

Related

SQL Server - Display the number of active sessions for each minute in a time frame

I have the below table:
SessionID | UserName | Started | Ended
----------------------------------------------------------------
100 Test1 2015-07-26 00:03:05 2015-07-26 00:08:12
As the title says, I need to extract between a given #FromDate and a #ToDate parameters, for each minute, how many active sessions were. What I have tried so far does not select the non-active session (when no customers were online in that minute) and I cannot figure it out how to do this.
My SQL Statement
CREATE PROCEDURE [dbo].[ActiveSessionsByMinute] #FromDate datetime, #ToDate datetime
AS
BEGIN
SET NOCOUNT ON
SELECT DATEADD(MINUTE, DATEPART(MINUTE, Started), CAST(CONVERT(varchar(20), Started, 112) AS datetime)) AS DateMinute,
COUNT(SessionID) AS ActiveSessions
FROM ApplicationSessionHistory
GROUP BY DATEADD(MINUTE, DATEPART(MINUTE, Started), CAST(CONVERT(varchar(20), Started, 112) AS datetime))
END
GO
Output
DateMinute | ActiveSessions
-----------------------------------------
2015-07-26 00:03:00.000 | 1
If I execute the below statement, I should get the desired output (below):
EXEC dbo.ActiveSessionsByMinute
#FromDate = '2015-07-26 00:00',
#ToDate = '2015-07-26 00:10'
Desired Output
DateMinute | ActiveSessions
-----------------------------------------
2015-07-26 00:00:00.000 | 0
2015-07-26 00:01:00.000 | 0
2015-07-26 00:02:00.000 | 0
2015-07-26 00:03:00.000 | 1
2015-07-26 00:04:00.000 | 1
2015-07-26 00:05:00.000 | 1
2015-07-26 00:06:00.000 | 1
2015-07-26 00:07:00.000 | 1
2015-07-26 00:08:00.000 | 1
2015-07-26 00:09:00.000 | 0
2015-07-26 00:00:00.000 | 0
Does anyone can give me a tip? Thanks
I would do this with a CTE tally table. Notice I added an extra Session in the sample data.
HERE IS A DEMO
--Sample data
declare #table table (SessionID int, UserName varchar(16), Started datetime, Ended datetime)
insert into #table
values
(100,'Test1','2015-07-26 00:03:05','2015-07-26 00:08:12')
,(101,'Test1','2015-07-26 00:04:05','2015-07-26 00:05:12')
--used as a beginning anchor for the tally table
declare #startDate datetime = (select min(cast(Started as date)) from #table)
--take the original data, and truncate the seconds
;with NewTable as(
select
SessionID
,UserName
,Started = CAST(DateAdd(minute, DateDiff(minute, 0, Started), 0) AS smalldatetime)
,Ended = CAST(DateAdd(minute, DateDiff(minute, 0, Ended), 0) AS smalldatetime)
from #table
),
--tally table to get 10K minutes.
--This can be expanded for larger date ranges, and is faster than recursive CTE
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT TallyDate = dateadd(minute,ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),#startDate) FROM E4
)
--use cross apply and and a case statement to find if it falls in the range
select
DateMinute = N
,SessionID
,Started
,Ended
,IsActive = case when (Started <=N and Ended >= N) then 1 else 0 end
from NewTable t
cross apply cteTally
where N <= (select max(Ended) from #table)
order by SessionID, N
For the sum part, you can simply aggregate. Replace the last SELECT with this one
--based on the above output, just do the SUM
select
DateMinute = N
,ActiveSessions = sum(case when (Started <=N and Ended >= N) then 1 else 0 end)
from NewTable t
cross apply cteTally
where N <= (select max(dateadd(minute,1,Ended)) from #table)
group by N
order by N
You'll want to SELECT from a tally table with all the minutes and LEFT JOIN to your ApplicationSessionHistory table:
CREATE PROCEDURE [dbo].[ActiveSessionsByMinute]
#FromDate DATETIME
, #ToDate DATETIME
AS
BEGIN
SET NOCOUNT ON;
SELECT allminutes.alltimes AS DateMinute
, COUNT(SessionID) AS ActiveSessions
FROM
(
SELECT DATEADD(MINUTE, myrows.rn, #FromDate) AS alltimes
FROM
(
SELECT ROW_NUMBER() OVER (ORDER BY s.id) - 1 rn
FROM master.sys.syscolumns AS s
) myrows
) allminutes
LEFT OUTER JOIN ApplicationSessionHistory ON allminutes.alltimes BETWEEN ApplicationSessionHistory.Started AND ApplicationSessionHistory.Ended
WHERE allminutes.alltimes <= #ToDate
GROUP BY DATEADD(MINUTE, DATEPART(MINUTE, Started), CAST(CONVERT(VARCHAR(20), Started, 112) AS DATETIME));
END;

Group and count by 16 hours time interval not working

I am trying to get the number of records for a 16 hour time interval. Below is the code that I am using now.
;With Cte_hours as ( --hours generation
Select top(6) hr = (Row_number() over (order by (Select NULL))-1)*4 from master..spt_values
), cte2 as ( --getting range
Select DateAdd(HH, c.hr, Convert(datetime,d.dts) ) as Dts_Start, DateAdd(MS, -2, DateAdd(HH, c.hr+ 4, Convert(datetime,d.dts) ) ) Dts_end
from (select distinct convert(date, dt) as dts from TEST2 ) d
cross apply Cte_hours c
) --actual query
Select c2.Dts_Start as DT, Sum(case when t.Dt is not null then 1 else 0 end) No_of_records,LD_VOY_N,LD_VSL_M
from cte2 c2
Left Join TEST2 t
on t.Dt between c2.Dts_Start and c2.Dts_end
group by c2.Dts_Start,LD_VOY_N,LD_VSL_M
order by LD_VOY_N, LD_VSL_M, Dts_Start ASC
This code is able to count the number of records I have based on a 4,6, and 12 hour interval. However, if I try to count based on a 16 hour interval, it somehow does not work. Below is my code and output that I used for the 16 hour interval.
;With Cte_hours as ( --hours generation
Select top(6) hr = (Row_number() over (order by (Select NULL))-1)*16 from master..spt_values
), cte2 as ( --getting range
Select DateAdd(HH, c.hr, Convert(datetime,d.dts) ) as Dts_Start, DateAdd(MS, -2, DateAdd(HH, c.hr+ 16, Convert(datetime,d.dts) ) ) Dts_end
from (select distinct convert(date, dt) as dts from TEST2 ) d
cross apply Cte_hours c
) --actual query
Select c2.Dts_Start as DT, Sum(case when t.Dt is not null then 1 else 0 end) No_of_records,LD_VOY_N,LD_VSL_M
from cte2 c2
Left Join TEST2 t
on t.Dt between c2.Dts_Start and c2.Dts_end
group by c2.Dts_Start,LD_VOY_N,LD_VSL_M
order by LD_VOY_N, LD_VSL_M, Dts_Start ASC
Result:
DT No_of_records LD_VOY_N LD_VSL_M
2017-05-05 16:00:00.000 14 0002W pqo emzmnwp
2017-05-06 00:00:00.000 14 0002W pqo emzmnwp
2017-05-06 08:00:00.000 12 0002W pqo emzmnwp
2017-05-06 16:00:00.000 12 0002W pqo emzmnwp
2017-05-01 16:00:00.000 1 0007E omq ynzmeoyn
2017-05-02 00:00:00.000 1 0007E omq ynzmeoyn
It is taking the 8 hour timing as well. Do any of you have any idea why?

SQLSERVER 2008: Breaking a output for the entire day into 2 records for 12 hours

I am looking for the count of records as below.
PLANNED_SHIP_From_Date PLANNED_SHIP_To Date Total_Lines_Count ....
1) 09-04-2016 07:00:01 09-04-2016 18:59:59 165 .....
2) 09-04-2016 19:00:00 10-04-2016 07:00:00 121 .....
3) 10-04-2016 07:00:01 10-04-2016 18:59:59 165 .....
4) 10-04-2016 19:00:00 11-04-2016 07:00:00 123 .....
5) 11-04-2016 07:00:01 11-04-2016 18:59:59 234 .....
.
Currently my query is counting the records as per date.
SELECT
cast(shdr.PLANNED_SHIP_DATE as date),
SUM(sdtl_1_1.TOTAL_LINES_COUNT) AS TOTAL_LINES_COUNT
FROM
dbo.SHIPMENT_HEADER AS shdr WITH (NOLOCK)
INNER JOIN
(
SELECT
SHIPMENT_ID,
COUNT(*) AS TOTAL_LINES_COUNT
FROM
dbo.SHIPMENT_DETAIL AS SHIPMENT_DETAIL_1 WITH (NOLOCK)
WHERE
(
STATUS1 >= 401
)
AND (
DATEDIFF(day, PLANNED_SHIP_DATE, CONVERT(date, SYSDATETIME())) < 4
)
GROUP BY
SHIPMENT_ID
) AS sdtl_1_1
ON sdtl_1_1.SHIPMENT_ID = shdr.SHIPMENT_ID
WHERE
(
shdr.TRAILING_STS >= 401
)
AND (
DATEDIFF(day, shdr.PLANNED_SHIP_DATE, CONVERT(date, SYSDATETIME())) < 4
)
GROUP BY
cast(shdr.PLANNED_SHIP_DATE as date)
Try this -
DECLARE #ReportDays int = 30,
#StartHr int = 7,
#Today DATETIME2 = CAST(SYSDATETIME() AS DATE);
--http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/you-require-a-numbers-table.aspx
WITH
a AS (SELECT 1 AS i UNION ALL SELECT 1),
b AS (SELECT 1 AS i FROM a AS x, a AS y),
c AS (SELECT 1 AS i FROM b AS x, b AS y),
d AS (SELECT 1 AS i FROM c AS x, c AS y),
e AS (SELECT 1 AS i FROM d AS x, d AS y),
numbers as (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS number FROM e),
StartDates AS (
SELECT
DATEADD(
HH,
#StartHr + (n2.number * 12),
DATEADD(D, 0-n1.number, #Today)
) AS StartDT
FROM
(SELECT * FROM numbers WHERE Number BETWEEN 0 AND #ReportDays) n1
CROSS JOIN (SELECT * FROM numbers WHERE Number IN (0,1)) n2
),
DateRanges AS
(SELECT StartDT, DATEADD(hh, 12, StartDT) AS EndDT FROM StartDates),
Shipments AS
(SELECT
StartDT AS PLANNED_SHIP_From_Date,
EndDT AS PLANNED_SHIP_To_Date,
1 AS Shipment
FROM
DateRanges dr
LEFT JOIN dbo.SHIPMENT_DETAIL sd
ON sd.Status1 >=401
AND sd.PLANNED_SHIP_DATE BETWEEN dr.StartDT AND dr.EndDT)
SELECT
PLANNED_SHIP_From_Date,
PLANNED_SHIP_To_Date,
SUM(Shipment) AS TOTAL_LINES_COUNT
FROM
Shipments
ORDER BY
PLANNED_SHIP_From_Date;
What we're doing is -
Building a numbers table
Using that to pull a list of days, with two records per day
Working out the start & finish times for each time window
Joining the time windows to the records and summing
Hope that helps :-)
Add another column to your select....
CASE
WHENE DATEPART(HOUR, Planned_SHIP_DATE) < 12 THEN 'AM' ELSE 'PM'
END AS ShipPeriod
You could then add that column into a GROUPING to seperate the 'AM's from 'PM's
Of course I have assuumed you wanted AM/PM. But you can modify the CASE statement to break the hours up as you see fit.
Hope this helps
Thank you all for helping me out.
I have created a SQL query which worked for me. This query gives the count of records from morning 7 AM to 7 PM as MORNING_SHIFT count and 7PM to next day 7AM morning as EVENING_SHIFT for dates greater than 14 days in the past.
SELECT
CASE
WHEN convert(VARCHAR(50), sh.PLANNED_SHIP_DATE, 120) BETWEEN
(convert(VARCHAR(10), sh.PLANNED_SHIP_DATE, 120) + ' 07:00:00') AND
(convert(VARCHAR(10), sh.PLANNED_SHIP_DATE, 120) + ' 18:59:59')
THEN (CONCAT(cast(sh.PLANNED_SHIP_DATE as date),' ','morning_shift'))
WHEN convert(VARCHAR(50), sh.PLANNED_SHIP_DATE, 120) BETWEEN
(convert(VARCHAR(10), sh.PLANNED_SHIP_DATE, 120) + ' 00:00:00') AND
(convert(VARCHAR(10), sh.PLANNED_SHIP_DATE, 120) + ' 06:59:59')
then (CONCAT(cast(DATEADD(DAY, -1, sh.PLANNED_SHIP_DATE) as date),' ','EVENING_shift'))
when
convert(VARCHAR(50), DATEADD(DAY, -1, sh.PLANNED_SHIP_DATE) , 120) BETWEEN (convert(VARCHAR(10), cast(DATEADD(DAY, -1, sh.PLANNED_SHIP_DATE) as date), 120) + ' 19:00:00') AND
(convert(VARCHAR(10), cast(DATEADD(DAY, -1, sh.PLANNED_SHIP_DATE) as date), 120) + ' 23:59:59')
THEN (CONCAT(cast(DATEADD(DAY, -1, sh.PLANNED_SHIP_DATE) as date),' ','EVENING_shift'))
END AS 'actual_date_time', sh.PLANNED_SHIP_DATE
FROM dbo.SHIPMENT_HEADER AS sh WITH (nolock)
WHERE (shdr.TRAILING_STS >= 401) AND (DATEDIFF(day, shdr.ACTUAL_SHIP_DATE_TIME, CONVERT(date, SYSDATETIME())) < 14)
group by sh.ACTUAL_SHIP_DATE_TIME;

How to count number of logins every minute and return zero if the result is null

It appears there is a lot of information out there regarding this topic, but I don't have enough SQL knowledge to apply it to my situation.
This is the query I'm currently working with:
/* Number of successful logins per minute for a given date range */
SELECT
DATEADD(minute, DATEDIFF(minute, 0, AuditMessage.EventDateTime), 0) AS Time,
COUNT(AuditMessage.EventTypeCodeUid) AS CountSuccessfulLoginAttemptsPerMinute
FROM IRWSDB.dbo.AuditMessage
JOIN AuditEventTypeCode
ON AuditEventTypeCode.EventTypeCodeUid = AuditMessage.EventTypeCodeUid
WHERE AuditEventTypeCode.DisplayName = 'Login'
AND AuditMessage.EventDateTime >= CONVERT(DATETIME, '2016-03-03 00:00:00', 120)
AND AuditMessage.EventDateTime <= CONVERT(DATETIME, '2016-03-04 00:00:00', 120)
GROUP BY DATEADD(minute, DATEDIFF(minute, 0, AuditMessage.EventDateTime), 0)
ORDER BY DATEADD(minute, DATEDIFF(minute, 0, AuditMessage.EventDateTime), 0)
Example Output (works as expected):
Time CountSuccessfulLoginAttemptsPerMinute
2016-03-03 17:48:00.000 1
2016-03-03 17:49:00.000 1
2016-03-03 17:50:00.000 1
2016-03-03 17:55:00.000 2
Desired Output:
Time CountSuccessfulLoginAttemptsPerMinute
2016-03-03 17:48:00.000 1
2016-03-03 17:49:00.000 1
2016-03-03 17:50:00.000 1
2016-03-03 17:51:00.000 0
2016-03-03 17:52:00.000 0
2016-03-03 17:53:00.000 0
2016-03-03 17:54:00.000 0
2016-03-03 17:55:00.000 2
I tried to modify the above query to get the desired output, but every path I've gone down has been a dead end. Any help would be greatly appreciated. Thank you.
If your database doesn't have every min of the day, it won't be able to get desired output. Unless you backend/whatever will insert a record to your table every min or refer to this link Fill empty dates in a matrix SSRS. Then you can modify your query like this
SELECT
DATEADD(minute, DATEDIFF(minute, 0, AuditMessage.EventDateTime), 0) AS Time,
ISNULL(COUNT(AuditMessage.EventTypeCodeUid),0) AS CountSuccessfulLoginAttemptsPerMinute
FROM IRWSDB.dbo.AuditMessage
JOIN AuditEventTypeCode
ON AuditEventTypeCode.EventTypeCodeUid = AuditMessage.EventTypeCodeUid
WHERE AuditEventTypeCode.DisplayName = 'Login'
AND AuditMessage.EventDateTime >= CONVERT(DATETIME, '2016-03-03 00:00:00', 120)
AND AuditMessage.EventDateTime <= CONVERT(DATETIME, '2016-03-04 00:00:00', 120)
GROUP BY DATEADD(minute, DATEDIFF(minute, 0, AuditMessage.EventDateTime), 0)
ORDER BY DATEADD(minute, DATEDIFF(minute, 0, AuditMessage.EventDateTime), 0)
First, you need to generate all minutes in the date range. To do this, you need a Tally Table. Then do a LEFT JOIN on your original query.
DECLARE #start DATETIME,
#end DATETIME
SELECT #start = '20160303', #end = '20160304'
;WITH E1(N) AS(
SELECT 1 FROM(VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))t(N)
),
E2(N) AS(SELECT 1 FROM E1 a CROSS JOIN E1 b),
E4(N) AS(SELECT 1 FROM E2 a CROSS JOIN E2 b),
CteTally(N) AS(
SELECT TOP(DATEDIFF(MINUTE, #start, #end) + 1) ROW_NUMBER() OVER(ORDER BY(SELECT NULL))
FROM E4
),
CteMinute(dt) AS(
SELECT dt = DATEADD(MINUTE, N-1, #start) FROM CteTally
)
SELECT
cm.dt AS [Time],
ISNULL(t.cnt, 0) AS CountSuccessfulLoginAttemptsPerMinute
FROM CteMinute cm
LEFT JOIN (
SELECT
DATEADD(MINUTE, DATEDIFF(MINUTE, 0, am.EventDateTime), 0) AS [Time],
COUNT(am.EventTypeUid) AS cnt
FROM IRWSDB.dbo.AuditMessage am
INNER JOIN AuditEventTypeCode atc
ON atc.EventTypeCodeUid = am.EventTypeCodeUid
WHERE
atc.DisplayName = 'Login'
AND am.EventDateTime >= #start
AND am.EventDateTime <= #end
GROUP BY DATEADD(MINUTE, DATEDIFF(MINUTE, 0, am.EventDateTime), 0)
) t
ON cm.dt = t.[Time]
GROUP BY cm.dt
One way is to use LEFT JOIN of "list of every minutes" to your current query
2nd way is to UNION ALL the current query with the "list of every minutes" and then do a SUM on the Count
There are may ways to create the "list of every minutes" like using tally/number table, recursive cte, cross join etc.
Here is a method using recursive cte
;with minutes as
(
select Time = convert(datetime, '2016-03-03')
union all
select Time = dateadd(minute, 1, Time)
from minutes
where Time < '2016-03-05'
)
select Time
from minues
Method 1 : LEFT JOIN
;with minutes as
(
select Time = convert(datetime, '2016-03-03')
union all
select Time = dateadd(minute, 1, Time)
from minutes
where Time < '2016-03-05'
),
data as
(
< your current query here without the order by clause>
)
select m.Time, isnull(CountSuccessfulLoginAttemptsPerMinute, 0) as CountSuccessfulLoginAttemptsPerMinute
from minues m
left join data d on m.Time = d.Time
Method 2 : UNION ALL
;with minutes as
(
select Time = convert(datetime, '2016-03-03')
union all
select Time = dateadd(minute, 1, Time)
from minutes
where Time < '2016-03-05'
)
select Time, sum(CountSuccessfulLoginAttemptsPerMinute) as CountSuccessfulLoginAttemptsPerMinute
FROM
(
< your current query here without the order by clause>
union all
select Time, 0 as CountSuccessfulLoginAttemptsPerMinute
from mintues
) d
group by Time

Divide up a work day into blocks of time set up in employee schedule SQL

I have a database which contains a table of time clock setup entries. This serves as the "buckets" of time an employee's day could fall into. This table can contain any number of buckets as long as they don't overlap eachother.
For example, this is our table of buckets:
ID Start End timeType
-------------------------------------------------
1 08:00:00.000 12:00:00.000 REGULAR
1 12:00:00.000 12:30:00.000 BREAK
1 12:30:00.000 16:00:00.000 REGULAR
1 16:00:00.000 00:00:00.000 OVERTIME
I have a punch in time of say, 07:55 and a punch out time of 17:00. I need to figure out how much of my day falls into each bucket, in hours, minutes and seconds. The data output has to look like this and I can not add columns to either table:
ID Start End timeType hrs
-----------------------------------------------------
1 07:55:00.000 12:00:00.000 REGULAR 4.08
1 12:00:00.000 12:30:00.000 BREAK 0.50
1 12:30:00.000 16:00:00.000 REGULAR 3.50
1 16:00:00.000 00:00:00.000 OVERTIME 1.00
I'm thinking a SQL inline table valued function that will be run for one day at a time, but I am having trouble getting to the hours calculation piece. So far, I think I have the logic for all scenarios, I just need help with calculating the hours as a decimal(5,2) for each scenario. I'm putting this out there for SQL suggestions but also...am I over complicating this?
Here's my stab at the logic for each scenario:
Select Case When CONVERT(time, #PunchInDate) <= CONVERT(time, EndDate)
And CONVERT(time, #PunchInDate) >= CONVERT(time, StartDate)
AND CONVERT(time, #PunchOutDate) <= CONVERT(time, EndDate)
AND CONVERT(time, #PunchOutDate) >= CONVERT(time, StartDate)Then
'Starts and ends in this range.'
Else
''
End as ScenarioA
, Case
When CONVERT(time, #PunchInDate) <= CONVERT(time, StartDate)
AND CONVERT(time, #PunchInDate) <= CONVERT(time, EndDate)
AND CONVERT(time, #PunchOutDate) >= CONVERT(time, StartDate)
AND CONVERT(time, #PunchOutDate) <= CONVERT(time, EndDate)
Then
'Starts before this range and ends in this range'
Else
''
End as ScenarioB
, Case
When CONVERT(time, #PunchInDate) >= CONVERT(time, StartDate)
And CONVERT(time, #PunchInDate) <= CONVERT(time, EndDate)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, StartDate)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, EndDate)Then
'Starts in this range and ends after the range'
Else ''
END as ScenarioC
, Case
When CONVERT(time, #PunchInDate) <= CONVERT(time, StartDate)
And CONVERT(time, #PunchInDate) >= CONVERT(time, EndDate)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, StartDate)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, EndDate)Then
'Starts before this range and ends after the range'
Else ''
END as ScenarioD
From MyTable
Where EmpID = #EmpID
If you don't already have one, create a number table. This is just a table with a single int column that contains all the integers between 0 and some large number (my table goes to 9999).
create table #numbers(num int)
insert #numbers
SELECT TOP 10000 row_number() over(order by t1.number) -1 as N
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
create table #bucket(Id int, StartTime time, EndTime time, TimeType varchar(10))
insert #bucket
select 0, '00:00:00', '08:00:00', 'OTHER' union
select 1, '08:00:00', '12:00:00', 'REGULAR' union
select 2, '12:00:00', '12:30:00', 'BREAK' union
select 3, '12:30:00', '16:00:00', 'REGULAR' union
select 4, '16:00:00', '00:00:00', 'OVERTIME'
declare #punchInDate datetime
declare #punchOutDate datetime
set #punchInDate = '5/15/2014 7:55'
set #punchOutDate = '5/15/2014 17:00'
--Using the number table, break the punchIn and punchOut times into individual rows for each minute and store them in a temp table.
select convert(time, dateadd(mi, n.num, #punchInDate)) TimeMinute
into #temp
from #numbers n
where n.num <= datediff(mi, #punchInDate, #punchOutDate)
order by 1
--Now you can just join your temp rows with your bucket table, grouping and getting the count of the number of minutes in each bucket.
select b.Id, b.StartTime, b.EndTime, b.TimeType, convert(decimal, COUNT(t.TimeMinute))/60
from #bucket b
join #temp t on t.TimeMinute>= b.StartTime and t.TimeMinute <= dateadd(mi, -1, b.EndTime)
group by b.Id, b.StartTime, b.EndTime, b.TimeType
Converting Datetime or Time Bucket Values to Numbers
You should probably store the bucket Start and End values as int values, either instead of the times or in addition-to.
To convert datetime values or expressions into something you can use for matching to the buckets, you can use something like in this code:
DECLARE #when datetime = GETDATE();
SELECT DATEDIFF(minute, DATEADD( day, DATEDIFF(day, 0, #when), 0 ), #when);
--MinutesSinceStartOfDate
-------------------------
--858
--(1 row(s) affected)
If you need seconds, just change the above to something like this:
DECLARE #when datetime = GETDATE();
SELECT DATEDIFF(second, DATEADD( day, DATEDIFF(day, 0, #when), 0 ), #when);
--SecondsSinceStartOfDate
-------------------------
--52466
--(1 row(s) affected)
Matching a Range (Start and End Values) to Buckets (Partitions)
Here's some code that you should be able to adapt to match 'buckets':
CREATE TABLE #buckets (
Id int,
Start int,
Finish int
);
GO
INSERT #buckets ( Id, Start, Finish )
VALUES ( 1, 8, 12 ),
( 2, 12, 13 ),
( 3, 13, 16 ),
( 4, 16, 24 );
DECLARE #beginning int = 9,
#ending int = 17;
SELECT x.*,
EffectiveInterval = x.EffectiveFinish - x.EffectiveStart
FROM ( SELECT *,
EffectiveStart = CASE WHEN Start < #beginning THEN #beginning ELSE Start END,
EffectiveFinish = CASE WHEN Finish > #ending THEN #ending ELSE Finish END
FROM #buckets
WHERE Finish >= #beginning
AND Start <= #ending
) x;
DROP TABLE #buckets;
--(4 row(s) affected)
--Id Start Finish EffectiveStart EffectiveFinish EffectiveInterval
------------- ----------- ----------- -------------- --------------- -----------------
--1 8 12 9 12 3
--2 12 13 12 13 1
--3 13 16 13 16 3
--4 16 24 16 17 1
--(4 row(s) affected)
Comparison of your Code and Mine
CREATE TABLE #buckets (
Id int,
Start time,
Finish time
);
INSERT #buckets ( Id, Start, Finish )
VALUES ( 1, '08:00', '12:00' ),
( 2, '12:00', '12:30' ),
( 3, '12:30', '16:00' ),
( 4, '16:00', '11:59:59.999' );
DECLARE #PunchInDate datetime = '2014-05-15 07:55',
#PunchOutDate datetime = '2014-05-15 17:00';
Select Case When CONVERT(time, #PunchInDate) <= CONVERT(time, Finish)
And CONVERT(time, #PunchInDate) >= CONVERT(time, Start)
AND CONVERT(time, #PunchOutDate) <= CONVERT(time, Finish)
AND CONVERT(time, #PunchOutDate) >= CONVERT(time, Start)Then
'Starts and ends in this range.'
Else
''
End as ScenarioA
, Case
When CONVERT(time, #PunchInDate) <= CONVERT(time, Start)
AND CONVERT(time, #PunchInDate) <= CONVERT(time, Finish)
AND CONVERT(time, #PunchOutDate) >= CONVERT(time, Start)
AND CONVERT(time, #PunchOutDate) <= CONVERT(time, Finish)
Then
'Starts before this range and ends in this range'
Else
''
End as ScenarioB
, Case
When CONVERT(time, #PunchInDate) >= CONVERT(time, Start)
And CONVERT(time, #PunchInDate) <= CONVERT(time, Finish)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, Start)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, Finish)Then
'Starts in this range and ends after the range'
Else ''
END as ScenarioC
, Case
When CONVERT(time, #PunchInDate) <= CONVERT(time, Start)
And CONVERT(time, #PunchInDate) >= CONVERT(time, Finish)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, Start)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, Finish)Then
'Starts before this range and ends after the range'
Else ''
END as ScenarioD
FROM #buckets;
DECLARE #PunchInTime time,
#PunchOutTime time;
SELECT #PunchInTime = CONVERT(time, #PunchInDate),
#PunchOutTime = CONVERT(time, #PunchOutDate);
SELECT x.*,
EffectiveInterval = DATEDIFF(second, x.EffectiveStart, x.EffectiveFinish),
EffectiveIntervalDecimalHourse = DATEDIFF(second, x.EffectiveStart, x.EffectiveFinish) / 3600.
FROM ( SELECT *,
EffectiveStart = CASE WHEN Start < #PunchInTime THEN #PunchInTime ELSE Start END,
EffectiveFinish = CASE WHEN Finish > #PunchOutTime THEN #PunchOutTime ELSE Finish END
FROM #buckets
WHERE Finish >= #PunchInTime
AND Start <= #PunchOutTime
) x;
DROP TABLE #buckets;
--(4 row(s) affected)
--ScenarioA ScenarioB ScenarioC ScenarioD
-------------------------------- ----------------------------------------------- --------------------------------------------- -------------------------------------------------
--(4 row(s) affected)
--Id Start Finish EffectiveStart EffectiveFinish EffectiveInterval EffectiveIntervalDecimalHourse
------------- ---------------- ---------------- ---------------- ---------------- ----------------- ---------------------------------------
--1 08:00:00.0000000 12:00:00.0000000 08:00:00.0000000 12:00:00.0000000 14400 4.000000
--2 12:00:00.0000000 12:30:00.0000000 12:00:00.0000000 12:30:00.0000000 1800 0.500000
--3 12:30:00.0000000 16:00:00.0000000 12:30:00.0000000 16:00:00.0000000 12600 3.500000
--4 16:00:00.0000000 11:59:59.9990000 16:00:00.0000000 11:59:59.9990000 -14401 -4.000277
--(4 row(s) affected)
If you're using SQLServer 2012 you can get the expected result in a single select.
declare #punchInDate datetime = '5/15/2014 7:55'
declare #punchOutDate datetime = '5/15/2014 17:00';
WITH punchTime AS (
SELECT punchIn = cast(#punchInDate AS Time)
, punchOut = cast(#punchOutDate AS Time)
), T AS (
SELECT b.ID
, b.StartTime, b.EndTime
, pt.punchIn, pt.punchOut
, sIn = SUM(CASE WHEN pt.punchIn < b.StartTime
THEN 1
ELSE 0
END) OVER (ORDER BY ID)
, sOut = SUM(CASE WHEN pt.punchOut > b.StartTime
THEN 1
ELSE 0
END) OVER (ORDER BY ID DESC)
FROM bucket b
CROSS JOIN punchTime pt
), C AS (
SELECT ID
, StartTime = CASE sIn WHEN 1 THEN punchIn ELSE StartTime END
, EndTime = CASE sOut WHEN 1 THEN punchOut ELSE EndTime END
FROM T
)
SELECT ID
, StartTime
, EndTime
, hrs = Cast(DateDiff(mi, StartTime, EndTime) / 60.0 AS Decimal(4, 2))
FROM C
ORDER BY ID
SQLFiddle demo (in the demo the punch times are in a table)
Using SUM OVER(ORDER BY) we get a rolling sum.
sIn will be 1 in the first row where StartTime is after punchIn.
sOut will be 1 in the last row where StartTime is before punchOut.
With those pointers is easy to substitute the punch time to the standard bucket time and get the worked hours.

Resources