SQL query - Find daily MIN value from hourly sums - sql

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;

Related

Calculating averages by quarters

I have a table in presto with 2 columns: date and value.
I want to calculate the average of 2nd Quarter's values so the expected result should be:
15.
How can I do this in presto?
date value
2021-01-01 10
2021-01-30 20
2021-02-10 10
2021-04-01 20
2021-04-02 10
2021-07-10 20
You can divide month by 3 and group by the result:
-- sample data
WITH dataset (date, value) AS (
VALUES (date '2021-01-01' , 10),
(date '2021-01-30' , 20),
(date '2021-02-10' , 10),
(date '2021-04-01' , 20),
(date '2021-04-02' , 10),
(date '2021-07-10', 20)
)
--query
SELECT avg(value)
FROM dataset
WHERE month(date) / 3 = 1
GROUP BY month(date) / 3
Output:
_col0
15.0
Use quarter function:
with mytable as (
SELECT * FROM (
VALUES
(date '2021-01-01', 10),
(date '2021-01-30', 20),
(date '2021-02-10', 10),
(date '2021-04-01', 20),
(date '2021-04-02', 10),
(date '2021-07-10', 20)
) AS t (date, value)
)
select quarter(date) as qt, avg(value) as avg
from mytable
where quarter(date)=2
group by quarter(date)
Result:
qt avg
2 15.0

Getting a Monthly Date Range

How can you make a date range in a big query? A date range starts from 29th of the month and ends with 28th of the next month. It should be like this
Date | Starting Date | Ending Date
03-13-2020 | 02-29-2020 | 03-28-2021
06-30-2020 | 06-29-2020 | 07-28-2021
01-01-2021 | 12-29-2020 | 01-28-2021
11-11-2021 | 10-28-2021 | 11-29-2021
Actually, i make an article on it.
Check this out:
https://www.theaccountingtactics.com/2021/12/BigQueryBQ-DateProblems-DateSituations-that-are-Hard-to-Analyze-and-Takes-Time-ToCrack%20.html?m=1
Consider below approach
create temp function set_day(date date, day int64) as (
ifnull(
safe.date(extract(year from date), extract(month from date), day),
last_day(date)
)
);
select Date,
set_day(Starting_Date, 29) as Starting_Date,
set_day(Ending_Date, 28) as Ending_Date
from (
select *, if(extract(day from Date) < 29,
struct(date_sub(Date, interval 1 month) as Starting_Date, Date as Ending_Date),
struct(Date as Starting_Date, date_add(Date, interval 1 month) as Ending_Date)
).*
from your_table
)
if applied to sample data as in your question
with your_table as (
select date '2020-03-13' Date union all
select '2021-03-13' union all
select '2020-06-30' union all
select '2021-01-01' union all
select '2021-11-11'
)
output is
You can test whole stuff using below
create temp function set_day(date date, day int64) as (
ifnull(
safe.date(extract(year from date), extract(month from date), day),
last_day(date)
)
);
with your_table as (
select date '2020-03-13' Date union all
select '2021-03-13' union all
select '2020-06-30' union all
select '2021-01-01' union all
select '2021-11-11'
)
select Date,
set_day(Starting_Date, 29) as Starting_Date,
set_day(Ending_Date, 28) as Ending_Date
from (
select *, if(extract(day from Date) < 29,
struct(date_sub(Date, interval 1 month) as Starting_Date, Date as Ending_Date),
struct(Date as Starting_Date, date_add(Date, interval 1 month) as Ending_Date)
).*
from your_table
)

Group by on range of dates

I've read some topics about group by sequence but I could not figure out an solution for my problem.
I have a table (the name is ViewHistory) like this.
Tme Value
2020-07-22 09:30:00 1
2020-07-22 09:31:00 2
2020-07-22 09:32:00 3
2020-07-22 09:33:00 4
2020-07-22 09:34:00 5
2020-07-22 09:35:00 6
.
.
.
The data can grow indefinitely.
In this table, there are many records with 1 min TimeFrame.
I want to group on range of dataTime with timeFrame 2 min and Sum(value).
like this output:
TimeFrame SumData
09:30 1
09:32 5 -- sum of range 09:31_09:32
09:34 9 -- sum of range 09:33_09:34
.
.
.
How can I do this automatically, instead of using a:
WHERE Tme BETWEEN ('2020-07-22 09:31:00' AND '2020-07-22 09:32:00') and etc.
I am sure there is a simpler way, but its not coming to me right now.
declare #Test table (tme datetime2, [value] int)
insert into #Test (tme, [value])
values
('2020-07-22 09:30:00', 1),
('2020-07-22 09:31:00', 2),
('2020-07-22 09:32:00', 3),
('2020-07-22 09:33:00', 4),
('2020-07-22 09:34:00', 5),
('2020-07-22 09:35:00', 6);
with cte as (
select convert(date, tme) [date], datepart(hour, tme) [hour], datepart(minute,dateadd(minute, 1,tme)) / 2 [minute], sum([value]) [value]
from #Test
group by convert(date, tme), datepart(hour, tme), datepart(minute,dateadd(minute, 1,tme)) / 2
)
select convert(varchar(2),[hour]) + ':' + convert(varchar(2), [minute] * 2) [time], [value]
-- , dateadd(minute, [minute] * 2, dateadd(hour, [hour], convert(datetime2, [date]))) -- Entire date if desired
from cte;
Which gives:
time
value
9:30
1
9:32
5
9:34
9
9:36
6

Calculate total time worked in a day with multiple stops and starts

I can use DATEDIFF to find the difference between one set of dates like this
DATEDIFF(MINUTE, #startdate, #enddate)
but how would I find the total time span between multiple sets of dates? I don't know how many sets (stops and starts) I will have.
The data is on multiple rows with start and stops.
ID TimeStamp StartOrStop TimeCode
----------------------------------------------------------------
1 2017-01-01 07:00:00 Start 1
2 2017-01-01 08:15:00 Stop 2
3 2017-01-01 10:00:00 Start 1
4 2017-01-01 11:00:00 Stop 2
5 2017-01-01 10:30:00 Start 1
6 2017-01-01 12:00:00 Stop 2
This code would work assuming that your table only store data from one person, and they should be of the order Start/Stop/Start/Stop
WITH StartTime AS (
SELECT
TimeStamp
, ROW_NUMBER() PARTITION BY (ORDER BY TimeStamp) RowNum
FROM
<<table>>
WHERE
TimeCode = 1
), StopTime AS (
SELECT
TimeStamp
, ROW_NUMBER() PARTITION BY (ORDER BY TimeStamp) RowNum
FROM
<<table>>
WHERE
TimeCode = 2
)
SELECT
SUM (DATEDIFF( MINUTE, StartTime.TimeStamp, StopTime.TimeStamp )) As TotalTime
FROM
StartTime
JOIN StopTime ON StartTime.RowNum = StopTime.RowNum
This will work if your starts and stops are reliable. Your sample has two starts in order - 10:00 and 10:30 starts. I assume in production you will have an employee id to group on, so I added this to the sample data in place of the identity column.
Also in production, the CTE sets will be reduced by using a parameter on date. If there are overnight shifts, you would want your stops CTE to use dateadd(day, 1, #startDate) as your upper bound when retrieving end date.
Set up sample:
declare #temp table (
EmpId int,
TimeStamp datetime,
StartOrStop varchar(55),
TimeCode int
);
insert into #temp
values
(1, '2017-01-01 07:00:00', 'Start', 1),
(1, '2017-01-01 08:15:00', 'Stop', 2),
(1, '2017-01-01 10:00:00', 'Start', 1),
(1, '2017-01-01 11:00:00', 'Stop', 2),
(2, '2017-01-01 10:30:00', 'Start', 1),
(2, '2017-01-01 12:00:00', 'Stop', 2)
Query:
;with starts as (
select t.EmpId,
t.TimeStamp as StartTime,
row_number() over (partition by t.EmpId order by t.TimeStamp asc) as rn
from #temp t
where Timecode = 1 --Start time code?
),
stops as (
select t.EmpId,
t.TimeStamp as EndTime,
row_number() over (partition by t.EmpId order by t.TimeStamp asc) as rn
from #temp t
where Timecode = 2 --Stop time code?
)
select cast(min(sub.StartTime) as date) as WorkDay,
sub.EmpId as Employee,
min(sub.StartTime) as ClockIn,
min(sub.EndTime) as ClockOut,
sum(sub.MinutesWorked) as MinutesWorked
from
(
select strt.EmpId,
strt.StartTime,
stp.EndTime,
datediff(minute, strt.StartTime, stp.EndTime) as MinutesWorked
from starts strt
inner join stops stp
on strt.EmpId = stp.EmpId
and strt.rn = stp.rn
)sub
group by sub.EmpId
This works assuming your table has an incremental ID and interleaving start/stop records
--Data sample as provided
declare #temp table (
Id int,
TimeStamp datetime,
StartOrStop varchar(55),
TimeCode int
);
insert into #temp
values
(1, '2017-01-01 07:00:00', 'Start', 1),
(2, '2017-01-01 08:15:00', 'Stop', 2),
(3, '2017-01-01 10:00:00', 'Start', 1),
(4, '2017-01-01 11:00:00', 'Stop', 2),
(5, '2017-01-01 10:30:00', 'Start', 1),
(6, '2017-01-01 12:00:00', 'Stop', 2)
--let's see every pair start/stop and discard stop/start
select start.timestamp start, stop.timestamp stop,
datediff(mi,start.timestamp,stop.timestamp) minutes
from #temp start inner join #temp stop
on start.id+1= stop.id and start.timecode=1
--Sum all for required result
select sum(datediff(mi,start.timestamp,stop.timestamp) ) totalMinutes
from #temp start inner join #temp stop
on start.id+1= stop.id and start.timecode=1
Results
+-------------------------+-------------------------+---------+
| start | stop | minutes |
+-------------------------+-------------------------+---------+
| 2017-01-01 07:00:00.000 | 2017-01-01 08:15:00.000 | 75 |
| 2017-01-01 10:00:00.000 | 2017-01-01 11:00:00.000 | 60 |
| 2017-01-01 10:30:00.000 | 2017-01-01 12:00:00.000 | 90 |
+-------------------------+-------------------------+---------+
+--------------+
| totalMinutes |
+--------------+
| 225 |
+--------------+
Maybe the tricky part is the join clause. We need to join #table with itself by deferring 1 ID. Here is where on start.id+1= stop.id did its work.
In the other hand, for excluding stop/start couple we use start.timecode=1. In case we don't have a column with this information, something like stop.id%2=0 works just fine.

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