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

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

Related

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

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

Generate list of dates based on the day and occurrence of the month

I want to generate based on the day of the week and number of the occurrence in the month of a date, a list of dates for each month between two dates. Assuming I have a #StartDate = 2016/04/01 and #EndDate = 2016/09/01, i check that #StartDate is on a first Friday of April, then to #EndDate will create dates for all first Friday of each month:
2016/05/06
2016/06/03
2016/07/01
2016/08/05
In case #StartDate = 2016/04/12 and #EndDate = 2016/09/01, I note that the #StartDate is the second Tuesday of April, then went to get every second Tuesday Tuesday of each month :
2016/05/10
2016/06/14
2016/07/12
2016/08/09
In case#StartDate = 2016/04/28 and #EndDate = 2016/09/01, I note that the #StartDate is on the last Thursday of the month of April:
2016/05/26
2016/06/30
2016/07/28
2016/08/25
In the last case, i need to verify the number of weeks of each month, because exists months only with 4 weeks or with 5 weeks and i want the last occurrence.
What I have done? I found a code that gives me every Monday in the third week of the month, and i adopted a little to get a #StartDate and #EndDate:
;with
filler as (select row_number() over (order by a) a from (select top 100 1 as a from syscolumns) a cross join (select top 100 1 as b from syscolumns) b),
dates as (select dateadd(month, a-1, #StartDate ) date from filler where a <= 1000 and dateadd(month, a-1, #StartDate) < #EndDate),
FirstMonday as (
select dateadd(day, case datepart(weekday,Date) /*this is the case where verify the week day*/
when 1 then 1
when 2 then 0
when 3 then 6
when 4 then 5
when 5 then 4
when 6 then 3
when 7 then 2
end, Date) as Date
,case when datepart(weekday, #StartDate) = 1 then 3 else 2 end as Weeks /*here i verify the number of weeks to sum in the next date*/
from dates
)
select dateadd(week, Weeks, Date) as ThirdMonday
from FirstMonday
So, it is:
set #NumSemana = datepart(day, datediff(day, DATEADD(mm, DATEDIFF(mm,0,#StartDate), 0), #StartDate)/7 * 7)/7 + 1;
WITH AllDays
AS ( SELECT #StartDate AS [Date], DATEPART(month, #StartDate) as validMonth
UNION ALL
SELECT DATEADD(week, 1, [Date]),
iif(DATEPART(month,DATEADD(week, 1, [Date])) < validMonth + #PeriodicityRepeat, validMonth, validMonth + #PeriodicityRepeat)
FROM AllDays
WHERE
DATEPART(month,[Date]) <= DATEPART(month,#EndDate)
and DATEPART(year,[Date]) <= DATEPART(year,#EndDate)
),
rankedDays
AS(
SELECT [Date], validMonth,
row_number() over ( partition by DATEPART( month, [Date]) order by [Date]) ascOrder,
row_number() over ( partition by DATEPART( month, [Date]) order by [Date] desc) descOrder
FROM AllDays
WHERE DATEPART(month, [Date]) = validMonth
)
select [Date]
from rankedDays
where ((ascOrder = #NumSemana and #NumSemana <=4 )
or (descOrder = 1 and #NumSemana = 5)
or [Date] = #StartDate )
and [Date] < #EndDate
OPTION (MAXRECURSION 0)
Query:
DECLARE #StartDate DATE = '2016-04-28',
#EndDate DATE = '2016-09-01'
;WITH dates AS (
SELECT DATEADD(week, -5, #StartDate) as date_
UNION ALL
SELECT DATEADD(week,1,date_)
FROM dates
WHERE DATEADD(week,1,date_) < #enddate
), final AS (
SELECT ROW_NUMBER() OVER (PARTITION BY DATEPART(year,date_), DATEPART(month,date_) ORDER BY date_ ASC) as RN,
date_
FROM dates
), weeks AS (
SELECT *
FROM (VALUES
(1,1),
(2,2),
(3,3),
(4,4),
(4,5),
(5,4),
(5,5)
) as t(w1,w2)
WHERE w1 = (SELECT RN FROM final WHERE date_ = #StartDate)
)
SELECT MAX(date_) as date_
FROM final f
INNER JOIN weeks w ON f.RN = w.w2
WHERE date_ between #StartDate and #EndDate AND date_ != #StartDate
GROUP BY DATEPART(YEAR,date_), DATEPART(MONTH,date_)
ORDER BY MAX(date_) ASC
Outputs:
For #StartDate = 2016/04/01 and #EndDate = 2016/09/01
date_
----------
2016-05-06
2016-06-03
2016-07-01
2016-08-05
(4 row(s) affected)
For #StartDate = 2016/04/12 and #EndDate = 2016/09/01
date_
----------
2016-05-10
2016-06-14
2016-07-12
2016-08-09
(4 row(s) affected)
For #StartDate = 2016/04/28 and #EndDate = 2016/09/01
date_
----------
2016-05-26
2016-06-30
2016-07-28
2016-08-25
(4 row(s) affected)

How do I calculate Workdays except Weekends in SQL query?

I want to calculate which of my holidays are conflict with working days.
I have a Holidays table which is below.
NameOfHoliday StartDate DurationByDay
Christmas 26.12.2015 5
26 and 27 are at weekend. So this shouldt be calculated. So I have to get only 4 days as result.
DECLARE #t TABLE(
NameOfHoliday VARCHAR(10),
StartDate DATE,
DurationByDay SMALLINT
)
INSERT INTO #t
VALUES ('Christmas', '20151226', 5)
;WITH cte AS
(
SELECT *, cnt = 0
FROM #t
UNION ALL
SELECT t2.NameOfHoliday, DATEADD(DAY, t1.cnt + 1, t2.StartDate), t2.DurationByDay, t1.cnt + 1
FROM cte t1
JOIN #t t2 ON t1.NameOfHoliday = t2.NameOfHoliday
WHERE t1.cnt < t1.DurationByDay
)
SELECT NameOfHoliday, StartDate, DT
FROM (
SELECT *
, DT = DATENAME(DW, StartDate)
, RowNum = ROW_NUMBER() OVER (PARTITION BY StartDate ORDER BY NameOfHoliday)
FROM cte
) t
WHERE RowNum = 1
AND DT NOT IN ('Saturday', 'Sunday')
OPTION (MAXRECURSION 0)
output -
NameOfHoliday StartDate DT
------------- ---------- ------------------
Christmas 2015-12-28 Monday
Christmas 2015-12-29 Tuesday
Christmas 2015-12-30 Wednesday
Christmas 2015-12-31 Thursday
I Hope your requirement is like this..
if object_id('tempdb..#Holidays') is not null drop table #Holidays
create table #Holidays(id int identity(1,1),Holiday date)
insert into #Holidays values ('2015-12-25'),('2015-12-28')
set dateformat ymd
declare #StartDate date = '2015-12-01'
declare #EndtDate date = '2015-12-31'
--some times #EndtDate might be NULL so, set to getdate()
set #EndtDate = isnull(#EndtDate,cast(getdate() as date))
declare #Holiday int
set #Holiday = (select count(*) from #Holidays where Holiday between #StartDate and #EndtDate)
SELECT
(DATEDIFF(dd, #StartDate, #EndtDate) + 1)
-(DATEDIFF(wk, #StartDate, #EndtDate) * 2)
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, #EndtDate) = 'Saturday' THEN 1 ELSE 0 END)
-#Holiday