Conditional Count On Row_Number - sql

I have a query that calculates the number working days within a month based on a table which stores all our public holidays.
The current output would show all working days, excluding public holidays and Saturday and Sunday, I would like to show each day of the month, but don't increment on a public holiday or Saturday or Sunday.
Is there a way to conditionally increment the row number?
Query is below:
DECLARE #startnum INT=0
DECLARE #endnum INT=365;
WITH gen AS
(
SELECT #startnum AS num
UNION ALL
SELECT num + 1
FROM gen
WHERE num + 1 <= #endnum
)
, holidays AS
(
SELECT CONVERT(DATE, transdate) AS HolidayDate
FROM WORKCALENDER w
WHERE w.CALENDARID = 'PubHoliday'
)
, allDays AS
(
SELECT DATEADD( d, num, CONVERT( DATE, '1 Jan 2016' ) ) AS DateOfYear
, DATENAME( dw, DATEADD( d, num, CONVERT( DATE, '1 Jan 2016' ))) AS [dayOfWeek]
FROM gen
)
select number = ROW_NUMBER() OVER ( ORDER BY DateOfYear )
, *
from allDays
LEFT OUTER JOIN holidays
ON allDays.DateOfYear = holidays.HolidayDate
WHERE holidays.HolidayDate IS NULL
AND allDays.dayOfWeek NOT IN ( 'Saturday', 'Sunday')
AND DateOfYear >= CONVERT( DATE, '1 ' + DATENAME( MONTH, GETDATE() ) + ' 2016' )
AND DateOfYear < CONVERT( DATE, '1 ' + DATENAME( MONTH, DATEADD( month, 1, GETDATE()) ) + ' 2016' )
option (maxrecursion 10000)

kind of pseudo code
select date, row_number() over (order by date) as num
from ( select date
from allDates
where month = x and weekday
exept
select date
from holidays
where month is x
) as t
union all
select date, null
from holidays
where month is x
order by date

You could use a windowed sum, see how the output of WorkdaySequenceInMonth is composed.
DECLARE #startDate DATE = '20160101'
, #numDays INT = 365
, #num INT = 0;
DECLARE #Holidays TABLE (Holiday DATE);
INSERT INTO #Holidays(Holiday)
VALUES ('20160101')
, ('20160115')
, ('20160714');
WITH nums AS
(
SELECT row_number() OVER (ORDER BY object_id) - 1 as num
FROM sys.columns
),
dateRange as
(
SELECT
DATEADD(DAY, num, #startDate) AS Dt
, num
FROM nums
WHERE num < #numDays
),
Parts AS
(
SELECT
R.Dt as [Date]
, Year(R.Dt) as [Year]
, Month(R.Dt) as [Month]
, Day(R.Dt) as [Day]
, Datename(weekday, R.Dt) as [Weekday]
, CASE WHEN H.Holiday IS NOT NULL
OR Datename(weekday, R.Dt) IN ('Saturday', 'Sunday')
THEN 0
ELSE 1
END AS IsWorkday
FROM dateRange R
LEFT JOIN #Holidays H ON R.Dt = H.Holiday
)
--
select
*
, sum(IsWorkday) over (PARTITION BY [Year],[month]
ORDER BY [Day]
ROWS UNBOUNDED PRECEDING) as WorkdaySequenceInMonth
from Parts
order by [Year], [Month]

Hi You can try this query, the initial part is the data generation, maybe you won't need it.
Then I generate a temp table with all the dates for the time period set in #StartYear, #EndYear
Then just simple queries to return the data
-- generate holidays table
select holiday
into #tempHolidays
from
(
select '20160101' as holiday
union all
select '20160201' as holiday
union all
select '20160205' as holiday
union all
select '20160301' as holiday
union all
select '20160309' as holiday
union all
select '20160315' as holiday
) as t
create table #tempCalendar (Date_temp date)
select * from
#tempHolidays
declare #startYear int , #endYear int, #i int, #dateStart datetime , #dateEnd datetime, #date datetime, #i = 0
Select #startYear = '2016'
,#endYear = '2016'
,#dateStart = (Select cast( (cast(#startYear as varchar(4)) +'0101') as datetime))
,#dateEnd = (Select cast( (cast(#startYear as varchar(4)) +'1231') as datetime))
,#date = #dateStart
--Insert dates of the period of time
while (#date <> #dateEnd)
begin
insert into #tempCalendar
Select #date
set #date = (select DATEADD(dd,1,#date))
end
-- Retrive Date list
Select Date_temp
from #tempCalendar
where Date_temp not in (Select holiday from #tempHolidays)
and datename(weekday,Date_temp) not in ('Saturday','Sunday')
--REtrieve sum of working days per month
select DATEPART(year,Date_temp) as year
,DATEPART(month,Date_temp) as Month
,Count(*) as CountOfWorkingDays
from #tempCalendar
where Date_temp not in (Select holiday from #tempHolidays)
and datename(weekday,Date_temp) not in ('Saturday','Sunday')
Group by DATEPART(year,Date_temp)
,DATEPART(month,Date_temp)
You should change #tempHolidays for your Holidays table, and use #StarYear and #EndYear as your time period.

Here's a simple demo that shows the use of the partition by clause to keep contiguity in your sequencing for non-holidays
IF OBJECT_ID('tempdb.dbo.#dates') IS NOT null
DROP TABLE #dates;
CREATE TABLE #dates (d DATE);
IF OBJECT_ID('tempdb.dbo.#holidays') IS NOT null
DROP TABLE #holidays;
CREATE TABLE #holidays (d DATE);
INSERT INTO [#holidays]
( [d] )
VALUES
('2016-12-25'),
('2017-12-25'),
('2018-12-25');
INSERT INTO [#dates]
( [d] )
SELECT TOP 1000 DATEADD(DAY, n, '2015-12-31')
FROM [Util].dbo.[Numbers] AS [n];
WITH holidays AS (
SELECT d.*, CASE WHEN h.d IS NULL THEN 0 ELSE 1 END AS [IsHoliday]
FROM [#dates] AS [d]
LEFT JOIN [#holidays] AS [h]
ON [d].[d] = [h].[d]
)
SELECT d, ROW_NUMBER() OVER (PARTITION BY [holidays].[IsHoliday] ORDER BY d)
FROM [holidays]
ORDER BY d;
And please forgive my marking only Christmas as a holiday!

Related

my end goal is to see end of month data for previous month

My end goal is to see end of month data for previous month.
Our processing is a day behind so if today is 7/28/2021 our Process date is 7/27/2021
So, I want my data to be grouped.
DECLARE
#ProcessDate INT
SET #ProcessDate = (SELECT [PrevMonthEnddatekey] FROM dbo.dimdate WHERE datekey = (SELECT [datekey] FROM sometable [vwProcessDate]))
SELECT
ProcessDate
, LoanOrigRiskGrade
,SUM(LoanOriginalBalance) AS LoanOrigBalance
,Count(LoanID) as CountofLoanID
FROM SomeTable
WHERE
ProcessDate in (20210131, 20210228,20210331, 20210430, 20210531, 20210630)
I do not want to hard code these dates into my WHERE statement. I have attached a sample of my results.
I am GROUPING BY ProcessDate, LoanOrigRiskGrade
Then ORDERING BY ProcessDate, LoanOrigIRskGrade
It looks like you want the last day of the month for months within a specified range. You can parameterize that.
For SQL Server:
DECLARE #ProcessDate INT
SET #ProcessDate = (
SELECT [PrevMonthEnddatekey]
FROM dbo.dimdate
WHERE datekey = (
SELECT [datekey]
FROM sometable [vwProcessDate]
)
)
DECLARE #startDate DATE
DECLARE #endDate DATE
SET #startDate = '2021-01-01'
SET #endDate = '2021-06-30'
;
with d (dt, eom) as (
select #startDate
, convert(int, replace(convert(varchar(10), eomonth(#startDate), 102), '.', ''))
union all
select dateadd(month, 1, dt)
, eomonth(dateadd(month, 1, dt))
from d
where dateadd(month, 1, dt) < #endDate
)
SELECT ProcessDate
, LoanOrigRiskGrade
, SUM(LoanOriginalBalance) AS LoanOrigBalance
, Count(LoanID) as CountofLoanID
FROM SomeTable
inner join d on d.eom = SomeTable.ProcessDate
Difficult to check without sample data.

Based on day fetch all dates - sql

I have start date, end date and name of days. How can fetch all dates between those two dates of that specific days in sql?
example data:
start_date:4/11/2018
end_date: 5/11/2018
days: monday, thursday
expected output: all dates between start and end date which comes on monday and thursday and store them in table
updated
my present code(not working)
; WITH CTE(dt)
AS
(
SELECT #P_FROM_DATE
UNION ALL
SELECT DATEADD(dw, 1, dt) FROM CTE
WHERE dt < #P_TO_DATE
)
INSERT INTO Table_name
(
ID
,DATE_TIME
,STATUS
,CREATED_DATE
,CREATED_BY
)
SELECT #P_ID
,(SELECT dt FROM CTE WHERE DATENAME(dw, dt) In ('tuesday','friday',null))
,'NOT SENT'
,CAST(GETDATE() AS DATE)
,#USER_ID
Another approach for generating dates between ranges can be like following query. This will be faster compared to CTE or WHILE loop.
DECLARE #StartDate DATETIME = '2018-04-11'
DECLARE #EndDate DATETIME = '2018-05-15'
SELECT #StartDate + RN AS DATE FROM
(
SELECT (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))-1 RN
FROM master..[spt_values] T1
) T
WHERE RN <= DATEDIFF(DAY,#StartDate,#EndDate)
AND DATENAME(dw,#StartDate + RN) IN('Monday','Thursday')
Note:
If the row count present in master..[spt_values] is not sufficient for the provided range, you can make a cross join with the same to get a bigger range like following.
SELECT (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))-1 RN
FROM master..[spt_values] T1
CROSS JOIN master..[spt_values] T2
By this you will be able to generate date between a range with gap of 6436369 days.
You can use a recursive common table expression (CTE) to generate a list of days. With datepart(dw, ...) you can filter for specific days of the week.
An example that creates a list of Mondays and Thursdays between March 1st and today:
create table ListOfDates (dt date);
with cte as
(
select cast('2018-03-01' as date) as dt -- First day of interval
union all
select dateadd(day, 1, dt)
from cte
where dt < getdate() -- Last day of interval
)
insert into ListOfDates
(dt)
select dt
from cte
where datepart(dw, dt) in (2, 5) -- 2=Monday and 5=Thursday
option (maxrecursion 0)
See it working at SQL Fiddle.
This will work for you:
DECLARE #table TABLE(
ID INT IDENTITY(1,1),
Date DATETIME,
Day VARCHAR(50)
)
DECLARE #Days TABLE(
ID INT IDENTITY(1,1),
Day VARCHAR(50)
)
INSERT INTO #Days VALUES ('Monday')
INSERT INTO #Days VALUES ('Thursday')
DECLARE #StartDate DATETIME='2018-01-01';
DECLARE #EndDate DATETIME=GETDATE();
DECLARE #Day VARCHAR(50)='Friday';
DECLARE #TempDate DATETIME=#StartDate;
WHILE CAST(#TempDate AS DATE)<=CAST(#EndDate AS DATE)
BEGIN
IF EXISTS (SELECT 1 FROM #Days WHERE DAY IN (DATENAME(dw,#TempDate)))
BEGIN
INSERT INTO #table
VALUES (
#TempDate, -- Date - datetime
DATENAME(dw,#TempDate) -- Day - varchar(50)
)
END
SET #TempDate=DATEADD(DAY,1,#TempDate)
END
SELECT * FROM #table
INSERT INTO TargetTab(dateCOL)
SELECT dateCOL
FROM tab
WHERE dateCOL >= startdate AND dateCOL <= enddate
AND (DATENAME(dw,dateCOL) ='Thursday' OR DATENAME(dw,dateCOL) = 'Monday')
Try this query to get your result.
Use a recursive CTE to generate your dates, then filter by week day.
SET DATEFIRST 1 -- 1: Monday, 7 Sunday
DECLARE #StartDate DATE = '2018-04-11'
DECLARE #EndDate DATE = '2018-05-15'
DECLARE #WeekDays TABLE (WeekDayNumber INT)
INSERT INTO #WeekDays (
WeekDayNumber)
VALUES
(1), -- Monday
(4) -- Thursday
;WITH GeneratingDates AS
(
SELECT
GeneratedDate = #StartDate,
WeekDay = DATEPART(WEEKDAY, #StartDate)
UNION ALL
SELECT
GeneratedDate = DATEADD(DAY, 1, G.GeneratedDate),
WeekDay = DATEPART(WEEKDAY, DATEADD(DAY, 1, G.GeneratedDate))
FROM
GeneratingDates AS G -- Notice that we are referencing a CTE that we are also declaring
WHERE
G.GeneratedDate < #EndDate
)
SELECT
G.GeneratedDate
FROM
GeneratingDates AS G
INNER JOIN #WeekDays AS W ON G.WeekDay = W.WeekDayNumber
OPTION
(MAXRECURSION 30000)
Try this:
declare #start date = '04-11-2018'
declare #end date = '05-11-2018'
declare #P_ID int = 1
declare #USER_ID int = 11
;with cte as(
select #start [date]
union all
select dateadd(DAY, 1, [date]) from cte
where [date] < #end
)
--if MY_TABLE doesn't exist
select #P_ID,
[date],
'NOT SENT',
cast(getdate() as date),
#USER_ID
into MY_TABLE
from cte
--here you can specify days: 1 - Sunday, 2 - Monday, etc.
where DATEPART(dw,[date]) in (2, 5)
option (maxrecursion 0)
--if MY_TABLE does exist
--insert into MY_TABLE
--select #P_ID,
-- [date],
-- 'NOT SENT',
-- cast(getdate() as date),
-- #USER_ID
--from cte
--where DATEPART(dw,[date]) in (2, 5)
--option (maxrecursion 0)

How to get date between 2 days name from a table

I have 2 day names as input (For Example: "Friday" and "Monday". I need to get need to get all dates from a table in between those Friday and Monday (Friday, Saturday, Sunday, Monday) . If it is "Tuesday" and "Thursday" i need to get (Tuesday, Wednesday and Thursday )
My table is
If days are friday and monday. My Output should be
I tried this
SELECT
EMPLID, DUR, DayName, TRC
FROM DayTable
WHERE
DayName BETWEEN 'FRIDAY' AND 'MONDAY'
ORDER BY DUR ASC
but not working, Help me to solve this. Thanks in Advance
Try this query:
DECLARE #From int,#To int
Create Table #Days(Id int, DayOfWeek Varchar(100))
Insert into #Days Values
(1,'Sunday'),
(2,'Monday'),
(3,'Tuesday'),
(4,'Wednesday'),
(5,'Thursday'),
(6,'Friday'),
(7,'Saturday')
Select #From = Id from #Days where DayOfWeek = 'Friday'
Select #To = Id from #Days where DayOfWeek = 'Monday'
Select T.EMPLID, T.DUR, T.DayName, T.TRC from DayTable T
Inner Join #Days D on T.DayName = D.DayOfWeek AND (D.Id <= #To Or D.Id >= #From)
Hope this helps!
Update
Here's the same solution in a table valued function:
create function dbo.DaysBetween (
#DayFrom nvarchar(16)
, #DayTo nvarchar(16)
) returns #results table ([DayName] nvarchar(16))
as
begin
declare #daynames table (id smallint not null, [dayname] nvarchar(16) not null)
insert #daynames(id, [dayname])
values (0, 'Monday'),(1, 'Tuesday'),(2, 'Wednesday'),(3, 'Thursday'),(4, 'Friday'),(5, 'Saturday'),(6, 'Sunday')
declare #dayFromInt smallint
, #dayToInt smallint
select #dayFromInt = id from #daynames where [dayname] = #DayFrom
if (#dayFromInt is null)
begin
--hacky trick from https://stackoverflow.com/a/4681815/361842
set #dayFromInt = cast(('Invalid Day From Name: ' + #DayFrom) as int)
return
end
select #dayToInt = id from #daynames where [dayname] = #DayTo
if (#dayToInt is null)
begin
--hacky trick from https://stackoverflow.com/a/4681815/361842
set #dayToInt = cast(('Invalid Day To Name: '+ #DayTo) as int)
return
end
insert #results ([dayname])
select [dayname]
from #daynames
where
(
(#dayFromInt <= #dayToInt) and (id between #dayFromInt and #dayToInt)
or
(#dayFromInt > #dayToInt) and (id >= #dayFromInt or id <= #dayToInt)
)
return
end
go
Here are some example scenarios:
select * from dbo.DaysBetween('Monday','Friday')
select * from dbo.DaysBetween('Friday','Monday')
select * from dbo.DaysBetween('Tuesday','Thursday')
select * from dbo.DaysBetween('Thursday','Tuesday')
select * from dbo.DaysBetween('Christmasday','Monday')
go --required to get this result after the above error
select * from dbo.DaysBetween('Monday','Holiday')
To use this in your query, you'd do:
SELECT EMPLID
, DUR
, DayName
, TRC
FROM DayTable
WHERE
[DayName] in
(
select [DayName]
from dbo.DaysBetween('Friday','Monday')
)
ORDER BY DUR ASC
You cannot use the between operator like that. Datename returns a string, and the between operator is tru for all daynames that lexigraphically falls between "Friday" and "Monday".
I would suggest using
DayName in ('FRIDAY','SATURDAY','SUNDAY', 'MONDAY' )
Or use
set datefirst 2
...
where datepart(weekday,DUR)>3
If you use the DATEPART function on DUR, you will get the integer representation for the weekday.
So updating your WHERE clause:
SELECT
EMPLID, DUR, DayName, TRC
FROM DayTable
WHERE
DATEPART(weekday, DUR) BETWEEN 2 AND 6
ORDER BY DUR ASC
Where datepart will give you the weekday integer: SUN -> 1, MON-> 2 ... SAT -> 7
https://learn.microsoft.com/pt-br/sql/t-sql/functions/datepart-transact-sql
If you know your sql server's setting start day of the week is Sunday, then you can try this query :
SELECT
EMPLID, DUR, DayName, TRC
FROM DayTable
WHERE
DATEPART(WEEKDAY, DUR) <= 2 or DATEPART(WEEKDAY, DUR) >= 6
ORDER BY DUR ASC
Where week index of Monday = 2 and Friday = 6

Add Working Days to date

In my quest to construct a function that can calculate the date after x working days I came across this function:
ALTER FUNCTION [dbo].[AddBusinessDays] (#Date date,#n INT)
RETURNS DATE AS BEGIN
DECLARE #d INT;
SET #d=4-SIGN(#n)*(4-DATEPART(DW,#Date));
RETURN DATEADD(D,#n+((ABS(#n)+#d-2)/5)*2*SIGN(#n)-#d/7,#Date) END
This function works however I need to link it with my holiday table so that it can omit specific holidays in my country. When I run it with today's date (26-04-2017) I get this date after 20 working days 24-05-2017, so it omitted only the weekends. How do I modify it so that it also skips the holidays?
Apologies if I am sending too many requests for one problem. I am a beginner in SQL. Thanks
Instead of relying on hard to understand calculations wouldn't it be easier to explicitly have your working dates and select from there? For example:
DECLARE #NthWorkingDay INT = 33;
DECLARE #holidays TABLE
(
holiday DATE ,
[description] VARCHAR(500)
);
INSERT #holidays
( holiday, description )
VALUES ( '20170519', '...' ),
( '20170501', '...' ),
( '20170611', '...' ),
( '20170704', '...' );
DECLARE #sunday INT ,
#saturday INT;
-- 1/1/2000 is a known date - Saturday
SET #saturday = DATEPART(WEEKDAY, DATEFROMPARTS(2000, 1, 1));
SET #sunday = DATEPART(WEEKDAY, DATEFROMPARTS(2000, 1, 2));
WITH tally
AS ( SELECT TOP 5000
ROW_NUMBER() OVER ( ORDER BY t1.object_id ) AS N
FROM master.sys.all_columns t1
CROSS JOIN master.sys.all_columns t2
),
dates ( theDate )
AS ( SELECT DATEADD(DAY, N - 1, CAST(GETDATE() AS DATE))
FROM tally
),
workDates ( workingDay, workingDate )
AS ( SELECT ROW_NUMBER() OVER ( ORDER BY theDate ) ,
theDate
FROM dates
WHERE DATEPART(WEEKDAY, theDate) NOT IN ( #saturday, #sunday )
AND theDate NOT IN ( SELECT holiday
FROM #holidays )
)
SELECT workingDate
FROM workDates
WHERE workingDay = #NthWorkingDay;

Member Data between date ranges

I have a table in SQL Server 2014 named [Membership] containing personal member data and two date fields named [member from date] and [member to date].
I need to summarise the monthly membership. A member is counted in a given month only if they are a member for that whole month.
So for example, a person with [member from date] of '2014-02-01' and [member to date] of '2015-03-01' would be counted in the month of December 2014, but would not be if the [member to date] was, say, '2014-12-25'.
I need to summarise by every month going back to January 2010 and I have thousands of members in this table. The results need to look similar to this:
Month Count
Jan 2010 3230
Feb 2010 3235
Mar 2010 3232
..
Dec 2016 6279
I can't see how to work this because of the "only if they are a member for that whole month" rule.
Any help will be most appreciated!
Using spt_values and a cte to generate the calendar, here is an example that counts members.
declare #members table (member int, start_date date, end_date date)
insert #members select 1, '2015-12-15', '2017-01-15'
insert #members select 2, '2016-01-15', '2016-12-15'
insert #members select 3, '2016-03-01', '2016-10-31'
declare #cal_from datetime = '2016-01-01';
declare #cal_to datetime = '2016-12-31';
with calendar_cte as (
select top (datediff(month, #cal_from, #cal_to) + 1)
[Month] = month(dateadd(month, number, #cal_from))
, [Year] = year(dateadd(month, number, #cal_from))
, [Start] = dateadd(month, number, #cal_from)
, [End] = dateadd(day, -1, dateadd(month, number + 1, #cal_from))
from [master].dbo.spt_values
where [type] = N'P'
order by number
)
select [Month]
, [Year]
, [Count] = (select count(*)
from #members
where start_date <= [Start]
and end_date >= [End])
from calendar_cte
With the help of a Months table, this can be handled pretty easily:
/* creating a months table */
create table dbo.Months([Month] date primary key, MonthEnd date);
declare #StartDate date = '20100101'
,#NumberOfYears int = 30;
insert dbo.Months([Month],MonthEnd)
select top (12*#NumberOfYears)
[Month] = dateadd(month, row_number() over (order by number) -1, #StartDate)
, MonthEnd = dateadd(day,-1,
dateadd(month, row_number() over (order by number), #StartDate)
)
from master.dbo.spt_values;
/* the query */
select [Month], [Count]=count(*)
from dbo.Months mo
inner join dbo.[Membership] me on
/* Member since the start of the month */
me.MemberFromDate >= mo.[Month]
/* Member for the entire month being counted */
and me.MemberToDate > mo.[MonthEnd]
group by [Month]
order by [Month]
If you really don't want to have a Months table, you can use a cte like this:
declare #StartDate date = '20100101'
,#NumberOfYears int = 30;
;with Months as (
select top (12*#NumberOfYears)
[Month] = dateadd(month, row_number() over (order by number) -1, #StartDate)
, MonthEnd = dateadd(day,-1,
dateadd(month, row_number() over (order by number), #StartDate)
)
from master.dbo.spt_values
)
/* the query */
select [Month], [Count]=count(*)
from Months mo
inner join dbo.[Membership] me on
/* Member since the start of the month */
me.MemberFromDate >= mo.[Month]
/* Member for the entire month being counted */
and me.MemberToDate > mo.[MonthEnd]
group by [Month]
order by [Month]
create table members(name varchar(50),fromdate datetime, todate datetime)
go
create table months(firstday datetime)
go
insert members values('Joe','2014-02-01','2015-03-25'),('Jon','2014-03-12','2015-01-12')
declare #date datetime = '2000-01-01'
while (#date < '2016-01-01')
begin
insert into months values( #date )
select #date = dateadd(month,1,#date)
end
with MyCTE(date) as
( select left(convert(varchar, firstday, 120),7)
from members m
join months d on d.firstday > m.fromdate and d.firstday < datefromparts(year(m.todate),month(m.todate),1)
)
select date as 'month', count(*) as 'count'
from MyCTE
group by date