Date wise hourly (on 24 hour) coustomer count - sql
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.
Related
SQL Select only missing months
Notice the 2017-04-01, 2018-02-01, 2018-07-01, and 2019-01-01 months are missing in the output. I want to show only those months which are missing. Does anyone know how to go about this? Query: SELECT TO_DATE("Month", 'mon''yy') as dates FROM sample_sheet group by dates order by dates asc; Output: 2017-01-01 2017-02-01 2017-03-01 2017-05-01 2017-06-01 2017-07-01 2017-08-01 2017-09-01 2017-10-01 2017-11-01 2017-12-01 2018-01-01 2018-03-01 2018-04-01 2018-05-01 2018-06-01 2018-08-01 2018-09-01 2018-10-01 2018-11-01 2018-12-01 2019-02-01 2019-03-01 2019-04-01
I don't know Vertica, so I wrote a working proof of concept in Microsoft SQL Server and tried to convert it to Vertica syntax based on the online documentation. It should look like this: with months as ( select 2017 as date_year, 1 as date_month, to_date('2017-01-01', 'YYYY-MM-DD') as first_date, to_date('2017-01-31', 'yyyy-mm-dd') as last_date union all select year(add_months(first_date, 1)) as date_year, month(add_months(first_date, 1)) as date_month, add_months(first_date, 1) as first_date, last_day(add_months(first_date, 1)) as last_date from months where first_date < current_date ), sample_dates (a_date) as ( select to_date('2017-01-15', 'YYYY-MM-DD') union all select to_date('2017-01-22', 'YYYY-MM-DD') union all select to_date('2017-02-01', 'YYYY-MM-DD') union all select to_date('2017-04-15', 'YYYY-MM-DD') union all select to_date('2017-06-15', 'YYYY-MM-DD') ) select * from sample_dates right join months on sample_dates.a_date between first_date and last_date where sample_dates.a_date is null Months is a recursive dynamic table that holds all months since 2017-01, with first and last day of the month. sample_dates is just a list of dates to test the logic - you should replace it with your own table. Once you build that monthly calendar table all you need to do is check your dates against it using an outer query to see what dates are not between any of those periods between first_date and last_date columns.
You can build a TIMESERIES of all dates between the first input date and the last input date (The highest granularity of a TIMESERIES is the day.), and filter out only the months' first days out of that; then left join that created sequence of firsts of month with your input to find out where the join would fail, checking for NULLS from the input branch of the join: WITH -- your input input(mth1st) AS ( SELECT DATE '2017-01-01' UNION ALL SELECT DATE '2017-02-01' UNION ALL SELECT DATE '2017-03-01' UNION ALL SELECT DATE '2017-05-01' UNION ALL SELECT DATE '2017-06-01' UNION ALL SELECT DATE '2017-07-01' UNION ALL SELECT DATE '2017-08-01' UNION ALL SELECT DATE '2017-09-01' UNION ALL SELECT DATE '2017-10-01' UNION ALL SELECT DATE '2017-11-01' UNION ALL SELECT DATE '2017-12-01' UNION ALL SELECT DATE '2018-01-01' UNION ALL SELECT DATE '2018-03-01' UNION ALL SELECT DATE '2018-04-01' UNION ALL SELECT DATE '2018-05-01' UNION ALL SELECT DATE '2018-06-01' UNION ALL SELECT DATE '2018-08-01' UNION ALL SELECT DATE '2018-09-01' UNION ALL SELECT DATE '2018-10-01' UNION ALL SELECT DATE '2018-11-01' UNION ALL SELECT DATE '2018-12-01' UNION ALL SELECT DATE '2019-02-01' UNION ALL SELECT DATE '2019-03-01' UNION ALL SELECT DATE '2019-04-01' ) , -- need a series of month's firsts -- TIMESERIES works for INTERVAL DAY TO SECOND -- so build that timeseries, and filter out -- the month's firsts limits(mth1st) AS ( SELECT MIN(mth1st) FROM input UNION ALL SELECT MAX(mth1st) FROM input ) , alldates AS ( SELECT dt::DATE FROM limits TIMESERIES dt AS '1 day' OVER(ORDER BY mth1st::TIMESTAMP) ) , allfirsts(mth1st) AS ( SELECT dt FROM alldates WHERE DAY(dt)=1 ) SELECT allfirsts.mth1st FROM allfirsts LEFT JOIN input USING(mth1st) WHERE input.mth1st IS NULL; -- out mth1st -- out ------------ -- out 2017-04-01 -- out 2018-02-01 -- out 2018-07-01 -- out 2019-01-01
Pull out most non overlapping date range
Sorry, going to start over and try to explain from the start: I have a small list of dates: date mark 08-16-2016 1 08-17-2016 1 01-03-2017 1 02-16-2018 1 02-17-2018 1 From here I need to find out in a 3 year period if there is 2 continuous years where there are less than 3 marks. I'm looking over a date range from 2016-08-01 to 2019-08-01. So I setup the following query: with initData as( select date('2016-08-16') stamp, 1 mark from sysibm.sysdummy1 union select date('2016-08-17') stamp, 1 mark from sysibm.sysdummy1 union select date('2017-01-03') stamp, 1 mark from sysibm.sysdummy1 union select date('2018-02-16') stamp, 1 mark from sysibm.sysdummy1 union select date('2018-02-17') stamp, 1 mark from sysibm.sysdummy1 ) select * from( select a.startDate, a.endDate, coalesce(sum(b.mark),0) as mark from( select startDate, endDate from( select stamp startDate, stamp+1 YEAR endDate from( select stamp + ym YEAR stamp from( select date('2016-08-01') stamp from sysibm.sysdummy1 union select stamp from initData union select stamp+1 DAY from initData ), ( select 0 as ym from sysibm.sysdummy1 union select 1 as ym from sysibm.sysdummy1 union select 2 as ym from sysibm.sysdummy1 ) ) ) where endDate <= date('2019-08-01') ) a left outer join( select stamp, mark from initData ) b on b.stamp >= a.startDate and b.stamp < a.endDate group by a.startDate, a.endDate ) where mark < 3 order by startDate, endDate This gives me my list of ranges that I'm looking which have less than 3 marks. Now I need to find full years that don't over lap with other dates. 2016-08-17 2017-08-17 2 2016-08-18 2017-08-18 1 2017-01-03 2018-01-03 1 2017-01-04 2018-01-04 0 2017-08-01 2018-08-01 2 2017-08-16 2018-08-16 2 2017-08-17 2018-08-17 2 2017-08-18 2018-08-18 2 2018-01-03 2019-01-03 2 2018-01-04 2019-01-04 2 2018-02-16 2019-02-16 2 2018-02-17 2019-02-17 1 2018-02-18 2019-02-18 0 2018-08-01 2019-08-01 0 I have finally came up with some solution, but it seems a bit slow and seems like there should be a better way to do it: with initData as( select date('2016-08-16') stamp, 1 mark from sysibm.sysdummy1 union select date('2016-08-17') stamp, 1 mark from sysibm.sysdummy1 union select date('2017-01-03') stamp, 1 mark from sysibm.sysdummy1 union select date('2018-02-16') stamp, 1 mark from sysibm.sysdummy1 union select date('2018-02-17') stamp, 1 mark from sysibm.sysdummy1 ), dateRanges as( select startDate, endDate, mark, row_number() over (order by startDate, endDate) rn from( select a.startDate, a.endDate, coalesce(sum(b.mark),0) as mark from( select startDate, endDate from( select stamp startDate, stamp+1 YEAR endDate from( select stamp + ym YEAR stamp from( select date('2016-08-01') stamp from sysibm.sysdummy1 union select stamp from initData union select stamp+1 DAY from initData ), ( select 0 as ym from sysibm.sysdummy1 union select 1 as ym from sysibm.sysdummy1 union select 2 as ym from sysibm.sysdummy1 ) ) ) where endDate <= date('2019-08-01') ) a left outer join( select stamp, mark from initData ) b on b.stamp >= a.startDate and b.stamp < a.endDate group by a.startDate, a.endDate ) where mark < 3 ), dateRangeLimit1 as( select a.startDate, a.endDate, a.mark, row_number() over (order by a.startDate, a.endDate) rn from dateRanges a left outer join dateRanges b on a.startDate < b.endDate and b.rn = 1 and a.rn != b.rn where b.rn is null ) select a.* from dateRangeLimit1 a left outer join dateRangeLimit1 b on a.startDate < b.endDate and b.rn = 2 and a.rn <> b.rn and a.rn != 1 where b.rn is null This gives me back my expected date ranges that don't over lap with each other: 2016-08-17 2017-08-17 2 1 2017-08-17 2018-08-17 2 2 I hope this makes a bit more sense.
I'm not sure your data is quite right, but nonetheless does this help? WITH D(F,T) AS (VALUES ('2016-08-09','2017-08-09') ,('2016-08-16','2017-08-16') ,('2016-08-17','2017-08-17') ,('2016-08-18','2017-08-18') ,('2017-08-09','2018-08-09') ,('2017-08-16','2018-08-16') ,('2017-08-17','2018-08-17') ,('2017-08-18','2018-08-18') ,('2018-02-16','2019-02-16') ,('2018-02-17','2019-02-17') ,('2018-02-18','2019-02-18') ,('2018-08-09','2019-08-09') ) SELECT F,T FROM ( SELECT F,T , LEAD(F,1) OVER(ORDER BY F ASC) AS NEXT_F , LAG( T,1) OVER(ORDER BY F ASC) AS PREV_T FROM D ) WHERE T >= NEXT_F OR F <= PREV_T
from dual apparently points to ORACLE. Find the longest path of non-overlapping (end = start considered non-overlapping) intervals select level, sys_connect_by_path (startDate || ' .. ' || endDate, '/') path from blah a connect by (prior startDate < startDate) and not(prior startDate < endDate and startDate < prior endDate) order by level desc -- fetch is 12c+ feature fetch next 1 rows only; Using sample data returns 3 /09-AUG-16 .. 09-AUG-17/09-AUG-17 .. 09-AUG-18/09-AUG-18 .. 09-AUG-19 Fiddle
Cross join for time series postgresql query
I have a table with Items with Item_id, Item_time, Item_numbers 1 2017-01-01 18:00:00 2 2 2017-01-01 18:10:00 2 3 2017-01-01 19:10:00 3 I want to group the items by hourly for some specific time (between 9 to 3 for each day) and in case if there is no entry for the particular hours then it should it be a 0. Desired Output: Item_time Item_numbers 2017-01-01 18:00:00 4 2017-01-01 19:00:00 3 2017-01-01 20:00:00 0 with hour_items as (select date_trunc('hour', item_time) "hour", avg(item_numbers) as value from items where item_id=2 and fact_time::date= '2017-01-01' group by hour) select hour, value from hour_items where EXTRACT(HOUR FROM hour) >= '9' and EXTRACT(HOUR FROM > hour) < '15'. The above query groups them correctly but the where the hour is missing, there is no entry. Though it should be an entry with a 0 as stated in the desired output.
This should do. We get all the distinct days (CTE dates), then we generate hours for each of those dates (CTE hours) and finally we left join our data on "per our" basis. with sample_data as ( select 1 as item_id, '2018-01-01 12:03:15'::timestamp as item_time, 2 as item_numbers union all select 2 as item_id, '2018-01-01 12:41:15'::timestamp as item_time, 1 as item_numbers union all select 3 as item_id, '2018-01-01 17:41:15'::timestamp as item_time, 2 as item_numbers union all select 4 as item_id, '2018-01-01 19:41:15'::timestamp as item_time, 2 as item_numbers ), dates as ( select distinct item_time::date from sample_data ), hours as ( select item_time + interval '1 hour' * a as hour from dates cross join generate_series(0,23) a ) select h.hour, sum(coalesce(sd.item_numbers,0)) from hours h left join sample_data sd on h.hour = date_trunc('hour', sd.item_time) where extract(hour from hour) between 9 and 17 group by h.hour order by h.hour
SQL TSQL for Workers per Hour
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
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.