Finding the first and last business day for every month sql - sql

I am trying to find the first and last business day for every month since 1986.
Using this, I can find the first day of any given month using, but just that month and it does not take into consideration whether it is a business day or not. To make it easier for now, business day is simply weekdays and does not consider public holiday.
SELECT DATEADD(s,0,DATEADD(mm, DATEDIFF(m,0,getdate()),0))
But I am not able to get the correct business day, so I created a calendar table consisting of all the weekdays and thought that I can extract the min(date) from each month, but I am currently stuck.
Date
---------------
1986-01-01
1986-01-02
1986-01-03
1986-01-06
...and so on
I have tried to get the first day of every month instead, but it does not take into account whether the day is a weekend or not. It just simply give the first day of each month
declare #DatFirst date = '20000101', #DatLast date = getdate();
declare #DatFirstOfFirstMonth date = dateadd(day,1-day(#DatFirst),#DatFirst);
select DatFirstOfMonth = dateadd(month,n,#DatFirstOfFirstMonth)
from (select top (datediff(month,#DatFirstOfFirstMonth,#DatLast)+1)
n=row_number() over (order by (select 1))-1
from (values (1),(1),(1),(1),(1),(1),(1),(1)) a (n)
cross join (values (1),(1),(1),(1),(1),(1),(1),(1)) b (n)
cross join (values (1),(1),(1),(1),(1),(1),(1),(1)) c (n)
cross join (values (1),(1),(1),(1),(1),(1),(1),(1)) d (n)
) x
I am wondering if anyone can perhaps shed some light as to how can I best approach this issue.

If you already have your calendar table with all available dates, then you just need to filter by weekday.
SET DATEFIRST 1 -- 1: Monday, 7: Sunday
SELECT
Year = YEAR(T.Date),
Month = MONTH(T.Date),
FirstBusinessDay = MIN(T.Date),
LastBusinessDay = MAX(T.Date)
FROM
Calendar AS T
WHERE
DATEPART(WEEKDAY, T.Date) BETWEEN 1 AND 5 -- 1: Monday, 5: Friday
GROUP BY
YEAR(T.Date),
MONTH(T.Date)
You should use the query to mark these days on your calendar table, so it's easy to access them afterwards.
This is how you can mix it up with the generation of the calendar table (with recursion).
SET DATEFIRST 1 -- 1: Monday, 7: Sunday
declare
#DatFirst date = '20000101',
#DatLast date = getdate();
;WITH AllDays AS
(
SELECT
Date = #DatFirst
UNION ALL
SELECT
Date = DATEADD(DAY, 1, D.Date)
FROM
AllDays AS D
WHERE
D.Date < #DatLast
),
BusinessLimitsByMonth AS
(
SELECT
Year = YEAR(T.Date),
Month = MONTH(T.Date),
FirstBusinessDay = MIN(T.Date),
LastBusinessDay = MAX(T.Date)
FROM
AllDays AS T
WHERE
DATEPART(WEEKDAY, T.Date) BETWEEN 1 AND 5 -- 1: Monday, 5: Friday
GROUP BY
YEAR(T.Date),
MONTH(T.Date)
)
SELECT
*
FROM
BusinessLimitsByMonth AS B
ORDER BY
B.Year,
B.Month
OPTION
(MAXRECURSION 0) -- 0: Unlimited

If you got already a table with all the weekdays only:
select min(datecol), max(datecol)
from BusinessOnlyCalendar
group by year(datecol), month(datecol)
But you should expand your calendar to include all those calculations you might do on date, like FirstDayOfWeek/Month/Quarter/Year, WeekNumber, etc.
When you got a column in your calendar indicating business day yes/no, it's a simple:
select min(datecol), max(datecol)
from calendar
where businessday = 'y'
group by year(datecol), month(datecol)

Related

Every distinct Date between DateA and Date B -TSQL

I'm searching for a query like a calendar giving me back the distinct Dates between "Date A" and Date "A -49 days".
Date A is the a variable. If I look on the Query on Monday to Sunday it will give me back
the Date of the Sunday in the previous Week
the Date of the Sunday in the Week before the previous week
2 Weeks before the Previous Week
5 Weeks before the Previous Week
For Example: I started the query in '2022-01-23'
a_end: '2022-01-16' a_beginn: '2021-12-05' and every date between
b_end:'2022-01-09' b_beginn: '2021-11-29' and every date between
etc.
You could use a recursive CTE :
WITH T(d) AS (
SELECT CAST('2022-01-01' AS date)
UNION ALL
SELECT DATEADD(day, -1, d)
FROM T
WHERE d >= DATEADD(day, -49, '2022-01-01')
)
SELECT d
FROM T
-- OPTION (MAXRECURSION 1000)
If you have more than 100 days to generate you will need to set the MAXRECURSION query hint which is limited to 100 by default (0 means no limit). Beware of infinite loops with this setting though.
You can generate a dynamic calendar table as in this example:
with FindPrevSunday as (
select
dateadd(week,datediff(week, '1900-01-07', getdate()), '1900-01-07') PrevSunday
),
JustFourRows as (
select 1 as x union all select 1 as x union all
select 1 as x union all select 1 as x
),
LotsOfRows as (
select Dte=dateadd(day, -Row_number() over (order by a.x)+1, (select PrevSunday from FindPrevSunday))
from
JustFourRows a --4
cross Join
JustFourRows b --16
cross join
JustFourRows c --64
cross join
JustFourRows d -- 256
)
select Dte
from LotsOfRows
cross join
FindPrevSunday PrevS
where Dte between dateadd(day,-48, Prevs.PrevSunday) and PrevSunday
'1900-01-07' is a fixed reference point; known to be Sunday; datediff(week always brings whole/complete weeks; and we use the cross joins to quickly 'generate' rows corresponding to dates in the calendar; then we assign dates, and then filter for the limit we are interested in. This example can generate up to 256 days, but you can add more cross joins, if you wish.

Count # of Saturdays given a date range

I have a datetime field and a net field. The Sat Count field is done by =IIf(DatePart("w",Fields!DespatchDate.Value)=7,1,0)
I want to total the count of the Saturdays given a starting date and end date (typically a month).
I tried =Sum(IIf(DatePart("w",Fields!DespatchDate.Value)=7,1,0) but the total is wrong.
I also want to count Saturdays for rest of the month, e.g there's a missing 3rd Saturday in the picture.
I also want to do a total of the Net for Saturdays.
Can you point me in the direction. I can do it in SQL or in SSRS
Considering that we do not have any Input or desired output provided, I am assuming that You just want to count Saturdays in a given range:
Select COUNT(*), SUM(Net)
FROM table
WHERE Day# = 7 AND Date BETWEEN '2021-02-16' AND '2021-02-23'
Assuming you want to count saturdays even if it is not part of your dataset, what you need to do is pad out all your dates for the given range and then join it to your base data set.
This would ensure that it accounts for ALL days of the week regardless of a dispatch event occuring on that date / day.
Below is some SQL code that might help you make a start.
declare #startdate date = '2021-02-01'
declare #enddate date = '2021-02-28'
if OBJECT_ID ('tempdb..#dates') is not null
drop table #dates
;WITH mycte AS
(
SELECT CAST(#startdate AS DATETIME) DateValue
UNION ALL
SELECT DateValue + 1
FROM mycte
WHERE DateValue + 1 < #enddate
)
SELECT DateValue into #dates
FROM mycte
OPTION (MAXRECURSION 0)
select
d.DateValue
, datepart(weekday,d.DateValue) as day_no
,case when datepart(weekday,d.DateValue) = 7 then isnull(t.net,0) else 0 end as sat_net
,case when datepart(weekday,d.DateValue) = 1 then isnull(t.net,0) else 0 end as sun_net
from #dates d
left join your_table t
on d.DateValue = t.some_date
drop table #dates
Since I don't know what your required output is, I cannot summarise this any further. But you get the idea!

How to get six weeks data from a week column?

I have a legacy query in which I am looking data for six weeks as shown below. In my below AND condition I get data for past six weeks and it worked fine in 2020 middle and end. But since 2021 started, this stopped working because of obvious subtraction I am doing with 6.
AND data.week_col::integer BETWEEN DATE_PART(w, CURRENT_DATE) - 6 AND DATE_PART(w, CURRENT_DATE) - 1
There is a bug in above query because of which it stopped working in 2021. How can I change above condition so that it can work entire year without any issues and give me data for past 6 weeks.
Update
Below is my query which I am running:
select *,
dateadd(d, - datepart(dow, trunc(CONVERT_TIMEZONE('UTC','PST8PDT',client_date))), trunc(CONVERT_TIMEZONE('UTC','PST8PDT',client_date)) + 6) as day,
date_part(week, day) as week_col
from holder data
where data.week_col::integer BETWEEN DATE_PART(w, CURRENT_DATE) - 6 AND DATE_PART(w, CURRENT_DATE) - 1
client_date column has values like this - 2021-01-15 21:30:00.0. And from that I get value of day column and from day column I get value of
week_col column as shown above.
week_col column has values like 53, 52 .... It's a week number in general.
Because of my AND condition I am getting data for week 1 only but technically I want data for 49, 50, 51, 52, 53 and 1 as it is past six weeks. Can I use day column here to get correct past six weeks?
Would this serve as a solution? I do not know much about the redshirt syntax but I read it supports dateadd(). If you are normalizing client_date to a time zone converted day with no time then why not simply use that in the comparison to the current date converted to the same time zone.
WHERE
client_date BETWEEN
DATEADD(WEEK,-6,trunc(CONVERT_TIMEZONE('UTC','PST8PDT',CURRENT_DATE)))
AND
DATEADD(WEEK,-1,trunc(CONVERT_TIMEZONE('UTC','PST8PDT',CURRENT_DATE)))
If the above logic works out then you may want to convert the -6 and -1 week to variables, if that is supported.
Solution 2
This is a bit more verbose but involves virtualizing a calender table and then joining your current date parameter into the calender data, for markers. Finally, you can join your data against the calender which has been normalized by weeks in time chronologically.
This is SQL Server syntax, however, I am certain it can be converted to RS.
DECLARE #D TABLE(client_date DATETIME)
INSERT #D VALUES
('11/20/2020'),('11/27/2020'),
('12/4/2020'),('12/11/2020'),('12/18/2020'),('12/25/2020'),
('01/8/2021'),('01/8/2021'),('1/15/2021'),('1/22/2021'),('1/29/2021')
DECLARE #Date DATETIME = '1/23/2021'
DECLARE #StartDate DATETIME = '01/01/2010'
DECLARE #NumberOfDays INT = 6000
;WITH R1(N) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
R2(N) AS (SELECT 1 FROM R1 a, R1 b),
R3(N) AS (SELECT 1 FROM R2 a, R2 b),
Tally(Number) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM R3)
,WithTally AS
(
SELECT CalendarDate = DATEADD(DAY,T.Number,#StartDate)
FROM Tally T
WHERE T.Number < #NumberOfDays
)
,Calendar AS
(
SELECT
CalendarDate,
WeekIndex = DENSE_RANK() OVER(ORDER BY DATEPART(YEAR, CalendarDate), DATEPART(WEEK, CalendarDate))
FROM
WithTally
),
CalendarAlignedWithCurrentDateParamater AS
(
SELECT *
FROM
Calendar
CROSS JOIN (SELECT WeekIndexForToday=WeekIndex FROM Calendar WHERE Calendar.CalendarDate=#Date ) AS X
)
SELECT
D.*,
C.WeekIndex,
C.WeekIndexForToday
FROM
CalendarAlignedWithCurrentDateParamater C
INNER JOIN #D D ON D.client_date = C.CalendarDate
WHERE
C.WeekIndex BETWEEN C.WeekIndexForToday-6 AND C.WeekIndexForToday-1
OPTION (MAXRECURSION 0)

How to setup a cumulative count grouped by month with underlying conditions

I've come across somewhat of an interesting scenario where I'm needing to aggregate enrollment counts and group them by the individual month and all subsequent months leading up to the completion date. The starting counter will be placed into the month when the enrollment began, and now I'm needing to set up a cumulative sum to carry out the single count.
Here's a couple of test records I'm working with
I've set up the following query to compile the date_month CTE to compile the full 12 months derived from my Start/End Range variables. I've then joined it to my test table in order to establish the Counter placements.
DECLARE #EnrollmentDateStart DATETIME = '2020-01-01'
DECLARE #EnrollmentDateEnd DATETIME = '2020-12-01'
;WITH CTE_Months(year_month) AS
(
SELECT DATEADD(MONTH, n, DATEADD(MONTH, DATEDIFF(MONTH, 0, #EnrollmentDateStart), 0))
FROM ( SELECT TOP (DATEDIFF(MONTH, #EnrollmentDateStart, #EnrollmentDateEnd) + 1)
n = ROW_NUMBER() OVER (ORDER BY [object_id]) - 1
FROM sys.all_objects ORDER BY [object_id] ) AS n
)
SELECT
[Year] = YEAR(cm.year_month),
[Month] = DATENAME(MONTH, cm.year_month),
SUM(IIF(tt.[Enrollment Start Date] >= #EnrollmentDateStart,1,0)) AS EnrollmentCount
FROM CTE_Months cm
LEFT OUTER JOIN #TMP_Testing_Table tt
ON tt.[Enrollment Start Date] >= cm.year_month
AND tt.[Enrollment Start Date] < DATEADD(MONTH, 1, cm.year_month)
GROUP BY tt.Department, cm.year_month
At this stage, I'm pulling back the following results, so I now have the Enrollment Counts placed into the correct starting months derived from the Enrollment Start Date.
Now I'm trying to figure out what would be the best course of action to place the subsequent count for the additional months leading up to the Completion date?
For example - The first User (UserId: 1) was Enrolled in March, 2020, and Completed in August, 2020, so essentially I'm looking to produce the following result to reflect the number of months ranging between March <> July (Last month prior to Completion)
January: 0
February: 0
March: 1
April: 1
May: 1
June: 1
July: 1
August: 0
September: 0
October: 0
November: 0
December: 0
Thinking a cumulative total should be able to address the subsequent for the month by month range, however, I would then need to zero out the total for all subsequent months on and after the recorded Completion date for this record in question.
Seeing if I can get your thoughts/suggestions on how to address this scenario? Apologies if the information/explanation is confusing, but please let me know, and I'll do my best to elaborate.
....................
SELECT
[Year] = YEAR(cm.year_month),
[Month] = DATENAME(MONTH, cm.year_month),
count(tt.userid) AS EnrollmentCount
FROM CTE_Months cm
LEFT OUTER JOIN #TMP_Testing_Table tt on cm.year_month > eomonth([Enrollment Start Date], -1)
and cm.year_month <= tt.[Enrollment End Date]
GROUP BY cm.year_month

Query to check number of records created in a month.

My table creates a new record with timestamp daily when an integration is successful. I am trying to create a query that would check (preferably automated) the number of days in a month vs number of records in the table within a time frame.
For example, January has 31 days, so i would like to know how many days in january my process was not successful. If the number of records is less than 31, than i know the job failed 31 - x times.
I tried the following but was not getting very far:
SELECT COUNT (DISTINCT CompleteDate)
FROM table
WHERE CompleteDate BETWEEN '01/01/2015' AND '01/31/2015'
Every 7 days the system executes the job twice, so i get two records on the same day, but i am trying to determine the number of days that nothing happened (failures), so i assume some truncation of the date field is needed?!
One way to do this is to use a calendar/date table as the main source of dates in the range and left join with that and count the number of null values.
In absence of a proper date table you can generate a range of dates using a number sequence like the one found in the master..spt_values table:
select count(*) failed
from (
select dateadd(day, number, '2015-01-01') date
from master..spt_values where type='P' and number < 365
) a
left join your_table b on a.date = b.CompleteDate
where b.CompleteDate is null
and a.date BETWEEN '01/01/2015' AND '01/31/2015'
Sample SQL Fiddle (with count grouped by month)
Assuming you have an Integers table*. This query will pull all dates where no record is found in the target table:
declare #StartDate datetime = '01/01/2013',
#EndDate datetime = '12/31/2013'
;with d as (
select *, date = dateadd(d, i - 1 , #StartDate)
from dbo.Integers
where i <= datediff(d, #StartDate, #EndDate) + 1
)
select d.date
from d
where not exists (
select 1 from <target> t
where DATEADD(dd, DATEDIFF(dd, 0, t.<timestamp>), 0) = DATEADD(dd, DATEDIFF(dd, 0, d.date), 0)
)
Between is not safe here
SELECT 31 - count(distinct(convert(date, CompleteDate)))
FROM table
WHERE CompleteDate >= '01/01/2015' AND CompleteDate < '02/01/2015'
You can use the following query:
SELECT DATEDIFF(day, t.d, dateadd(month, 1, t.d)) - COUNT(DISTINCT CompleteDate)
FROM mytable
CROSS APPLY (SELECT CAST(YEAR(CompleteDate) AS VARCHAR(4)) +
RIGHT('0' + CAST(MONTH(CompleteDate) AS VARCHAR(2)), 2) +
'01') t(d)
GROUP BY t.d
SQL Fiddle Demo
Explanation:
The value CROSS APPLY-ied, i.e. t.d, is the ANSI string of the first day of the month of CompleteDate, e.g. '20150101' for 12/01/2015, or 18/01/2015.
DATEDIFF uses the above mentioned value, i.e. t.d, in order to calculate the number of days of the month that CompleteDate belongs to.
GROUP BY essentially groups by (Year, Month), hence COUNT(DISTINCT CompleteDate) returns the number of distinct records per month.
The values returned by the query are the differences of [2] - 1, i.e. the number of failures per month, for each (Year, Month) of your initial data.
If you want to query a specific Year, Month then just simply add a WHERE clause to the above:
WHERE YEAR(CompleteDate) = 2015 AND MONTH(CompleteDate) = 1