How to assign shift based on punch time - sql

Based on punch time shift automatically assigned to employee
Table Trnevents:
emp_reader_id EVENTID DT
3 1 2019-07-14 17:00:00.000
3 0 2019-07-14 10:00:00.000
3 1 2019-07-13 17:50:00.000
3 0 2019-07-13 10:05:00.000
3 1 2019-07-12 16:00:00.000
3 0 2019-07-12 08:55:00.000
declare
#start_date date='2019-07-12'
,#end_date date ='2019-07-14'
;WITH ByDays AS
( -- Number the entry register in each day
SELECT
emp_reader_id,
dt AS T,
CONVERT(VARCHAR(10),dt,102) AS Day,
FLOOR(CONVERT(FLOAT,dt)) DayNumber,
ROW_NUMBER() OVER(PARTITION BY FLOOR(CONVERT(FLOAT,dt)) ORDER BY dt) InDay
FROM trnevents
where
(
CONVERT(VARCHAR(26), dt, 23) >= CONVERT(VARCHAR(26), #start_date, 23)
and CONVERT(VARCHAR(26), dt, 23) <=CONVERT(VARCHAR(26), #end_date, 23)
)
)
,Diffs AS
(
SELECT
E.Day,
E.emp_Reader_id,
E.T ET,
O.T OT,
O.T-E.T Diff,
DATEDIFF(S,E.T,O.T) DiffSeconds -- difference in seconds
FROM
(
SELECT
BE.emp_Reader_id,
BE.T,
BE.Day,
BE.InDay
FROM ByDays BE
WHERE BE.InDay % 2 = 1
) E -- Even rows
INNER JOIN
(
SELECT
BO.emp_reader_id,
BO.T,
BO.Day,
BO.InDay
FROM ByDays BO
WHERE BO.InDay % 2 = 0
) O -- Odd rows
ON E.InDay + 1 = O.InDay -- Join rows (1,2), (3,4) and so on
AND E.Day = O.Day -- in the same day
)
SELECT * FROM Diffs
DECLARE #start TIME(0) = '9:00 AM', #end TIME(0) = '18:00 PM';
WITH x(n) AS
(
SELECT TOP (DATEDIFF(HOUR, #start, #end) + 1)
rn = ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_columns
ORDER BY [object_id]
)
SELECT
t = DATEADD(HOUR, n-1, #start)
,cast(DATEADD(HOUR, n-1, #start) as varchar(50))+' shift'
FROM x
ORDER BY t;
If employee punch in time between 8.30 to 9.30 am , it assigned to 9.00 shift
if 9.30 to 10.30. it assigned to 10.00 shift
Expected output:
Day emp_Reader_id ET OT Diff DiffSeconds Shift
2019.07.12 3 2019-07-12 08:55:00.000 2019-07-12 16:00:00.000 1900-01-01 07:05:00.000 25500 09:00:00 shift
2019.07.13 3 2019-07-13 10:05:00.000 2019-07-13 17:50:00.000 1900-01-01 07:45:00.000 27900 10:00:00 shift
2019.07.14 3 2019-07-14 12:00:00.000 2019-07-14 21:00:00.000 1900-01-01 07:00:00.000 25200 12:00:00 shift

Two solutions, one with LEAD.
First is without LEAD:
select
CAST(t1.DT as date) AS "Day",
t1.emp_reader_id AS emp_Reader_id,
t1.DT AS ET,
t2.DT AS OT,
t1.DT - t2.DT As Diff,
DATEDIFF(s, t1.DT, t2.DT) As DiffSeconds,
cast(dateadd(HOUR,datepart(HH,t1.DT)+ round(datepart(MINUTE,t1.dt)/60.0,0),0) as time) as Shift
from trnevents t1
inner join trnevents t2 on t2.emp_reader_id=t1.emp_reader_id and t2.EVENTID=1 and CAST(t2.DT as date)= CAST(t1.DT as date)
where t1.eventID=0
order by t1.DT
or:
SELECT
Day,
emp_reader_id,
ET,
OT,
ET-OT AS Diff ,
DATEDIFF(s,ET,OT) as DiffSeconds,
cast(dateadd(HOUR,datepart(HH,ET)+ round(datepart(MINUTE,ET)/60.0,0),0) as time) as Shift
FROM (
select
CAST(t1.DT as date) AS "Day",
t1.emp_reader_id AS emp_Reader_id,
t1.DT AS ET,
LEAD(t1.DT) over (order by emp_reader_id,dt) AS OT,
eventid,
--t1.DT - t2.DT As Diff,
--DATEDIFF(s, t1.DT, t2.DT) As DiffSeconds,
cast(dateadd(HOUR,datepart(HH,t1.DT)+ round(datepart(MINUTE,t1.dt)/60.0,0),0) as time) as Shift
from trnevents t1) x
where x.EVENTID=0
Both query produce same result (second one is probably quicker)

If employee punch in time between 8.30 to 9.30 am , it assigned to 9.00 shift if 9.30 to 10.30. it assigned to 10.00 shift
If I understand this correctly, you can use a case expression:
select e.*,
(case when dt >= '08:30:00' and dt < '09:30:00'
then 'Shift 09:00'
when dt >= '09:30:00' and dt < '10:30:00'
then 'Shift 10:00'
end) as shift
from Trnevents e
If you want a more general solution where the breaks are at 30 minute intervals throughout the day, then subtract 30 minutes and extract the hour:
select e.*,
datepart(hour, dateadd(minute, -30, dt)) as shift
from e;

Related

Keep last n business days records from today date in SQL Server

How can we keep last n business days records from today date in this table:
Suppose n = 7
Sample Data:
Table1:
Date
----------
2021-11-29
2021-11-30
2021-12-01
2021-12-02
2021-12-03
2021-12-04
2021-12-05
2021-12-06
2021-12-07
2021-12-08
2021-12-09
2021-12-10
2021-12-11
2021-12-12
2021-12-13
Based on this table data we want output like below. It should delete all the rows before the 03-Dec or data for last 7 business days.
Date
-------
2021-12-03
2021-12-06
2021-12-07
2021-12-08
2021-12-09
2021-12-10
2021-12-13
Note: It's fine if we keep data for Saturday, Sunday in between business days.
I tried this query
DECLARE #n INT = 7
SELECT * FROM Table1
WHERE [date] < Dateadd(day, -((#n + (#n / 5) * 2)), Getdate())
but Saturday, Sunday logic doesn't fit here with my logic. Please suggest better approach.
You can get the 7th working day from today as
select top(1) cast(dateadd(d, -n + 1, getdate()) as date) d
from (
select n
, sum (case when datename(dw, dateadd(d, -n + 1, getdate())) not in ('Sunday', 'Saturday') then 1 end) over(order by n) wdn
from (
values (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11)
)t0(n)
) t
where wdn = 7
order by n;
Generally using on-the-fly tally for a #n -th day
declare #n int = 24;
with t0(n) as (
select n
from (
values (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)
) t(n)
), tally as (
select top(#n + (#n/5 +1)*2) row_number() over(order by t1.n) n
from t0 t1, t0 t2, t0 t3
)
select top(1) cast(dateadd(d, -n + 1, getdate()) as date) d
from (
select n
, sum (case when datename(dw, dateadd(d, -n + 1, getdate())) not in ('Sunday', 'Saturday') then 1 end) over(order by n) wdn
from tally
) t
where wdn = #n
order by n;
You can use CTE to mark target dates and then delete all the others from the table as follows:
; With CTE As (
Select [Date], Row_number() Over (Order by [Date] Desc) As Num
From tbl
Where DATEPART(weekday, [Date]) Not In (6,7)
)
Delete From tbl
Where [Date] Not In (Select [Date] From CTE Where Num<=7)
If the number of business days in the table may be less than 7 and you need to bring the total number of days to 7 by adding days off, try this:
Declare #n Int = 7
; With CTE As (
Select [Date], IIF(DATEPART(weekday, [Date]) In (6,7), 0, 1) As IsBusinessDay
From tbl
)
Delete From tbl
Where [Date] Not In (Select Top(#n) [Date] From CTE Order By IsBusinessDay Desc, [Date] Desc)
If there is only one date for each day, you can simply do this:
SELECT TOP 7 [Date] FROM Table1
WHERE
[Date] < GETDATE() AND DATENAME(weekday, [DATE]) NOT IN ('Saturday', 'Sunday')
ORDER BY
[DATE] DESC

How can I get the count of inserts per minute in SQL

I have a table that looks like this
id
name
CreatedDate
1
test1
2014-06-30 09:00:00
1
test2
2014-06-30 09:01:10
1
test3
2014-06-30 09:01:23
1
test4
2014-06-30 09:01:43
1
test5
2014-06-30 09:02:02
1
test6
2014-06-30 09:02:34
1
test7
2014-06-30 09:03:22
1
test8
2014-06-30 09:03:28
1
test9
2014-06-30 09:04:14
1
test10
2014-06-30 09:04:22
1
test11
2014-06-30 09:04:28
I want to get the number of inserts that have happened per minute so the output looks like this
Inserts Per Min
Start Time
End Time
1
09:00:00
09:00:00
3
09:01:10
09:01:43
2
09:02:02
09:00:34
2
09:03:22
09:03:28
3
09:04:14
09:04:28
How can I do that?
This is the code that I have that gives me the Inserts per day but I can't get this to work per minute
Select Count(CreatedDate) as InsertsPerDay, Convert(varchar, CreatedDate, 101) as CreatedDate
From MyTable
Where DATEDIFF(day, CreatedDate, GETDATE())) < 30
Group By Convert(varchar, CreatedDate, 101)
Order By InsertsPerDay DESC
use subqueries and lag
declare #tmp as table(id int, name varchar(20), CreatedDate datetime)
insert into #tmp values(
1,'test1','2014-06-30 09:00:00')
,(1,'test2','2014-06-30 09:01:10')
,(1,'test3','2014-06-30 09:01:23')
,(1,'test4','2014-06-30 09:01:43')
,(1,'test5','2014-06-30 09:02:02')
,(1,'test6','2014-06-30 09:02:34')
,(1,'test7','2014-06-30 09:03:22')
,(1,'test8','2014-06-30 09:03:28')
,(1,'test9','2014-06-30 09:04:14')
,(1,'test1','2014-06-30 09:04:22')
,(1,'test11','2014-06-30 09:04:28')
select
IsNull(sum(case when Seconds between 0 and 60 then 1 end),0) Minute_One,
IsNull(sum(case when Seconds between 61 and 60*2 then 1 end),0) Minute_Two,
IsNull(sum(case when Seconds > 60*2 then 1 end),0) Minute_Others
from
(
select
(DATEPART(HOUR, DiffCreatedDate) * 3600) +
(DATEPART(MINUTE, DiffCreatedDate) * 60) +
(DATEPART(SECOND, DiffCreatedDate)) Seconds
from
(
select
CreatedDate-PriorCreatedDate DiffCreatedDate
from
(
select
CreatedDate,
lag(CreatedDate,1) over(order by CreatedDate) PriorCreatedDate
from #tmp
)x
)y
)z
--order by Seconds
DECLARE #Mytimes TABLE
(
id INT IDENTITY NOT NULL PRIMARY KEY,
name VARCHAR(10),
CreatedDate DATETIME
);
INSERT INTO #Mytimes
(
[name],
CreatedDate
)
VALUES
('test1', '2014-06-30 09:00:00'),
('test2', '2014-06-30 09:01:10'),
('test3', '2014-06-30 09:01:23'),
('test4', '2014-06-30 09:01:43'),
('test5', '2014-06-30 09:02:02'),
('test6', '2014-06-30 09:02:34'),
('test7', '2014-06-30 09:03:22'),
('test8', '2014-06-30 09:03:28'),
('test9', '2014-06-30 09:04:14'),
('test10', '2014-06-30 09:04:22'),
('test11', '2014-06-30 09:04:28');
WITH TALLY
AS (SELECT TOP (1440)
ROW_NUMBER() OVER (ORDER BY t1.object_id) AS N
FROM sys.all_columns t1
CROSS JOIN sys.all_columns t2),
ranges
AS (SELECT CAST(DATEADD(MINUTE, N - 1, '00:00') AS TIME(0)) AS [from],
CAST(DATEADD(MINUTE, N, '00:00') AS TIME(0)) AS [to]
FROM TALLY),
myTimes
AS (SELECT CAST(CreatedDate AS TIME(0)) ct
FROM #Mytimes)
--SELECT r.[from],
-- r.[to],
SELECT MIN(t.ct) [from],
MAX(t.ct) [to],
COUNT(t.ct)
FROM ranges r
-- If you want all minutes regardless there is inserts
--LEFT JOIN myTimes t
INNER JOIN myTimes t
ON t.ct >= r.[from]
AND t.ct < r.[to]
GROUP BY r.[from],
r.[to]
ORDER BY r.[from];
Note: In case of left join, you would need to edit the select to use coalesce for min(),max() times. ie:
...
SELECT MIN(COALESCE(t.ct, r.[from])) [from],
MAX(COALESCE(t.ct, r.[to])) [to],
COUNT(t.ct)
FROM ranges r
LEFT JOIN myTimes t
ON t.ct >= r.[from]
AND t.ct < r.[to]
GROUP BY r.[from],
r.[to]
ORDER BY r.[from];
This might work on 2008. (but can't verify)
Select
Count(CreatedDate) As [Inserts Per Min]
, Min(Cast(CreatedDate As Time(0))) As [Start Time]
, Max(Cast(CreatedDate As Time(0))) As [End Time]
From MyTable
--Where CreatedDate > DateAdd(month, -1, GetDate())
Group By Convert(SmallDateTime, Convert(Char(16), CreatedDate, 120))
Order By [Inserts Per Min] Desc;
Inserts Per Min
Start Time
End Time
3
09:01:10
09:01:43
3
09:04:14
09:04:28
2
09:02:02
09:02:34
2
09:03:22
09:03:28
1
09:00:00
09:00:00
Demo on db<>fiddle here

Splitting dates into intervals using Start Date and End Date

I have scenario where I need to split the given date range into monthly intervals.
For example, the input is like below:
StartDate EndDate
2018-01-21 2018-01-29
2018-01-30 2018-02-23
2018-02-24 2018-03-31
2018-04-01 2018-08-16
2018-08-17 2018-12-31
And the expected output should be like below:
StartDate EndDate
2018-01-21 2018-01-29
2018-01-30 2018-01-31
2018-02-01 2018-02-23
2018-02-24 2018-02-28
2018-03-01 2018-03-31
2018-04-01 2018-04-30
2018-05-01 2018-05-31
2018-06-01 2018-06-30
2018-07-01 2018-07-31
2018-08-01 2018-08-16
2018-08-17 2018-08-31
2018-09-01 2018-09-30
2018-10-01 2018-10-31
2018-11-01 2018-11-30
2018-12-01 2018-12-31
Below is the sample data.
CREATE TABLE #Dates
(
StartDate DATE,
EndDate DATE
);
INSERT INTO #Dates
(
StartDate,
EndDate
)
VALUES
('2018-01-21', '2018-01-29'),
('2018-01-30', '2018-02-23'),
('2018-02-24', '2018-03-31'),
('2018-04-01', '2018-08-16'),
('2018-08-17', '2018-12-31');
You can use a recursive CTE. The basic idea is to start with the first date 2018-01-21 and build a list of all months' start and end date upto the last date 2018-12-31. Then inner join with your data and clamp the dates if necessary.
DECLARE #Dates TABLE (StartDate DATE, EndDate DATE);
INSERT INTO #Dates (StartDate, EndDate) VALUES
('2018-01-21', '2018-01-29'),
('2018-01-30', '2018-02-23'),
('2018-02-24', '2018-03-31'),
('2018-04-01', '2018-08-16'),
('2018-08-17', '2018-12-31');
WITH minmax AS (
-- clamp min(start date) to 1st day of that month
SELECT DATEADD(MONTH, DATEDIFF(MONTH, CAST('00010101' AS DATE), MIN(StartDate)), CAST('00010101' AS DATE)) AS mindate, MAX(EndDate) AS maxdate
FROM #Dates
), months AS (
-- calculate first and last day of each month
-- e.g. for February 2018 it'll return 2018-02-01 and 2018-02-28
SELECT mindate AS date01, DATEADD(DAY, -1, DATEADD(MONTH, 1, mindate)) AS date31, maxdate
FROM minmax
UNION ALL
SELECT DATEADD(MONTH, 1, prev.date01), DATEADD(DAY, -1, DATEADD(MONTH, 2, prev.date01)), maxdate
FROM months AS prev
WHERE prev.date31 < maxdate
)
SELECT
-- clamp start and end date to first and last day of corresponding month
CASE WHEN StartDate < date01 THEN date01 ELSE StartDate END,
CASE WHEN EndDate > date31 THEN date31 ELSE EndDate END
FROM months
INNER JOIN #Dates ON date31 >= StartDate AND EndDate >= date01
If rCTE is not an option you can always JOIN with a table of numbers or table of dates (the idea above still applies).
You can Cross Apply with the Master..spt_values table to get a row for each month between StartDate and EndDate.
SELECT *
into #dates
FROM (values
('2018-01-21', '2018-01-29')
,('2018-01-30', '2018-02-23')
,('2018-02-24', '2018-03-31')
,('2018-04-01', '2018-08-16')
,('2018-08-17', '2018-12-31')
)d(StartDate , EndDate)
SELECT
SplitStart as StartDate
,case when enddate < SplitEnd then enddate else SplitEnd end as EndDate
FROM #dates d
cross apply (
SELECT
cast(dateadd(mm, number, dateadd(dd, (-datepart(dd, d.startdate) +1) * isnull((number / nullif(number, 0)), 0), d.startdate)) as date) as SplitStart
,cast(dateadd(dd, -datepart(dd, dateadd(mm, number+1, startdate)), dateadd(mm, number+1, startdate)) as date) as SplitEnd
FROM
master..spt_values
where type = 'p'
and number between 0 and (((year(enddate) - year(startdate)) * 12) + month(enddate) - month(startdate))
) s
drop table #dates
The following should also work
First i put startdates and enddates into a single column in the cte-block data.
In the block som_eom, i create the start_of_month and end_of_month for all 12 months.
I union steps 1 and 2 into curated_set
I create curated_set which is ordered by the date column
Finally i reject the unwanted records, in my filter clause not in('som','StartDate')
with data
as (select *
from dates
unpivot(x for y in(startdate,enddate))t
)
,som_eom
as (select top 12
cast('2018-'+cast(row_number() over(order by (select null)) as varchar(2))+'-01' as date) as som
,dateadd(dd
,-1
,dateadd(mm
,1
,cast('2018-'+cast(row_number() over(order by (select null)) as varchar(2))+'-01' as date)
)
) as eom
from information_schema.tables
)
,curated_set
as(select *
from data
union all
select *
from som_eom
unpivot(x for y in(som,eom))t
)
,curated_data
as(select x
,y
,lag(x) over(order by x) as prev_val
from curated_set
)
select prev_val as st_dt,x as end_dt
,y
from curated_Data
where y not in('som','StartDate')
Start with the initial StartDate and calculate the end of month or simply use the EndDate if it's within the same month.
Use the newly calculated EndDate+1 as StartDate for recursion and repeat the calculation.
WITH cte AS
( SELECT StartDate, -- initial start date
CASE WHEN EndDate < DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+1,0))
THEN EndDate
ELSE DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+1,0))
END AS newEnd, -- LEAST(end of current month, EndDate)
EndDate
FROM #Dates
UNION ALL
SELECT dateadd(DAY,1,newEnd), -- previous end + 1 day, i.e. 1st of current month
CASE WHEN EndDate <= DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+2,0))
THEN EndDate
ELSE DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+2,0))
END, -- LEAST(end of next month, EndDate)
EndDate
FROM cte
WHERE newEnd < EndDate
)
SELECT StartDate, newEnd
FROM cte

SQL Server Group by date and by time of day over a date range

I'm not even sure if this can/should be done is SQL but here goes.
I have a table that stores a start date and an end date like so
userPingId createdAt lastUpdatedAt
1 2017-10-17 11:31:52.160 2017-10-18 14:31:52.160
I want to return a result set that groups the results by date and if they were active between different points between the two date.
The different points are
Morning - Before 12pm
Afternoon - Between 12pm and 5pm
Evening - After 5pm
So for example I would get the following results
sessionDate morning afternoon evening
2017-10-17 1 1 1
2017-10-18 1 1 0
Here is what I have so far and I believe that it's quite close but the fact I can't get the results I need make me think that this might not be possible in SQL (btw i'm using a numbers lookup table in my query which I saw on another tutorial)
DECLARE #s DATE = '2017-01-01', #e DATE = '2018-01-01';
;WITH d(sessionDate) AS
(
SELECT TOP (DATEDIFF(DAY, #s, #e) + 1) DATEADD(DAY, n-1, #s)
FROM dbo.Numbers ORDER BY n
)
SELECT
d.sessionDate,
sum(case when
(CONVERT(DATE, createdAt) = d.sessionDate AND datepart(hour, createdAt) < 12)
OR (CONVERT(DATE, lastUpdatedAt) = d.sessionDate AND datepart(hour, lastUpdatedAt) < 12)
then 1 else 0 end) as Morning,
sum(case when
(datepart(hour, createdAt) >= 12 and datepart(hour, createdAt) < 17)
OR (datepart(hour, lastUpdatedAt) >= 12 and datepart(hour, lastUpdatedAt) < 17)
OR (datepart(hour, createdAt) < 12 and datepart(hour, lastUpdatedAt) >= 17)
then 1 else 0 end) as Afternoon,
sum(case when datepart(hour, createdAt) >= 17 OR datepart(hour, lastUpdatedAt) >= 17 then 1 else 0 end) as Evening
FROM d
LEFT OUTER JOIN MYTABLE AS s
ON s.createdAt >= #s AND s.lastUpdatedAt <= #e
AND (CONVERT(DATE, s.createdAt) = d.sessionDate OR CONVERT(DATE, s.lastUpdatedAt) = d.sessionDate)
WHERE d.sessionDate >= #s AND d.sessionDate <= #e
AND userPingId = 49
GROUP BY d.sessionDate
ORDER BY d.sessionDate;
Building on what you started with the numbers table, you can add the time ranges to your adhoc calendar table using another common table expression using cross apply()
and the table value constructor (values (...),(...)).
From there, you can use an inner join based on overlapping date ranges along with conditional aggregation to pivot the results:
declare #s datetime = '2017-01-01', #e datetime = '2018-01-01';
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, d as ( /* adhoc date/numbers table */
select top (datediff(day, #s, #e)+1)
SessionDate=convert(datetime,dateadd(day,row_number() over(order by (select 1))-1,#s))
from n as deka cross join n as hecto cross join n as kilo
cross join n as tenK cross join n as hundredK
order by SessionDate
)
, h as ( /* add time ranges to date table */
select
SessionDate
, StartDateTime = dateadd(hour,v.s,SessionDate)
, EndDateTime = dateadd(hour,v.e,SessionDate)
, v.point
from d
cross apply (values
(0,12,'morning')
,(12,17,'afternoon')
,(17,24,'evening')
) v (s,e,point)
)
select
t.userPingId
, h.SessionDate
, morning = count(case when point = 'morning' then 1 end)
, afternoon = count(case when point = 'afternoon' then 1 end)
, evening = count(case when point = 'evening' then 1 end)
from t
inner join h
on t.lastupdatedat >= h.startdatetime
and h.enddatetime > t.createdat
group by t.userPingId, h.SessionDate
rextester demo: http://rextester.com/MVB77123
returns:
+------------+-------------+---------+-----------+---------+
| userPingId | SessionDate | morning | afternoon | evening |
+------------+-------------+---------+-----------+---------+
| 1 | 2017-10-17 | 1 | 1 | 1 |
| 1 | 2017-10-18 | 1 | 1 | 0 |
+------------+-------------+---------+-----------+---------+
Alternately, you could use pivot() instead of conditional aggregation in the final select:
select UserPingId, SessionDate, Morning, Afternoon, Evening
from (
select
t.userPingId
, h.SessionDate
, h.point
from t
inner join h
on t.lastupdatedat >= h.startdatetime
and h.enddatetime > t.createdat
) t
pivot (count(point) for point in ([Morning], [Afternoon], [Evening])) p
rextester demo: http://rextester.com/SKLRG63092
You can using PIVOT on CTE's to derive solution to this problem.
Below is the test table
select * from ping
Below is the sql query
;with details as
(
select userPingId, createdAt as presenceDate , convert(date, createdAt) as
onlyDate,
datepart(hour, createdAt) as onlyHour
from ping
union all
select userPingId, lastUpdatedAt as presenceDate , convert(date,
lastUpdatedAt) as onlyDate,
datepart(hour, lastUpdatedAt) as onlyHour
from ping
)
, cte as
(
select onlyDate,count(*) as count,
case
when onlyHour between 0 and 12 then 'morning'
when onlyHour between 12 and 17 then 'afternoon'
when onlyHour>17 then 'evening'
end as 'period'
from details
group by onlyDate,onlyHour
)
select onlyDate, coalesce(morning,0) as morning,
coalesce(afternoon,0) as afternoon , coalesce(evening,0) as evening from
(
select onlyDate, count,period
from cte ) src
pivot
(
sum(count)
for period in ([morning],[afternoon],[evening])
) p
Below is the final result
This is a fairly similar answer to the one already posted, I just wanted the practice with PIVOT :)
I use a separate table with the time sections in it. this is then cross joined with the number table to create a date and time range for bucketing. i join this to the data and then pivot it (example: https://data.stackexchange.com/stackoverflow/query/750496/bucketing-data-into-date-am-pm-evening-and-pivoting-results)
SELECT
*
FROM (
SELECT
[userPingId],
dt,
[desc]
FROM (
SELECT
DATEADD(D, number, #s) AS dt,
CAST(DATEADD(D, number, #s) AS datetime) + CAST(s AS datetime) AS s,
CAST(DATEADD(D, number, #s) AS datetime) + CAST(e AS datetime) AS e,
[desc]
FROM #numbers
CROSS JOIN #times
WHERE number < DATEDIFF(D, #s, #e)
) ts
INNER JOIN #mytable AS m
ON m.createdat < ts.e
AND m.[lastUpdatedAt] >= ts.s
) src
PIVOT
(
COUNT([userPingId])
FOR [desc] IN ([am], [pm], [ev])
) piv;
the #times table is just:
s e desc
00:00:00.0000000 12:00:00.0000000 am
12:00:00.0000000 17:00:00.0000000 pm
17:00:00.0000000 23:59:59.0000000 ev

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

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