SQL TSQL for Workers per Hour - sql

I have a log with fingerprint timestamps as follows:
Usr TimeStamp
-------------------------
1 2015-07-01 08:01:00
2 2015-07-01 08:05:00
3 2015-07-01 08:07:00
1 2015-07-01 10:05:00
3 2015-07-01 11:00:00
1 2015-07-01 12:01:00
2 2015-07-01 13:03:00
2 2015-07-01 14:02:00
1 2015-07-01 16:03:00
2 2015-07-01 18:04:00
And I wish an output of workers per hour (rounding to nearest hour)
The theoretical output should be:
7:00 0
8:00 3
9:00 3
10:00 2
11:00 1
12:00 2
13:00 1
14:00 2
15:00 2
16:00 1
17:00 1
18:00 0
19:00 0
Can anyone think on how to approach this as SQL or if no other way, through TSQL?
Edit: The timestamps are logins and logouts of the different users. So at 8am 3 users logged in and the same 3 are still working at 9am. One of them leaves at 10am. etc

To start with you can use datepart to get hours for the days as following and then use group by user
SELECT DATEPART(HOUR, GETDATE());
SQL Fiddle
SELECT Convert(varchar(5),DATEPART(HOUR, timestamp)) + ':00' as time,
count(usr) as users
from tbl
group by DATEPART(HOUR, timestamp)

You need a datetime hour table to do this.
Note : This is just a example of showing how the query should work for one day. Replace the CTE with datetime hour table. In datetime hour table every date should start with 07:00:00 hour and end with 19:00:00 hour
When you want to do this for more than one day then you may have to include the Cast(dt.date_time AS DATE) in select and group by to differentiate the hour belong to which day
WITH datetime_table
AS (SELECT '2015-07-01 07:00:00' AS date_time
UNION ALL
SELECT '2015-07-01 08:00:00'
UNION ALL
SELECT '2015-07-01 09:00:00'
UNION ALL
SELECT '2015-07-01 10:00:00'
UNION ALL
SELECT '2015-07-01 11:00:00'
UNION ALL
SELECT '2015-07-01 12:00:00'
UNION ALL
SELECT '2015-07-01 13:00:00'
UNION ALL
SELECT '2015-07-01 14:00:00'
UNION ALL
SELECT '2015-07-01 15:00:00'
UNION ALL
SELECT '2015-07-01 16:00:00'
UNION ALL
SELECT '2015-07-01 17:00:00'
UNION ALL
SELECT '2015-07-01 18:00:00'
UNION ALL
SELECT '2015-07-01 19:00:00')
SELECT Datepart(hour, dt.date_time),
Hour_count=Count(t.id)
FROM datetime_table dt
LEFT OUTER JOIN Yourtable t
ON Cast(t.dates AS DATE) = Cast(dt.date_time AS DATE)
AND Datepart(hour, t.dates) =
Datepart(hour, dt.date_time)
GROUP BY Datepart(hour, dt.date_time)
SQLFIDDLE DEMO

You just need to group by hours and date. Check this below query and hope this helps you:
Create table #t1
(
usr int,
timelog datetime
)
Insert into #t1 values(1, '2015-07-01 08:01:00')
Insert into #t1 values(2, '2015-07-01 08:05:00')
Insert into #t1 values(3, '2015-07-01 08:07:00')
Insert into #t1 values(1, '2015-07-01 10:05:00')
Insert into #t1 values(3, '2015-07-01 11:00:00')
Insert into #t1 values(1, '2015-07-01 12:01:00')
Insert into #t1 values(2, '2015-07-01 13:03:00')
Insert into #t1 values(2, '2015-07-01 14:02:00')
Insert into #t1 values(1, '2015-07-01 16:03:00')
Insert into #t1 values(2, '2015-07-01 18:04:00')
Select cast(timelog as varchar(11)) as LogDate, Datepart(hour, timelog) as LogTime, count(usr) as UserCount from #t1
Group by Datepart(hour, timelog), cast(timelog as varchar(11))

The harder part is creating the zeros where data is missing. The usual approach is to generate a list of all possible "slots" and then do an outer join to the actual data. I'm assuming that you only want to run this for a single day at a time.
My approach, which is just an example, works because it does a cross join of two tables with 6 and 4 rows respectively and 6 times 4 is 24.
select f1.d * 6 + f0.d, coalesce(data.cnt, 0)
from
(
select 0 as d union all select 1 union all select 2 union all
select 3 union all select 4 union all select 5
) as f0,
(
select 0 as d union all select 1 union all
select 2 union all select 3
) as f1
left outer join
(
select
cast(datepart(hh, TimeStamp) as varchar(2)) + ':00' as hr,
count(*) as cnt
from LOG
group by datepart(hh, TimeStamp)
) as data
on data.hr = f1.d * 6 + f0.d

First you need to round up time to the closest hour
DATEADD(HOUR, DATEDIFF(HOUR, 0, DATEADD(MI, 30, TimeStamp)), 0)
As you see first we add 30 minutes to the original time (DATEADD(MI, 30, TimeStamp))
This approach will round up 08:04 to 08:00 or 07:58 to 8:00 too.
As I assume some workers can start working little bid early
SELECT DATEADD(HOUR, DATEDIFF(HOUR, 0, DATEADD(MI, 30, TimeStamp)), 0) As FingertipTime
FROM Fingertips
You can create a Computed column if you use rounded timestamp often
ALTER TABLE Fingertips ADD RoundedTimeStamp AS (DATEADD(HOUR, DATEDIFF(HOUR, 0, DATEADD(MI, 30, TimeStamp)), 0));
For comparing timestamps with constants of work hours you can find different methods. I will use a variable of type TABLE where i generate work hours for current day
Then using LEFT JOIN and GROUP BY we get quantity of timestamps
DECLARE #WorkHours TABLE(WorkHour DATETIME)
INSERT INTO #WorkHours (WorkHour) VALUES
('2015-07-01 07:00'),
('2015-07-01 08:00'),
('2015-07-01 09:00'),
('2015-07-01 10:00'),
('2015-07-01 11:00'),
('2015-07-01 12:00'),
('2015-07-01 13:00'),
('2015-07-01 14:00'),
('2015-07-01 15:00'),
('2015-07-01 16:00'),
('2015-07-01 17:00'),
('2015-07-01 18:00'),
('2015-07-01 19:00')
SELECT wh.Workhour
, COUNT(ft.TimeStamp) As Quantity
FROM #WorkHours wh
LEFT JOIN Fingertips ft ON ft.RoundedTimeStamp = wh.WorkHour
GROUP BY wh.WorkHour
Check this SQL Fiddle

Many separate parts that have to be glued together to get this done.
First rounding, this is easily done with obtaining the hour part of the date + 30 minutes. Then determine start and end records. If there are no fields to indicate this and assuming the first occurrence of a day is the login or start, you can use row_number and use the odd numbers as start records.
Then start and end have to be coupled, in sql server 2012 and higher this can be easily done with the lead function
To get the missing hours a sequence has to be created with all the hours. Several options for this (good link here), but I like the approach of using row_number on a table that is sure to contain enough rows (with a proper column for order by), such as sys.all_objects used in the link. That way hours 7 to 19 could be created as: select top 13 ROW_NUMBER() over (order by object_id) + 6 [Hour] from sys.all_objects
If there's only one date to check on, the query can simple left join on the hour of the timestamp fingerprints. If there are more dates, a second sequence could be created cross applied to the times to get all dates. Assuming the one date, final code would be:
declare #t table(Usr int, [timestamp] datetime)
insert #t values
(1 , '2015-07-01 08:01:00'),
(2 , '2015-07-01 08:05:00'),
(3 , '2015-07-01 08:07:00'),
(1 , '2015-07-01 10:05:00'),
(3 , '2015-07-01 11:00:00'),
(1 , '2015-07-01 12:01:00'),
(2 , '2015-07-01 13:03:00'),
(2 , '2015-07-01 14:02:00'),
(1 , '2015-07-01 16:03:00'),
(2 , '2015-07-01 18:04:00'),
(2 , '2015-07-01 18:04:00')
;with usrHours as
(
select Usr, datepart(hour, DATEADD(minute,30, times.timestamp)) [Hour] --convert all times to the rounded hour (rounding by adding 30 minutes)
, ROW_NUMBER() over (partition by usr order by [timestamp] ) rnr
from #t times --#t should be your logging table
), startend as --get next (end) hour by using lead
(
select Usr, [hour] StartHour , LEAD([Hour]) over (partition by usr order by rnr) NextHour ,rnr
from usrHours
),hours as --sequence of hours 7 to 19
(
select top 13 ROW_NUMBER() over (order by object_id) + 6 [Hour] from sys.all_objects
)
select cast([Hour] as varchar) + ':00' [Hour], COUNT(startend.usr) Users
from hours --sequence is leading
left join startend on hours.Hour between startend.StartHour and startend.NextHour
and rnr % 2 = 1 --every odd row number is a start time
group by Hours.hour

Here is my final working code:
create table tsts(id int, dates datetime)
insert tsts values
(1 , '2015-07-01 08:01:00'),
(2 , '2015-07-01 08:05:00'),
(3 , '2015-07-01 08:07:00'),
(1 , '2015-07-01 10:05:00'),
(3 , '2015-07-01 11:00:00'),
(1 , '2015-07-01 12:01:00'),
(2 , '2015-07-01 13:03:00'),
(2 , '2015-07-01 14:02:00'),
(1 , '2015-07-01 16:03:00'),
(2 , '2015-07-01 18:04:00')
select horas.hora, isnull(sum(math) over(order by horas.hora rows unbounded preceding),0) as Employees from
(
select 0 as hora union all
select 1 as hora union all
select 2 as hora union all
select 3 as hora union all
select 4 as hora union all
select 5 as hora union all
select 6 as hora union all
select 7 as hora union all
select 8 as hora union all
select 9 as hora union all
select 10 as hora union all
select 11 as hora union all
select 12 as hora union all
select 13 as hora union all
select 14 as hora union all
select 15 as hora union all
select 16 as hora union all
select 17 as hora union all
select 18 as hora union all
select 19 as hora union all
select 20 as hora union all
select 21 as hora union all
select 22 as hora union all
select 23
) as horas
left outer join
(
select hora, sum(math) as math from
(
select id, hora, iif(rowid%2 = 1,1,-1) math from
(
select row_number() over (partition by id order by id, dates) as rowid, id, datepart(hh,dateadd(mi, 30, dates)) as hora from tsts
) as Q1
) as Q2
group by hora
) as Q3
on horas.hora = Q3.hora
SQL Fiddle

Related

Date wise hourly (on 24 hour) coustomer count

I have a data set where customer id , customer join time and leave time available. I want to count hourly basis each date customer
Here is sample data set
My expected output
Here I going to add my code snip that i tried,where 1st created 24 hours span then tried to join and aggregate function for getting expected result and got for current date but i need for any date i.e dynamically
select logdate as date,timespan,count(customer_id)
(
SELECT userid,cast(joinTime as date) as logdate,customer_id
,starttime,endtime,timespan
FROM login_out_logs AS logTable
left join
(select '00:00:00 - 01:00:00' timespan,DATEadd(hh,0,cast(dateadd(dd,-1,getdate()))) starttime,dateadd(hh,1,cast(dateadd(dd,-1,getdate()))) endtime
union
select '01:00:00 - 02:00:00', dateadd(hh,1,cast(dateadd(dd,-1,getdate()))),dateadd(hh,2,cast(dateadd(dd,-1,getdate())))
union
select '02:00:00 - 03:00:00', dateadd(hh,2,cast(dateadd(dd,-1,getdate()))),dateadd(hh,3,cast(dateadd(dd,-1,getdate())))
union
select '03:00:00 - 04:00:00', dateadd(hh,3,cast(dateadd(dd,-1,getdate()))),dateadd(hh,4,cast(dateadd(dd,-1,getdate())))
union
select '04:00:00 - 05:00:00', dateadd(hh,4,cast(dateadd(dd,-1,getdate()))),dateadd(hh,5,cast(dateadd(dd,-1,getdate())))
union
select '05:00:00 - 06:00:00',dateadd(hh,5,cast(dateadd(dd,-1,getdate()))),dateadd(hh,6,cast(dateadd(dd,-1,getdate())))
union
select '06:00:00 - 07:00:00',dateadd(hh,6,cast(dateadd(dd,-1,getdate()))),dateadd(hh,7,cast(dateadd(dd,-1,getdate())))
union
select '07:00:00 - 08:00:00',dateadd(hh,7,cast(dateadd(dd,-1,getdate()))),dateadd(hh,8,cast(dateadd(dd,-1,getdate())))
union
select '08:00:00 - 09:00:00',dateadd(hh,8,cast(dateadd(dd,-1,getdate()))),dateadd(hh,9,cast(dateadd(dd,-1,getdate())))
union
select '09:00:00 - 10:00:00',dateadd(hh,9,cast(dateadd(dd,-1,getdate()))),dateadd(hh,10,cast(dateadd(dd,-1,getdate())))
union
select '10:00:00 - 11:00:00',dateadd(hh,10,cast(dateadd(dd,-1,getdate()))),dateadd(hh,11,cast(dateadd(dd,-1,getdate())))
union
select '11:00:00 - 12:00:00',dateadd(hh,11,cast(dateadd(dd,-1,getdate()))),dateadd(hh,12,cast(dateadd(dd,-1,getdate())))
union
select '12:00:00 - 13:00:00',dateadd(hh,12,cast(dateadd(dd,-1,getdate()))),dateadd(hh,13,cast(dateadd(dd,-1,getdate())))
union
select '13:00:00 - 14:00:00',dateadd(hh,13,cast(dateadd(dd,-1,getdate()))),dateadd(hh,14,cast(dateadd(dd,-1,getdate())))
union
select '14:00:00 - 15:00:00',dateadd(hh,14,cast(dateadd(dd,-1,getdate()))),dateadd(hh,15,cast(dateadd(dd,-1,getdate())))
union
select '15:00:00 - 16:00:00',dateadd(hh,15,cast(dateadd(dd,-1,getdate()))),dateadd(hh,16,cast(dateadd(dd,-1,getdate())))
union
select '16:00:00 - 17:00:00',dateadd(hh,16,cast(dateadd(dd,-1,getdate()))),dateadd(hh,17,cast(dateadd(dd,-1,getdate())))
union
select '17:00:00 - 18:00:00',dateadd(hh,17,cast(dateadd(dd,-1,getdate()))),dateadd(hh,18,cast(dateadd(dd,-1,getdate())))
union
select '18:00:00 - 19:00:00',dateadd(hh,18,cast(dateadd(dd,-1,getdate()))),dateadd(hh,19,cast(dateadd(dd,-1,getdate())))
union
select '19:00:00 - 20:00:00',dateadd(hh,19,cast(dateadd(dd,-1,getdate()))),dateadd(hh,20,cast(dateadd(dd,-1,getdate())))
union
select '20:00:00 - 21:00:00',dateadd(hh,20,cast(dateadd(dd,-1,getdate()))),dateadd(hh,21,cast(dateadd(dd,-1,getdate())))
union
select '21:00:00 - 22:00:00',dateadd(hh,21,cast(dateadd(dd,-1,getdate()))),dateadd(hh,22,cast(dateadd(dd,-1,getdate())))
union
select '22:00:00 - 23:00:00',dateadd(hh,22,cast(dateadd(dd,-1,getdate()))),dateadd(hh,23,cast(dateadd(dd,-1,getdate())))
union
select '24:00:00 - 00:00:00',dateadd(hh,23,cast(dateadd(dd,-1,getdate()))),dateadd(hh,23,dateadd(mi,59,cast(dateadd(dd,-1,getdate())))))a
on starttime between jointime and leaveTime
or endtime between jointime and leaveTime
or jointime>=starttime and jointime<endtime
) as T
group by leaveTime,timespan
Date Hour customer_count
2018-01-01 8-9 1
2018-01-01 9-10 1
2018-01-01 10-11 1
2018-01-01 11-12 1
2018-01-01 12-13 1
2018-01-01 13-14 1
2018-01-01 14-15 1
2018-01-01 15-16 1
2018-01-01 16-17 1
2018-01-01 17-18 1
2018-01-01 18-19 1
2018-01-01 19-20 1
2018-01-01 20-21 2
2018-01-01 21-22 3
2018-01-01 22-23 2
2018-01-01 23-00 1
Here is an approach - maybe this already solves your problem. I designed it in order to work with any day-difference between join and leave. However, I can't tell anything about the performance on larger sets since I tested with your example only and the evaluation of all relevant hours might take a bit longer if it comes to bigger data sets.
Anyways, I used a recursice cte here in order to evaluate all hours between join and leave and lateron I group by date and hour:
DECLARE #Cust TABLE(
customer_id INT,
joinTime DATETIME,
leaveTime DATETIME
)
INSERT INTO #Cust VALUES
(536, '2018-01-01 08:05:00', '2018-01-01 18:31:00'),
(344, '2018-01-01 19:37:00', '2018-01-01 20:16:00'),
(344, '2018-01-01 19:49:00', '2018-01-01 20:00:00'),
(899, '2018-01-01 20:49:00', '2018-01-01 21:14:00'),
(2336, '2018-01-01 21:02:00', '2018-01-01 21:03:00'),
(335, '2018-01-01 21:03:00', '2018-01-01 23:43:00'),
(2336, '2018-01-01 21:03:00', '2018-01-02 00:06:00'),
(899, '2018-01-01 21:18:00', '2018-01-01 22:24:00'),
(345, '2018-01-01 21:21:00', '2018-01-01 21:39:00'),
(345, '2018-01-01 21:53:00', '2018-01-02 00:13:00');
;WITH cte AS(
SELECT c.customer_id,
c.joinTime,
c.leaveTime,
c.joinTime x
FROM #Cust c
UNION ALL
SELECT c.customer_id,
c.joinTime,
c.leaveTime,
DATEADD(HOUR, 1, x) x
FROM cte c
WHERE DATEADD(HOUR, 1, x) <= CASE WHEN DATEPART(MINUTE, x) < DATEPART(MINUTE, c.leaveTime) THEN c.leaveTime ELSE DATEADD(HOUR, 1, c.leaveTime) END
)
SELECT CONVERT(DATE, x) AS cDate, DATEPART(HOUR, x) AS cHour, COUNT(*) AS cCount
FROM cte
GROUP BY CONVERT(DATE, x), DATEPART(HOUR, x)
ORDER BY 1,2
OPTION (MAXRECURSION 0)
Try this:
;WITH hourlist(starthour) AS (
SELECT 0 -- Seed Row
UNION ALL
SELECT starthour + 1 -- Recursion
FROM hourlist
where starthour+1<=23
)
SELECT
day
,convert(nvarchar,starthour)+'-'+convert(nvarchar,case when starthour+1=24 then 0 else starthour+1 end) hourtitle
,count(distinct customer_id) 'customer count'
FROM
hourlist h -- list of all hourse
cross join
(
select distinct dateadd(day,datediff(day,0, joinTime),0) from #login_out_logs
union
select distinct dateadd(day,datediff(day,0,leaveTime),0) from #login_out_logs
)q10(day) -- list of all days of jointime and leavetime
inner join #login_out_logs l on -- log considered for specific day/hour if starts before hourend and ends before hourstart
l.joinTime <dateadd(hour,starthour+1,q10.day)
and
l.leaveTime>=dateadd(hour,starthour ,q10.day)
group by day,starthour
order by day,starthour
Note: this will only work for jointimes and leavetimes that differ 0 or 1 days, not 2 or more.

Grouping incident counts into 5 minute time segments

Hoping someone can assist with how to modify the following SQL to achieve the result shown in desired output. I am not fluent in TSQL but know enough to get this far.
My objectrive is to count my incident data and group into 15 minutes time segments starting at midnight but also include zero (0) where there is no incident data in a particular time segment.
Curent Query#
;With cte As
(SELECT CONVERT (varchar(5),DATEADD(minute, 15 *
(DATEDIFF(minute, '20000101', I.CreateTimestamp) / 15), '20000101'),108)
AS CreationTime, I.IncidentShortReference AS Ref
FROM Incident I
WHERE i.CreateTimestamp between DATEADD(d,-1,GETDATE()) and GETDATE()
)
SELECT CTE.CreationTime, count(CTE.Ref) As Count
FROM cte CTE
GROUP BY CTE.CreationTime
ORDER BY CTE.CreationTime
My result
CreationTime count
00:15 2
01:00 1
01:15 1
01:30 1
01:45 2
02:00 1
02:15 1
02:30 4
(Truncated)
Desired Output
CreationTime count
00:15 2
00:30 0
00:45 0
01:00 1
01:15 1
01:30 1
01:45 2
02:00 1
02:15 1
02:30 4
02:45 0
03:00 0
(Truncated)
This uses a cte creating a record for every timestamp between midnight yesterday and now, with a count for the number of incidents in each range with sample data:
declare #incident table (CreateTimestamp datetime, IncidentShortReference varchar(5))
insert into #incident values ('4/10/2017 11:11:00', 'test')
insert into #incident values ('4/10/2017 11:12:00', 'test')
insert into #incident values ('4/10/2017 11:21:00', 'test')
insert into #incident values ('4/10/2017 11:31:00', 'test')
insert into #incident values ('4/10/2017 13:31:00', 'test')
DECLARE #dt datetime
SELECT #dt = dateadd(d, datediff(d, 0, getdate()), 0) - 1 -- yesterday at midnight
;with cte as
(
select #dt dt
union all
select DATEADD(minute, 15, dt) as Next15
FROM cte
WHERE DATEADD(minute, 15, dt) < GETDATE()
)
select convert(varchar(5), dt, 108) as CreationTime, (select count(*) FROM #incident WHERE CreateTimestamp >= dt and CreateTimestamp < dateadd(mi, 15, dt)) as count
from cte
Sample output from a random interval:
You could create a time interval CTE table like this
WITH TIME_CTE
AS(
SELECT
CAST('20170411 00:15:00' AS DATETIME) AS TimePeriod
UNION ALL
SELECT
DATEADD(MINUTE, 15, TimePeriod)
FROM TIME_CTE
WHERE
DATEADD(MINUTE, 15, TimePeriod) < CAST('20170411 23:59:00' AS DATETIME)
)
SELECT
LEFT(CONVERT(VARCHAR(10),TimePeriod,108), 5)
FROM TIME_CTE
Then join it with your original query
WITH TIME_CTE
AS(
SELECT
CAST('20170411 00:15:00' AS DATETIME) AS TimePeriod
UNION ALL
SELECT
DATEADD(MINUTE, 15, TimePeriod)
FROM TIME_CTE
WHERE
DATEADD(MINUTE, 15, TimePeriod) < CAST('20170411 23:59:00' AS DATETIME)
),
CTE
AS (
SELECT CONVERT (varchar(5),DATEADD(minute, 15 *
(DATEDIFF(minute, '20000101', I.CreateTimestamp) / 15), '20000101'),108)
AS CreationTime, I.IncidentShortReference AS Ref
FROM Incident I
WHERE i.CreateTimestamp between DATEADD(d,-1,GETDATE()) and GETDATE()
)
SELECT TIME_CTE.TimePeriod, SUM(IIF(CTE.Ref IS NULL, 0, 1)) As Count
FROM TIME_CTE
LEFT JOIN CTE ON CTE.CreationTime = TIME_CTE.TimePeriod
GROUP BY TIME_CTE.TimePeriod
ORDER BY TIME_CTE.TimePeriod

SQL query - Find daily MIN value from hourly sums

Let's cut to the chase. I have a table which looks like this one (using SQL Server 2014):
DEMO:
http://sqlfiddle.com/#!6/75f4a/1/0
CREATE TABLE TAB (
DT datetime,
VALUE float
);
INSERT INTO TAB VALUES
('2015-05-01 06:00:00', 12),
('2015-05-01 06:20:00', 10),
('2015-05-01 06:40:00', 11),
('2015-05-01 07:00:00', 14),
('2015-05-01 07:20:00', 15),
('2015-05-01 07:40:00', 13),
('2015-05-01 08:00:00', 10),
('2015-05-01 08:20:00', 9),
('2015-05-01 08:40:00', 5),
('2015-05-02 06:00:00', 19),
('2015-05-02 06:20:00', 7),
('2015-05-02 06:40:00', 11),
('2015-05-02 07:00:00', 9),
('2015-05-02 07:20:00', 7),
('2015-05-02 07:40:00', 6),
('2015-05-02 08:00:00', 10),
('2015-05-02 08:20:00', 19),
('2015-05-02 08:40:00', 15),
('2015-05-03 06:00:00', 8),
('2015-05-03 06:20:00', 8),
('2015-05-03 06:40:00', 8),
('2015-05-03 07:00:00', 21),
('2015-05-03 07:20:00', 12),
('2015-05-03 07:40:00', 7),
('2015-05-03 08:00:00', 10),
('2015-05-03 08:20:00', 4),
('2015-05-03 08:40:00', 10)
I need to:
sum values hourly
select the smallest 'hourly sum' for each day
select hour for which that sum occurred
In other words, I want to have a table which looks like this:
DATE | SUM VAL | ON HOUR
--------------------------
2015-03-01 | 24 | 8:00
2015-03-02 | 22 | 7:00
2015-03-03 | 24 | 6:00
First two points a very easy (check out sqlfiddle). I have a problem with the third one. I can't just like that select Datepart(HOUR, DT) bacause it has to be aggregated. I was trying to use JOINS and WHERE clause, but with no success (some values may occur in table more than once, which thrown an error).
I'm kinda new with SQL and I got stuck. Need your help SO! :)
One way is to use the set with minimum hourly values as a derived table and join against that. I would do something like this:
;WITH CTE AS (
SELECT Cast(Format(DT, 'yyyy-MM-dd HH:00') AS datetime) AS DT, SUM(VALUE) AS VAL
FROM TAB
GROUP BY Format(DT, 'yyyy-MM-dd HH:00')
)
SELECT b.dt "Date", val "sum val", cast(min(a.dt) as time) "on hour"
FROM cte a JOIN (
SELECT Format(DT,'yyyy-MM-dd') AS DT, MIN(VAL) AS DAILY_MIN
FROM cte HOURLY
GROUP BY Format(DT,'yyyy-MM-dd')
) b ON CAST(a.DT AS DATE) = b.DT and a.VAL = b.DAILY_MIN
GROUP BY b.DT, a.VAL
This would get:
Date sum val on hour
2015-05-01 24 08:00:00.0000000
2015-05-02 22 07:00:00.0000000
2015-05-03 24 06:00:00.0000000
I used min() for the time part as your sample data has the same low value for two separate hour for the 3rd. If you want both then remove the min function from the outer select and the group by. Then you would get:
Date sum val on hour
2015-05-01 24 08:00:00.0000000
2015-05-02 22 07:00:00.0000000
2015-05-03 24 06:00:00.0000000
2015-05-03 24 08:00:00.0000000
I'm sure it can be improved, but you should get the idea.
DECLARE #TAB TABLE
(
DT DATETIME ,
VALUE FLOAT
);
INSERT INTO #TAB
VALUES ( '2015-05-01 06:00:00', 12 ),
( '2015-05-01 06:20:00', 10 ),
( '2015-05-01 06:40:00', 11 ),
( '2015-05-01 07:00:00', 14 ),
( '2015-05-01 07:20:00', 15 ),
( '2015-05-01 07:40:00', 13 ),
( '2015-05-01 08:00:00', 10 ),
( '2015-05-01 08:20:00', 9 ),
( '2015-05-01 08:40:00', 5 ),
( '2015-05-02 06:00:00', 19 ),
( '2015-05-02 06:20:00', 7 ),
( '2015-05-02 06:40:00', 11 ),
( '2015-05-02 07:00:00', 9 ),
( '2015-05-02 07:20:00', 7 ),
( '2015-05-02 07:40:00', 6 ),
( '2015-05-02 08:00:00', 10 ),
( '2015-05-02 08:20:00', 19 ),
( '2015-05-02 08:40:00', 15 ),
( '2015-05-03 06:00:00', 8 ),
( '2015-05-03 06:20:00', 8 ),
( '2015-05-03 06:40:00', 8 ),
( '2015-05-03 07:00:00', 21 ),
( '2015-05-03 07:20:00', 12 ),
( '2015-05-03 07:40:00', 7 ),
( '2015-05-03 08:00:00', 10 ),
( '2015-05-03 08:20:00', 4 ),
( '2015-05-03 08:40:00', 10 );
WITH cteh
AS ( SELECT DT ,
CAST(dt AS DATE) AS D ,
SUM(VALUE) OVER ( PARTITION BY CAST(dt AS DATE),
DATEPART(hh, DT) ) AS S
FROM #TAB
),
ctef
AS ( SELECT * ,
ROW_NUMBER() OVER ( PARTITION BY D ORDER BY S ) AS rn
FROM cteh
)
SELECT D ,
S ,
CAST(DT AS TIME) AS H
FROM ctef
WHERE rn = 1
Output:
D S H
2015-05-01 24 08:00:00.0000000
2015-05-02 22 07:00:00.0000000
2015-05-03 24 06:00:00.0000000
Here's a method that uses a Temp Table (as opposed to the CTE's in the other solutions) to store calculated values and then filters the results to give you your desired output:
-- INSERT CALCULATED GROUPED VALUES INTO TEMP TABLE
SELECT CONVERT(DATE, DT) AS DateVal ,
SUM(VALUE) AS SumVal ,
DATEPART(HOUR, CONVERT(TIME, DT)) AS HourVal
INTO #TEMP_CALC
FROM TAB
GROUP BY CONVERT(DATE, DT) , DATEPART(HOUR, CONVERT(TIME, DT))
-- TAKE THE RELEVANT ROWS
SELECT t.DateVal ,
MIN(t.SumVal) AS SumVal ,
( SELECT TOP 1
HourVal
FROM #TEMP_CALC t2
WHERE t2.DateVal = t.DateVal
AND t2.SumVal = MIN(t.SumVal)
) AS MinHour
FROM #TEMP_CALC t
GROUP BY t.DateVal
ORDER BY DateVal
You can use DATEDIFF to get the time spans from any starting point in time (1990-1-1 in this sample) in hours and days. The use that spans to group and order, and finally use DATEADD with the same starting point to rebuild it:
WITH dates AS (
SELECT CAST(DT AS DATETIME) AS Date, -- cast the value to date
value FROM dbo.TAB AS T
),
ddh AS (SELECT
date,
DATEDIFF(DAY, '1990-1-1', date) AS daySpan, -- days span
DATEDIFF(HOUR, '1990-1-1', date) AS hourSpan, -- hours span
value
FROM dates
),
ddhv AS ( SELECT
daySpan,
hourSpan,
SUM(value) AS sumValues -- sum...
FROM ddh
group BY daySpan, hourSpan -- ...grouped by day & hour
),
ddhvr AS ( SELECT
daySpan,
hourSpan,
sumValues,
-- number rows by hourly sum of the value
ROW_NUMBER() OVER (PARTITION BY daySpan ORDER BY sumValues) AS row
FROM ddhv
)
SELECT
DATEADD(HOUR, hourSpan, '1990-1-1') AS DayHour, -- rebuild the date/hour
sumValues
FROM ddhvr
WHERE row = 1 -- take only the first occurrence for each day
This query has the advantage that you can change the periods, and the starting point easyly. For example you can make your days starts at 6:30 AM instead of at 00:00,so that the compared periods are 6:30 to 7:30, 7:30 to 8:30 and do on. And you can also change the grouping unit, for example, instead of 1 hour it could be half an hour, or 5 minutes or 2 hours. If you need to do do, please, see this SO answer. There you'll see how you can make the grouping by different periods, and get back the period staring point. It's just some simple maths.
I tested my against your fiddle:
with agg as (
select cast(dt as date) as dt, datepart(hh, dt) as hr, sum(VALUE) as sum_val
from TAB
group by cast(dt as date), datepart(hh, dt)
)
select
dt, min(sum_val) as "SUM VAL",
(
select cast(hr as varchar(2)) + ':00' from agg as agg2
where agg2.dt = agg.dt and not exists (
/* select earliest in case of ties */
select 1 from agg as agg3
where agg3.dt = agg2.dt and agg3.sum_val >= agg3.sum_val and agg3.hr > agg2.hr
)
) as "ON HOUR"
from agg
group by dt;

Calculating total time excluding overlapped time & breaks in SQLServer

From the list of start time and end times from a select query, I need to find out the total time excluding overlapping time and breaks.
StartTime EndTime
2014-10-01 10:30:00.000 2014-10-01 12:00:00.000 -- 90 mins
2014-10-01 10:40:00.000 2014-10-01 12:00:00.000 --0 since its overlapped with previous
2014-10-01 10:42:00.000 2014-10-01 12:20:00.000 -- 20 mins excluding overlapped time
2014-10-01 10:40:00.000 2014-10-01 13:00:00.000 -- 40 mins
2014-10-01 10:44:00.000 2014-10-01 12:21:00.000 -- 0 previous ones have already covered this time range
2014-10-13 15:50:00.000 2014-10-13 16:00:00.000 -- 10 mins
So the total should be 160 mins in this case.
I don't want to use so many loops to get through with this. Looking for some simple solution.
DECLARE #table TABLE (StartTime DateTime2, EndTime DateTime2)
INSERT INTO #table SELECT '2014-10-01 10:30:00.000', '2014-10-01 12:00:00.000'
INSERT INTO #table SELECT '2014-10-01 10:40:00.000', '2014-10-01 12:00:00.000'
INSERT INTO #table SELECT '2014-10-01 10:42:00.000', '2014-10-01 12:20:00.000'
INSERT INTO #table SELECT '2014-10-01 10:40:00.000', '2014-10-01 13:00:00.000'
INSERT INTO #table SELECT '2014-10-01 10:44:00.000', '2014-10-01 12:21:00.000'
INSERT INTO #table SELECT '2014-10-13 15:50:00.000', '2014-10-13 16:00:00.000'
;WITH addNR AS ( -- Add row numbers
SELECT StartTime, EndTime, ROW_NUMBER() OVER (ORDER BY StartTime, EndTime) AS RowID
FROM #table AS T
), createNewTable AS ( -- Recreate table according overlap time
SELECT StartTime, EndTime, RowID
FROM addNR
WHERE RowID = 1
UNION ALL
SELECT
CASE
WHEN a.StartTime <= AN.StartTime AND AN.StartTime <= a.EndTime THEN a.StartTime
ELSE AN.StartTime END AS StartTime,
CASE WHEN a.StartTime <= AN.EndTime AND AN.EndTime <= a.EndTime THEN a.EndTime
ELSE AN.EndTime END AS EndTime,
AN.RowID
FROM addNR AS AN
INNER JOIN createNewTable AS a
ON a.RowID + 1 = AN.RowID
), getMinutes AS ( -- Get difference in minutes
SELECT DATEDIFF(MINUTE,StartTime,MAX(EndTime)) AS diffMinutes
FROM createNewTable
GROUP BY StartTime
)
SELECT SUM(diffMinutes) AS Result
FROM getMinutes
And the result is 160
To get the result with the data you gave, I assume that the end time is not included (otherwise it would be 91 minutes for the first run). With that in mind, this will give you the result you want with no cursors or loops. If the times span multiple days, the logic will need to be adjusted.
--Create sample data
CREATE TABLE TimesToCheck
([StartTime] datetime, [EndTime] datetime)
;
INSERT INTO TimesToCheck
([StartTime], [EndTime])
VALUES
('2014-10-01 10:30:00', '2014-10-01 12:00:00'),
('2014-10-01 10:40:00', '2014-10-01 12:00:00'),
('2014-10-01 10:42:00', '2014-10-01 12:20:00'),
('2014-10-01 10:40:00', '2014-10-01 13:00:00'),
('2014-10-01 10:44:00', '2014-10-01 12:21:00'),
('2014-10-13 15:50:00', '2014-10-13 16:00:00')
;--Now the solution.
;WITH
E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), -- 1*10^1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), -- 1*10^2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), -- 1*10^4 or 10,000 rows
N AS (SELECT TOP (3600) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))-1 AS Number FROM E4),
TimeList AS (SELECT CAST(DATEADD(minute,n.number,0) as time) AS m FROM N),
--We really only need the Timelist table. If it is already created, we can start here.
ActiveTimes AS (SELECT DISTINCT t.m FROM TimeList T
INNER JOIN TimesToCheck C ON t.m BETWEEN CAST(c.StartTime as time) AND CAST(DATEADD(minute,-1,c.EndTime) as time))
SELECT COUNT(*) FROM ActiveTimes

SQL to get an daily average from month total

I have a table that lists month totals (targets)
person total month
----------- --------------------- -----------
1001 114.00 201005
1001 120.00 201006
1001 120.00 201007
1001 120.00 201008
.
1002 114.00 201005
1002 222.00 201006
1002 333.00 201007
1002 111.00 201008
.
.
but month is an integer(!)
I also have another table that has a list of working days (calendar)
tran_date day_type
----------------------- ---------------------------------
1999-05-01 00:00:00.000 WEEKEND
1999-05-02 00:00:00.000 WEEKEND
1999-05-03 00:00:00.000 WORKING_DAY
1999-05-04 00:00:00.000 WORKING_DAY
1999-06-01 00:00:00.000 .....
.
.
.
What I want to do is get a list of dates with the average for that day based on the number of days in the month where day_type is 'WORKING_DAY' / the month's total.
so if I had say 20 working days in 201005 then I'd get an average of 114/20 on each working day, while the other days would be 0.
somthing like
person tran_date day_avg
------- ----------------------- ---------------------------------
1001 2010-05-01 00:00:00.000 0
1001 2010-05-02 00:00:00.000 0
1001 2010-05-03 00:00:00.000 114/2 (as there are two working days)
1001 2010-05-04 00:00:00.000 114/2 (as there are two working days)
.
.
.
It has to be done as a CTE as this is a limitation of the target system (I can only do one statement)
I can start off with (Dates to
WITH
Dates AS
(
SELECT CAST('19990501' as datetime) TRAN_DATE
UNION ALL
SELECT TRAN_DATE + 1
FROM Dates
WHERE TRAN_DATE + 1 <= CAST('20120430' as datetime)
),
Targets as
(
select CAST(cast(month as nvarchar) + '01' as dateTime) mon_start,
DATEADD(MONTH, 1, CAST(cast(month as nvarchar) + '01' as dateTime)) mon_end,
total
from targets
)
select ????
Sample data (may vary):
select * into #totals from (
select '1001' as person, 114.00 as total, 199905 as month union
select '1001', 120.00, 199906 union
select '1001', 120.00, 199907 union
select '1001', 120.00, 199908
) t
select * into #calendar from (
select cast('19990501' as datetime) as tran_date, 'WEEKEND' as day_type union
select '19990502', 'WEEKEND' union
select '19990503', 'WORKING_DAY' union
select '19990504', 'WORKING_DAY' union
select '19990505', 'WORKING_DAY' union
select '19990601', 'WEEKEND' union
select '19990602', 'WORKING_DAY' union
select '19990603', 'WORKING_DAY' union
select '19990604', 'WORKING_DAY' union
select '19990605', 'WORKING_DAY' union
select '19990606', 'WORKING_DAY' union
select '19990701', 'WORKING_DAY' union
select '19990702', 'WEEKEND' union
select '19990703', 'WEEKEND' union
select '19990704', 'WORKING_DAY' union
select '19990801', 'WORKING_DAY' union
select '19990802', 'WORKING_DAY' union
select '19990803', 'WEEKEND' union
select '19990804', 'WEEKEND' union
select '19990805', 'WORKING_DAY' union
select '19990901', 'WORKING_DAY'
) t
Select statement, it returns 0 if the day is 'weekend' or not exists in calendar table. Please keep in mind that MAXRECURSION is a value between 0 and 32,767.
;with dates as (
select cast('19990501' as datetime) as tran_date
union all
select dateadd(dd, 1, tran_date)
from dates where dateadd(dd, 1, tran_date) <= cast('20010101' as datetime)
)
select t.person , d.tran_date, (case when wd.tran_date is not null then t.total / w_days else 0 end) as day_avg
from dates d
left join #totals t on
datepart(yy, d.tran_date) * 100 + datepart(mm, d.tran_date) = t.month
left join (
select datepart(yy, tran_date) * 100 + datepart(mm, tran_date) as month, count(*) as w_days
from #calendar
where day_type = 'WORKING_DAY'
group by datepart(yy, tran_date) * 100 + datepart(mm, tran_date)
) c on t.month = c.month
left join #calendar wd on d.tran_date = wd.tran_date and wd.day_type = 'WORKING_DAY'
where t.person is not null
option(maxrecursion 20000)
You could calculate the number of working days per month in a subquery. Only the subquery would have to use group by. For example:
select t.person
, wd.tran_date
, t.total / m.WorkingDays as day_avg
from #Targets t
join #WorkingDays wd
on t.month = convert(varchar(6), wd.tran_date, 112)
left join
(
select convert(varchar(6), tran_date, 112) as Month
, sum(case when day_type = 'WORKING_DAY' then 1 end) as WorkingDays
from #WorkingDays
group by
convert(varchar(6), tran_date, 112)
) as m
on m.Month = t.month
Working example at SE Data.
For the "magic number" 112 in convert, see the MSDN page.
If I understood your question correctly, the following query should do it:
SELECT
*,
ISNULL(
(
SELECT total
FROM targets
WHERE
MONTH(tran_date) = month - ROUND(month, -2)
AND c1.day_type = 'WORKING_DAY'
) /
(
SELECT COUNT(*)
FROM calendar c2
WHERE
MONTH(c1.tran_date) = MONTH(c2.tran_date)
AND c2.day_type = 'WORKING_DAY'
),
0
) day_avg
FROM
calendar c1
In plain English:
For each row in calendar,
get the total of the corresponding month if this row is a working day (otherwise get NULL),
get the number of working days in the same month
and divide them.
Finally, convert the NULL (of non-working days) into 0.