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

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

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

In SQL how to calculate days in a year based on a start date and the number of days lapsed

What would be the SQL to calculate the number of days in each year if I had a start date and the number of days that have lapsed?
For example, the date (ymd) 2013-01-01 and the days lapsed is 1000.
I would like the result to look like this
2013 = 365
2014 = 365
2015 = 270
Can this be written as a function like datediff?
I have tried using a calendar table, but of course, linking to this just gives me 2013 = 1000
My calendar table looks like this.
DATE_ID | DATE | CALENDAR_YEAR | FINANCIAL_YEAR
-----------------------------------------------
20130101 | 2013-01-01 | 2013 | 2013/14
This is what i have tried.
SELECT
D.FISCAL_YEAR, SUM([DAYS]) AS NUMBER_OF_DAYS
FROM [dbo].[FACT] F
LEFT JOIN [dbo].[DIM_DATE] D ON D.DATE_ID = F.DATE_ID
GROUP BY
D.FISCAL_YEAR
The result for this is.
FISCAL_YEAR | NUMBER_OF_DAYS
----------------------------
2013/14 |2820
2014/15 |6635
2015/16 |2409
I would personally build a tally table to do this. Once you build that, you can easly get every date and count the number of days in each year:
DECLARE #YMD date = '20130101',
#Lapsed int = 1000;
--Build a Tally table
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1, N N2, N N3, N N4), --10,000 should be enough
--Build the dates table
Dates AS(
SELECT DATEADD(DAY, T.I, #YMD) AS CalendarDate
FROM Tally T
WHERE T.I <= #Lapsed - 1)
--And count the days
SELECT DATEPART(YEAR, CalendarDate) AS Year,
COUNT(CalendarDate) AS Days
FROM Dates D
GROUP BY DATEPART(YEAR, CalendarDate);
As a function:
CREATE FUNCTION CountDays (#YMD date, #Lapsed int)
RETURNS table
AS RETURN
--Build a Tally table
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1, N N2, N N3, N N4), --10,000 should be enough
--Build the dates table
Dates AS(
SELECT DATEADD(DAY, T.I, #YMD) AS CalendarDate
FROM Tally T
WHERE T.I <= #Lapsed - 1)
--And count the days
SELECT DATEPART(YEAR, CalendarDate) AS Year,
COUNT(CalendarDate) AS Days
FROM Dates D
GROUP BY DATEPART(YEAR, CalendarDate);
GO
SELECT *
FROM (VALUES('20130101',1000),
('20150501',755))V(YMD, Lapsed)
CROSS APPLY dbo.CountDays(V.YMD,V.Lapsed) CD;
One method is a recursive CTE:
with dates as (
select v.d, 1000 - datediff(day, v.d, dateadd(year, 1, v.d)) as days
from (values (datefromparts(2013, 1, 1))) v(d)
union all
select dateadd(year, 1, d), days - datediff(day, d, dateadd(year, 1, d))
from dates
where days > 0
)
select d,
(case when days > 0 then datediff(day, d, dateadd(year, 1, d))
else datediff(day, d, dateadd(year, 1, d)) + days
end)
from dates;
Here is a db<>fiddle.

How to get previous 7 days' data from today in SQL Server

I have a DataEntry Table called GuestAddressData(UserId INT, EDate DateTime) with users data. I need to fetch the count of users for today to previous 7 Days. My Query:
SELECT
row_number() over (order by (SELECT 1)) ID,
count(*) Total,
LEFT(Datename(weekday, Cast(EDate as date)), 3) Day
FROM
CRM0001GuestAddressData
WHERE
EDate >= dateadd(week, datediff(d, -1, getdate()-2)/7, -1)
GROUP BY
Cast(EDate as date)
ORDER BY
Cast(EDate as date)
For example if today is Friday then my expected output is:
ID | TOTAL | DAY
------------------------
1 | 78 | Sat
2 | 23 | Sun
3 | 54 | Mon
4 | 17 | Tues
5 | 56 | Wed
6 | 45 | Thus
7 | 78 | Fri - Today
but this is not correct. How to solve it?
You can "generate" a list of seven numbers and use it to build the desired dates. Then left join with your data to get the counts, including zeros:
WITH datelist(num, a, b) AS (
SELECT num, DATEADD(DAY, -num, CAST(CURRENT_TIMESTAMP AS DATE)), DATEADD(DAY, -num + 1, CAST(CURRENT_TIMESTAMP AS DATE))
FROM (VALUES (0), (1), (2), (3), (4), (5), (6)) AS v(num)
)
SELECT 7 - num AS ID, datelist.a AS Day, COUNT(IDBooking)
FROM datelist
LEFT JOIN T_Bookings ON Opened >= datelist.a AND Opened < datelist.b
GROUP BY datelist.a, datelist.num
ORDER BY datelist.a
SELECT
row_number() over (order by dDate) ID,
cnt,
LEFT(Datename(weekday, dDate), 3) Day
from
(Select cast(EDate as Date) as dDate,
count(*) as cnt
FROM (values (0),(1),(2),(3),(4),(5),(6)) t(v)
inner join
CRM0001GuestAddressData gd on datediff(d, gd.Edate, getdate()) = t.v
WHERE
EDate >= dateadd(d, -6, cast(getdate() as date)) and EDate < dateadd(d,1,cast(getdate() as date))
GROUP BY
Cast(EDate as date)) tmp;
Note: You meant to get 7 days from yesterday, right? Nevermind, corrected based on your sample.
DBFiddle demo
EDIT: Having all days:
SELECT
row_number() over (order by dDate) ID,
cnt,
LEFT(Datename(weekday, dDate), 3) Day
from
(Select dateadd(d,-v,cast(getdate() as date)) as dDate,
count(Edate) as cnt
FROM (values (0),(1),(2),(3),(4),(5),(6)) t(v)
left join
CRM0001GuestAddressData gd on Datediff(d,gd.EDate, getdate()) = t.v
GROUP BY
dateadd(d,-v,cast(getdate() as date))) tmp;
DBFiddle Demo

Function to count if X amount amount of days as a full month

We offer services for clients and each client has an Authorization for 90 days
I want to create a function which counts 15 days as full months.
For example, let’s say a client get Authorization on 10/17/2017. It’s means it’s less than 15 days for October so that Authorization will not count for October, but it has to count for November, December and January 2018.
;WITH CTE AS (
select
d.ClientId,
LOC
datediff(day, l.DecisionOn, d.duedate) 'Days',
l.DecisionOn,
d.duedate
from code d
join codeloc l on d.curdocversionid = l.docversionid
join codeaccess a on a.docversionid = d.curdocversionid
where codeid = 69999
and aoca in ('68','69','70','71','72','74')
),
T AS (
SELECT ClientId, LOC, COUNT(*) CNT FROM CTE
WHERE [Days] > 15
AND AuthorizedDecisionOn > DATEADD(MONTH, (CASE WHEN DAY(GETDATE()) > 15 THEN 1 ELSE 0 END) , CAST( GETDATE() as date))
AND duedate < DATEADD(MONTH,3 + (CASE WHEN DAY(GETDATE()) > 15 THEN 1 ELSE 0 END) , CAST( GETDATE() as date))
GROUP BY ClientId, LOC
)
Here's an inline table valued function (iTvf) that will give you what you need.
(note: I use iTvf's because they outperform scalar udfs)
CREATE FUNCTION dbo.monthsBetweenMinDay
(
#fromDate date,
#toDate date,
#minDays tinyint
)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT Months = m.mb +
CASE WHEN DATEDIFF(day,d.fd,dateadd(month, -m.mb, d.td)) >= #minDays THEN 1 ELSE 0 END
FROM (VALUES (#fromDate, #toDate)) d(fd,td) -- from date and todate
CROSS APPLY (VALUES(
CASE WHEN d.fd > d.td THEN NULL
WHEN DATEPART(day, d.fd) > DATEPART(day, d.td) THEN DATEDIFF(month, d.fd, d.td)-1
ELSE DATEDIFF(month, d.fd, d.td) END)) m(mb);
Here's an example of the function in action:
-- sample data
CREATE TABLE #dates (date1 date, date2 date);
INSERT #dates
SELECT dt.dt, CAST(DATEADD(day, [days].d, DATEADD(month, months.m, dt.dt)) as date)
FROM (VALUES ('20170101')) dt(dt), (VALUES (4),(15),(25)) [days](d), (VALUES(0),(1),(4)) months(m);
-- solution
SELECT *
FROM #dates d
CROSS APPLY dbo.monthsBetweenMinDay(d.date1, d.date2, 15);
Results
date1 date2 Months
---------- ---------- -----------
2017-01-01 2017-01-05 0
2017-01-01 2017-01-16 1
2017-01-01 2017-01-26 1
2017-01-01 2017-02-05 1
2017-01-01 2017-02-16 2
2017-01-01 2017-02-26 2
2017-01-01 2017-05-05 4
2017-01-01 2017-05-16 5
2017-01-01 2017-05-26 5

How to group by ID as well as date with this query

This is a followup to a question I posted earlier. This code does exactly what I was asking about but I realized upon finally getting it working that what I was trying to do isn't actually quite what I need.
DECLARE #StartDateTime datetime = '2015-07-13 14:00:00',
#EndDateTime datetime = '2015-07-13 16:00:00';
WITH JumpsOf15 AS
(
SELECT ROW_NUMBER() OVER(ORDER BY object_id) * 15 AS Step
FROM sys.objects
),
Dates as
(
SELECT currentDate = steppedDate.steppedDate
FROM JumpsOf15
CROSS APPLY(SELECT DATEADD(MINUTE,Step,#StartDateTime) AS steppedDate ) AS steppedDate
WHERE #EndDateTime>steppedDate.steppedDate
)
SELECT d.currentDate, t.Value, t.FK_ConfigId
FROM Dates AS d
OUTER APPLY
( SELECT TOP 1 t.[Timestamp], t.Value, t.FK_ConfigId
FROM myTable AS t
WHERE t.[Timestamp] <= d.currentDate and t.FK_ConfigId in (208812, 208809, 208815)
ORDER BY t.[Timestamp] DESC, t.Value, t.FK_ConfigId
) AS t
This gives an output like so:
currentDate value FK_ConfigId
1/1/2015 12:15 2 208809
1/1/2015 12:30 5 208815
1/1/2015 12:45 1 208815
But actually I need to have one record per timestamp per unique FK_ConfigId that I specify in a list. Right now I am only getting one record per timestamp regardless of the config ID. The output I want:
currentDate value FK_ConfigId
1/1/2015 12:15 2 208809
1/1/2015 12:15 4 208815
1/1/2015 12:30 5 208809
1/1/2015 12:30 1 208815
How could I achieve this?
Try this and tell me what you think:
DECLARE #StartDateTime datetime = '2015-07-13 14:00:00',
#EndDateTime datetime = '2015-07-13 16:00:00';
WITH JumpsOf15 AS (
SELECT Step = Row_Number() OVER (ORDER BY (SELECT 1)) * 15
FROM sys.objects
),
Dates AS (
SELECT currentDate = DateAdd(minute, Step, #StartDateTime)
FROM JumpsOf15
WHERE Step < DateDiff(minute, 0, #EndDateTime - #StartDateTime)
)
SELECT
d.currentDate,
t.ConfigId,
t.Value
FROM
Dates d
CROSS JOIN (VALUES
(208812), (208809), (208815)
) c (ConfigId)
OUTER APPLY (
SELECT TOP 1
t.[Timestamp],
t.Value,
t.FK_ConfigId
FROM myTable t
WHERE
d.currentDate >= t.[Timestamp]
AND c.ConfigId = t.FK_ConfigId
ORDER BY
t.[Timestamp] DESC,
t.Value
) t
;