What is the best way to find next n week days - sql

I got the following code from the following question I asked:
Passing in Week Day name to get nearest date in SQL
I need to find next 4 Weekdays based on today's date for corresponding Day-Of-Week in my table ie, if today is 2015-01-24 the result should be 1/24, 1/31, 2/7, 2/14 for Saturdays.
TABLE
SAMPLE QUERY
create table #t
(
jobId int,
personId int,
frequencyVal varchar(10)
);
insert into #t values (1,100,'Mondays'),(2,101,'Saturdays');
WITH cte(n) AS
(
SELECT 0
UNION ALL
SELECT n+1 FROM cte WHERE n < 3
)
select #t.jobId, #t.personId, #t.frequencyVal, STUFF(a.d, 1, 1, '') AS FutureDates
from #t
cross apply (SELECT CASE #t.frequencyVal
WHEN 'SUNDAYS' THEN 1
WHEN 'MONDAYS' THEN 2
WHEN 'TUESDAYS' THEN 3
WHEN 'WEDNESDAYS' THEN 4
WHEN 'THURSDAYS' THEN 5
WHEN 'FRIDAYS' THEN 6
WHEN 'SATURDAYS' THEN 7
END)tranlationWeekdays(n)
cross apply (select ',' + CONVERT(varchar(10), CONVERT(date,dateadd(WEEK, cte.n,CONVERT(DATE, DATEADD(DAY, (DATEPART(WEEKDAY, GETDATE()) + tranlationWeekdays.n) % 7, GETDATE()))))) from cte FOR XML PATH('')) a(d);
drop table #t;
EXPECTED RESULT

Gets the first day of current month.
DECLARE #FIRSTDAY DATE=DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0)
Create the table and insert values
create table #t
(
jobId int,
personId int,
frequencyVal varchar(10)
);
insert into #t values (1,100,'Mondays'),(2,101,'Saturdays');
You can use either of the below queries for your situation.
QUERY 1 : Select the first 4 week of days in current month for particular week day
-- Gets the first day of current month
DECLARE #FIRSTDAY DATE=DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0)
;WITH CTE as
(
-- Will find all dates in current month
SELECT #FIRSTDAY as DATES
UNION ALL
SELECT DATEADD(DAY,1,DATES)
FROM CTE
WHERE DATES < DATEADD(MONTH,1,#FIRSTDAY)
)
,CTE2 AS
(
-- Join the #t table with CTE on the datename+'s'
SELECT jobId,personId,frequencyVal, DATES,
ROW_NUMBER() OVER(PARTITION BY DATENAME(WEEKDAY,CTE.DATES) ORDER BY CTE.DATES) DATECNT
FROM CTE
JOIN #t ON DATENAME(WEEKDAY,CTE.DATES)+'s' = #t.frequencyVal
WHERE MONTH(DATES)= MONTH(GETDATE())
)
-- Converts to CSV and make sure that only 4 days are generated for month
SELECT DISTINCT C2.jobId,C2.personId,frequencyVal,
SUBSTRING(
(SELECT ', ' + CAST(DATEPART(MONTH,DATES) AS VARCHAR(2)) + '/' +
CAST(DATEPART(DAY,DATES) AS VARCHAR(2))
FROM CTE2
WHERE C2.jobId=jobId AND C2.personId=personId AND DATECNT<5
ORDER BY CTE2.DATES
FOR XML PATH('')),2,200000) futureDates
FROM CTE2 C2
SQL FIDDLE
For example, in Query1 the nearest date(here we take example as Saturday) of
2015-Jan-10 will be 01/03,01/10,01/17,01/24
2015-Jan-24 will be 01/03,01/10,01/17,01/24
QUERY 2 : Select nearest 4 week of days in current month for particular week day
-- Gets the first day in current month
DECLARE #FIRSTDAY DATE=DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0)
;WITH CTE as
(
-- Will find all dates in current
SELECT CAST(#FIRSTDAY AS DATE) as DATES
UNION ALL
SELECT DATEADD(DAY,1,DATES)
FROM CTE
WHERE DATES < DATEADD(MONTH,1,#FIRSTDAY)
)
,CTE2 AS
(
-- Join the #t table with CTE on the datename+'s'
SELECT jobId,personId,frequencyVal,DATES,
-- Get week difference for each weekday
DATEDIFF(WEEK,DATES,GETDATE()) WEEKDIFF,
-- Count the number of weekdays in a month
COUNT(DATES) OVER(PARTITION BY DATENAME(WEEKDAY,CTE.DATES)) WEEKCOUNT
FROM CTE
JOIN #t ON DATENAME(WEEKDAY,CTE.DATES)+'s' = #t.frequencyVal
WHERE MONTH(DATES)= MONTH(GETDATE())
)
-- Converts to CSV and make sure that only nearest 4 week of days are generated for month
SELECT DISTINCT C2.jobId,C2.personId,frequencyVal,
SUBSTRING(
(SELECT ', ' + CAST(DATEPART(MONTH,DATES) AS VARCHAR(2)) + '/' +
CAST(DATEPART(DAY,DATES) AS VARCHAR(2))
FROM CTE2
WHERE C2.jobId=jobId AND C2.personId=personId AND C2.frequencyVal=frequencyVal AND
((WEEKDIFF<3 AND WEEKDIFF>-3 AND WEEKCOUNT = 5) OR WEEKCOUNT <= 4)
ORDER BY CTE2.DATES
FOR XML PATH('')),2,200000) futureDates
FROM CTE2 C2
SQL FIDDLE
For example, in Query2 the nearest date(here we take example as Saturday) of
2015-Jan-10 will be 01/03,01/10,01/17,01/24
2015-Jan-24 will be 01/10,01/17,01/24,01/31
QUERY 3 : Select next 4 week's dates for particular week day irrelevant of month
;WITH CTE as
(
-- Will find all dates in current month
SELECT CAST(GETDATE() AS DATE) as DATES
UNION ALL
SELECT DATEADD(DAY,1,DATES)
FROM CTE
WHERE DATES < DATEADD(DAY,28,GETDATE())
)
,CTE2 AS
(
-- Join the #t table with CTE on the datename+'s'
SELECT jobId,personId,frequencyVal, DATES,
ROW_NUMBER() OVER(PARTITION BY DATENAME(WEEKDAY,CTE.DATES) ORDER BY CTE.DATES) DATECNT
FROM CTE
JOIN #t ON DATENAME(WEEKDAY,CTE.DATES)+'s' = #t.frequencyVal
)
-- Converts to CSV and make sure that only 4 days are generated for month
SELECT DISTINCT C2.jobId,C2.personId,frequencyVal,
SUBSTRING(
(SELECT ', ' + CAST(DATEPART(MONTH,DATES) AS VARCHAR(2)) + '/' +
CAST(DATEPART(DAY,DATES) AS VARCHAR(2))
FROM CTE2
WHERE C2.jobId=jobId AND C2.personId=personId AND C2.frequencyVal=frequencyVal
AND DATECNT < 5
ORDER BY CTE2.DATES
FOR XML PATH('')),2,200000) futureDates
FROM CTE2 C2
SQL FIDDLE
The following would be the output if the GETDATE() (if its Saturday) is
2015-01-05 - 1/10, 1/17, 1/24, 1/31
2015-01-24 - 1/24, 1/31, 2/7, 2/14

This is a simpler way I think, and I think it fits your requirements
Note that I have changed your frequency_val column to an integer that represents the day of the week from SQL servers perspective and added a calculated column to illustrate how you can easily derive the day name from that.
/*************************************************/
--Set up our sample table
/*************************************************/
declare #t table
(
jobId int,
personId int,
--frequencyVal varchar(10) -- why store a string when a tiny int will do.
frequency_val tinyint,
frequency_day as datename(weekday,frequency_val -1) + 's'
)
insert into #t
values
(1,100,1),--'Mondays'),
(2,101,6),--'Saturdays');
(3,101,7),--'Sundays');
(4,100,2)--'Tuesdays'),
--select * from #t
/*************************************************/
--Declare & initialise variables
/*************************************************/
declare #num_occurances int = 4
declare #from_date date = dateadd(dd,3,getdate()) -- this will allow you to play with the date simply by changing the increment value
/*************************************************/
-- To get a row for each occurance
/*************************************************/
;with r_cte (days_ahead, occurance_date)
as (select 0, convert(date,#from_date,121)
union all
select r_cte.days_ahead +1, convert(date,dateadd(DD, r_cte.days_ahead+1, #from_date),121)
from r_cte
where r_cte.days_ahead < (7 * #num_occurances) -1
)
select t.*, r_cte.occurance_date
from
#t t
inner join r_cte
on DATEPART(WEEKDAY, dateadd(dd,##DATEFIRST - 1 ,r_cte.occurance_date)) = t.frequency_val
/*************************************************/
--To get a single row with a CSV of every occurance
/*************************************************/
;with r_cte (days_ahead, occurance_date)
as (select 0, convert(date,#from_date,121)
union all
select r_cte.days_ahead +1, convert(date,dateadd(DD, r_cte.days_ahead+1, #from_date),121)
from r_cte
where r_cte.days_ahead < (7 * #num_occurances) -1
)
select
t.*,
STUFF( (select ', '
+ convert(varchar(2),datepart(month,occurance_date),0) + '/'
+ convert(varchar(2),datepart(day,occurance_date),0) as occurance
from r_cte
where DATEPART(WEEKDAY, dateadd(dd,##DATEFIRST - 1 ,r_cte.occurance_date)) = t.frequency_val
FOR XML PATH (''),TYPE).value('.','varchar(30)')
,1,2,'') occurance_date -- rest of STUFF() function
from
#t t

Related

How can I find how many Week Numbers are in a given interval of time in T-SQL

I am a bit stuck here, I want to find all the week numbers in a given interval of time and I can't really figure it out
For example , instead of
- > datepart(week,dateadd(day,-1,#oneSingleDate)) (monday is the first day of the week) ,
I need something like
- > datepart(week,#startDate,#endDate)
Given the interval '2019-01-04'-'2019-01-28' the output needs to be :
WeekNo : 1,2,3,4,5 .
i've used a recursive CTE to generate all the dates in the range, then I've selected the DISTINCT week numbers from them using DATEPART. Then I've concatenated then into your comma-separated string into a variable called #OUT
DECLARE #startDate as date = '20190104';
DECLARE #endDate as date = '2019-01-28';
DECLARE #OUT as nvarchar(max);
WITH CTE AS (SELECT #startDate As X
UNION ALL
SELECT DATEADD(y, 1, X) X FROM CTE where x < #endDate),
CTE2 AS (SELECT DISTINCT datepart(wk, X) wk from cte)
select #out = coalesce(#out + ', ', '') + cast(wk as nvarchar(4)) from cte2
OPTION (MAXRECURSION 0);
select #out;
You can do it as follows:
using :
- [CTE][1] common Table expression
- [Sql recursion union all][2]
- [Concatenation of multiple rows into one line][3]
declare #startDate as date ;
declare #endDate as date ;
set #startDate='2019-01-04';
set #endDate='2019-01-28' ;
DECLARE #weeks VARCHAR(8000) ;
with cte as (select #startDate as mydate,datepart(week,#startDate) w
union all select
dateadd(day,1,mydate),datepart(week,dateadd(day,1,mydate)) from cte
where mydate < #endDate) , cte2 as (select distinct(w) from cte)
select #weeks=COALESCE(#weeks + ', ', '') +cast(w as varchar(2)) from
cte2 OPTION (MAXRECURSION 360) select #weeks [Result]

How to calculate MTD given daily account balance in SQL Server?

I have a table with columns [accountid], [DateEnding], and [AccountBalance].
I need to calculate MTD using the balance of the current month and subtracting the account balance from the last day of the previous month for each accountid.
So far I have this:
SELECT [accountid]
,[DateEnding]
,[AccountBalance]
,[AccountBalance MTD Last] = AccountBalance - FIRST_VALUE(AccountBalance) OVER (PARTITION BY accountid, YEAR(DATEADD(mm,-1,[DateEnding])), MONTH(DATEADD(mm,-1,[DateEnding])) ORDER BY [DateEnding] DESC)
FROM [test]
ORDER BY accountid, DateEnding;
Here, for each distinct account, we find the latest record available according to DateEnding
we then find the last day of the last month by taking a number of days away equal to the current day number. e.g 23rd April 2019 we subtract 23 days to get 1st March 2019
we can then find the balance on that day.
Then put the calculation together in the SELECT
SELECT Q1.accountid,
Q2.DateEnding ,
Q3.EOMbalance,
Q2.LatestBalance,
Q2.LatestBalance - Q3.EOMbalance EOM
FROM (
SELECT Distinct t1.accountid FROM test t1
) Q1
CROSS APPLY (
SELECT TOP 1 t2.AccountBalance LatestBalance, t2.[DateEnding]
FROM test t2
WHERE t2.[accountid] = Q1.accountid
ORDER BY t2.[DateEnding] DESC
) Q2
CROSS APPLY (
SELECT Top 1 t3.AccountBalance EOMbalance
FROM test t3
WHERE t3.[accountid] = Q1.accountid
AND t3.[DateEnding]
= dateadd(day,0 - DAY(q2.dateending), q2.dateending)
ORDER BY t3.[DateEnding] DESC
) Q3
The first answer seems a little complicated for this problem (Cross Apply isn't necessary here).
The following may be easier for you:
I first look at the current day's account balances in subquery 'a'.
Then I look at the account balances from the last day of last month's data, in subquery 'b'.
Then it's just a matter of subtracting the two to show the MTD delta:
select a.accountid,
a.DateEnding,
a.AccountBalance as [Current AccountBalance],
b.AccountBalance as [EOM prior AccountBalance], --added for clarity
a.AccountBalance-b.AccountBalance as [AccountBalance MTD Last]
from
(select accountid, DateEnding, AccountBalance
from #test
where DateEnding = cast(getdate() as date)
/* getdate() returns today's date, so this query will also be with respect to today */
) a
left join
(select *
from #test
where DateEnding = DATEADD(MONTH, DATEDIFF(MONTH, -1, GETDATE())-1, -1)
/*this returns the last day of last month, always*/
) b
on a.accountid = b.accountid
Here is the SQL that makes this sample data and #test table. Simply execute it to have your own '#test' table to run against:
/*drop table #test
drop table #dates */
create table #test ([accountid] varchar(255),[DateEnding] date, [AccountBalance] decimal(16,2))
create table #dates (rnk int,dt date)
insert into #dates (dt)
values (cast('20180101' as date))
DECLARE
#basedate DATE,
#d INT
SELECT
#basedate = '20180101',
#d = 1
WHILE #d < (select datediff(day,cast('20180101' as date),getdate())+2) --select datediff(day,getdate(),cast('20180101' as datetime))
BEGIN
INSERT INTO #dates (dt)
values (DATEADD(day, 1, (select max(dt) from #dates)))
set #d = #d+1
END
update a
set a.rnk = b.rnk
from #dates a
left join (select rank() over (order by dt) rnk,dt from #dates) b on a.dt = b.dt
declare #a int
set #a = 1
declare #i int
set #i = 1
while #a <20
begin
while #i < (select max(rnk) from #dates)
begin
insert into #test
values (#a,(select dt from #dates where rnk = #i),cast(rand()*1000.0+#i as decimal(16,2)))
set #i=#i+1
end
set #a=#a+1
set #i = 1
end

How to list all months in specific period of time which doesn't have any order at that month

I want to know how to list all months in specific period of time which doesn't have any order. If you can help me.
I have Order Table has OrderDate column
I just make this:
select distinct month(Order.OrderDate) from Order where year(Order.OrderDate) = 1997
the result will show me the months that have order in specific year only
what should i do to complete this query
You need to retrieve the months in which no orders are placed for that we can use below query
;WITH months(MonthNumber) AS
(
SELECT 1
UNION ALL
SELECT MonthNumber+1
FROM months
WHERE MonthNumber < 12
)
SELECT DATENAME( month , DATEADD( month ,MonthNumber , 0 ) )
FROM months
EXCEPT
SELECT DISTINCT month([Order].OrderDate)
FROM [Order]
WHERE YEAR([Order].OrderDate) = 1997
You can try using left join like below
DEMO
select * from
(
VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12)
) AS M(val)
left join t1 on month(OrderDate)=val
and year(orderdate)=1997
where month(OrderDate) is null
The problem here is that if we we have a date range, then we may go beyond single year, for example 01-Jul-2017 to 30-Jun-2018 has 2 years, therefore creating a month range may NOT work in this scenario. The possible solution is to have a list of all the months in range along with the year, so that when we search an order, we'll search by the month and year both.
-- this is test order table, just to test the output
declare #order table(OrderDate date);
insert into #order(OrderDate) values('2018-01-01')
declare #dateRange table(d datetime not null primary key);
-- date range input parameter
declare #startDate date = '2017-06-01';
declare #endDate date = '2018-06-30';
-- modifying date range so that we go from start
-- of the month to the end of the month in the range
set #startDate = cast(year(#startDate) as varchar(100)) + '-' + cast(month(#startDate) as varchar(100)) + '-1';
set #endDate = dateadd(day, -1, dateadd(month, 1, cast(year(#endDate) as varchar(100)) + '-' + cast(month(#endDate) as varchar(100)) + '-1'));
-- creating dates for every month
declare #d date = #startDate;
while(#d <= #endDate)
begin
insert into #dateRange(d) values(#d);
set #d = dateadd(month, 1, #d);
end
-- selecting all the months in the range where
-- order does not exists
select cast(year(t.d) as varchar(100)) + '-' + DATENAME(month, t.d) as [Month]
from #dateRange as t
where not exists(
select 1
from #order as x
where month(x.OrderDate) = month(t.d) and year(x.OrderDate) = year(t.d)
)
order by t.d
Output: (notice that 2018-January is missing from result because it has an order)
Month
------------------
2017-June
2017-July
2017-August
2017-September
2017-October
2017-November
2017-December
2018-February
2018-March
2018-April
2018-May
2018-June
You are looking for a LEFT JOIN
CREATE TABLE Orders
(
OrderDate DATE
);
INSERT INTO Orders VALUES
('2018-01-01'),
('2018-03-01'),
('2018-05-15');
DECLARE #MND DATE = (SELECT MIN(OrderDate) FROM Orders);
DECLARE #MXD DATE = (SELECT MAX(OrderDate) FROM Orders);
WITH CTE AS
(
SELECT #MND OrderDate
UNION ALL
SELECT DATEADD(Month, 1, CTE.OrderDate)
FROM CTE
WHERE CTE.OrderDate <= DATEADD(Month, -1, #MXD)
)
SELECT MONTH(CTE.OrderDate) [Months]
FROM CTE LEFT JOIN Orders O ON MONTH(CTE.OrderDate) = MONTH(O.OrderDate)
AND
YEAR(CTE.OrderDate) = YEAR(O.OrderDate)
WHERE O.OrderDate IS NULL;
-- Add extra conditions here to filter the period needed
Returns:
+--------+
| Months |
+--------+
| 2 |
| 4 |
+--------+
Demo

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)

Possible recursive CTE query using date ranges

Not sure how to even phrase the title on this one!
I have the following data:
IF OBJECT_ID ('tempdb..#data') IS NOT NULL DROP TABLE #data
CREATE TABLE #data
(
id UNIQUEIDENTIFIER
,reference NVARCHAR(30)
,start_date DATETIME
,end_date DATETIME
,lapse_date DATETIME
,value_received DECIMAL(18,3)
)
INSERT INTO #data VALUES ('BE91B9C1-C02F-46F7-9B63-4D0B25D9BA2F','168780','2006-05-01 00:00:00.000',NULL,'2011-09-27 00:00:00.000',537.42)
INSERT INTO #data VALUES ('B538F123-C839-447A-B300-5D16EACF4560','320858','2011-08-08 00:00:00.000',NULL,NULL,0)
INSERT INTO #data VALUES ('1922465D-2A55-434D-BAAA-8E15D681CF12','306597','2011-04-08 00:00:00.000','2011-06-22 13:14:40.083','2011-08-07 00:00:00.000',12)
INSERT INTO #data VALUES ('7DF8FBCC-B490-4892-BDC5-8FD2D73B0323','321461','2011-07-01 00:00:00.000',NULL,'2011-09-25 00:00:00.000',8.44)
INSERT INTO #data VALUES ('1EC2E754-F325-4313-BDFC-9010E255F6FE','74215','2000-10-31 00:00:00.000',NULL,'2011-08-30 00:00:00.000',258)
INSERT INTO #data VALUES ('9E59B09C-0198-48AC-8EEC-A0D76CEA9385','169194','2008-06-25 00:00:00.000',NULL,'2011-09-25 00:00:00.000',1766.4)
INSERT INTO #data VALUES ('97CF6C0F-324A-49A6-B9D8-AC848A1F821A','288039','2010-09-01 00:00:00.000','2011-07-29 00:00:00.000','2011-08-21 00:00:00.000',55)
INSERT INTO #data VALUES ('97CF6C0F-324A-49A6-B9D8-AC848A1F821A','324423','2011-08-01 00:00:00.000',NULL,'2011-09-25 00:00:00.000',5)
INSERT INTO #data VALUES ('D5E5197A-E8E1-468C-9991-C8712224C2BF','323395','2011-08-25 00:00:00.000',NULL,NULL,0)
INSERT INTO #data VALUES ('0EC4976C-16B9-4C99-BD07-D0CBDF014D32','323741','2011-08-25 00:00:00.000',NULL,NULL,0)
And I want to be able to group all references into a category of 'active', 'lapsed' or 'new' based upon the following criteria:
Active has a start date that is less than the last date of the reference month, a lapse date after the last day of the prior month and a value_received > 0;
New has a start date which falls within the reference month;
Lapsed has a lapse date which falls within the reference month.
And to then apply these definitions for each reference for a rolling 13 months (so from Now going back as far as July 2010) so that for each month I can see how many references fall into each group.
I am able to use the following to define this for the current month:
select
id
,reference
,start_date
,end_date
,lapse_date
,value_received
,CASE WHEN start_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start date
AND lapse_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE())+1,0)) --last day of current month
AND value_received > 0
THEN 'Active'
WHEN lapse_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start
AND lapse_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE()),0)) --last day of prior month
THEN 'lapse'
WHEN start_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start date
AND start_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE()),0)) --last day of prior month
THEN 'New'
ELSE 'Not applicable'
END AS [type]
from #data
But I can't see a nice / efficient way of doing this (other than to repeat this query 13 times and union the results, which I know is just awful)
Would this be a case for using the current month as an anchor and using recursion (if so, some pointers would be most appreciated)?
Any help most appreciated as always :)
* Edited to include actual solution *
In case it's of interest to anyone, this is the final query I used:
;WITH Months as
(
SELECT DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE())+1,0)) as month_end
,0 AS level
UNION ALL
SELECT DATEADD(month, -1, month_end)as month_end
,level + 1 FROM Months
WHERE level < 13
)
SELECT
DATENAME(Month,month_end) + ' ' + DATENAME(YEAR,month_end) as date
,SUM(CASE WHEN start_date <= month_end
AND Month(start_date) <> MONTH(Month_end)
AND lapse_date > Month_end
THEN 1 ELSE 0 END) AS Active
,SUM(CASE WHEN start_date <= Month_end
AND DATENAME(MONTH,start_date) + ' ' + DATENAME(YEAR,start_date) =
DATENAME(MONTH,month_end) + ' ' + DATENAME(YEAR,month_end)
THEN 1 ELSE 0 END) AS New
,SUM(CASE WHEN lapse_date <= Month_end
AND Month(lapse_date) = MONTH(Month_end)
THEN 1 ELSE 0 END) AS lapse
FROM #data
CROSS JOIN Months
WHERE id IS NOT NULL
AND start_date IS NOT NULL
GROUP BY DATENAME(Month,month_end) + ' ' + DATENAME(YEAR,month_end)
ORDER by MAX(level) ASC
You don't need a "real" recursive CTE here. You can use one for the month references though:
;WITH Months
as
(
SELECT DATEADD(day, -DATEPART(day, GETDATE())+1, GETDATE()) as 'MonthStart'
UNION ALL
SELECT DATEADD(month, -1, MonthStart) as 'MonthStart'
FROM Months
)
Then you can JOIN to SELECT TOP 13 * FROM Months in your above query.
I'm not going to try to parse all your CASE statements, but essentially you can use a GROUP BY on the date and the MonthStart fields, like:
GROUP BY Datepart(year, monthstart), Datepart(month, monthstart)
and aggregate by month. It will probably be easiest to have all your options (active, lapsed, etc) as columns and calculate each with a SUM(CASE WHEN ... THEN 1 ELSE 0 END) as it will be easier with a GROUP BY.
You can cross join your request with a recursive CTE, this is a good idea.
WITH thirteenMonthBack(myDate, level) as
(
SELECT GETDATE() as myDate, 0 as level
UNION ALL
SELECT DATEADD(month, -1, myDate), level + 1
FROM thirteenMonthBack
WHERE level < 13
)
SELECT xxx
FROM youQuery
CROSS JOIN thirteenMonthBack
DECLARE #date DATE = GETDATE()
;WITH MonthsCTE AS (
SELECT 1 [Month], DATEADD(DAY, -DATEPART(DAY, #date)+1, #date) as 'MonthStart'
UNION ALL
SELECT [Month] + 1, DATEADD(MONTH, 1, MonthStart)
FROM MonthsCTE
WHERE [Month] < 12 )
SELECT * FROM MonthsCTE
/*
| The below SELECT statements show TWO examples of how this can be useful.
| Example 1 SELECT: Simple example of showing how to generate 12 days ahead based on date entered
| Example 2 SELECT: This example shows how to generate 12 months ahead based on date entered
| This example tries to mimic as best it can Oracles use of LEVEL and CONNECT BY LEVEL
*/
WITH dynamicRecords(myDate, level) AS
(
SELECT GETDATE() AS myDate, 1 AS level
UNION ALL
SELECT myDate + 1, level + 1 /* 12 Days - WHERE level < 12 */
--SELECT DATEADD(month, 1, myDate), level + 1 /* 12 Months - WHERE level < 12 */
FROM dynamicRecords
WHERE level < 12
)
SELECT *
FROM dynamicRecords
Option (MaxRecursion 0) /* The default MaxRecursion setting is 100. Generating more than 100 dates using this method will require the Option (MaxRecursion N) segment of the query, where N is the desired MaxRecursion setting. Setting this to 0 will remove the MaxRecursion limitation altogether */
Screenshots:
/* Original T-SQL Solution I found here: https://riptutorial.com/sql-server/example/11098/generating-date-range-with-recursive-cte
| The below provides an example of how to generate the days within a date range of the dates entered.
| The below SELECT statements show TWO examples of how this can be useful.
| Example 1 SELECT: Uses static dates to display ALL of the dates within the range for the dates entered
| Example 2 SELECT: This example uses GETDATE() and then obtains the FOM day and the EOM day of the dates
| beging entered to then show all days in the month of the dates entered.
*/
With DateCte AS
(
SELECT CAST('2021-04-21' AS DATE) AS BeginDate, CAST('2022-05-02' AS DATE) AS EndDate
--SELECT CAST( GETDATE() - Day(GETDATE()) + 1 AS DATE ) AS BeginDate, CAST(EOMONTH(GETDATE()) AS DATE) AS EndDate
UNION ALL
SELECT DateAdd(Day, 1, BeginDate), EndDate
FROM DateCte
WHERE BeginDate < EndDate
)
Select BeginDate AS Dates
From DateCte
Option (MaxRecursion 0) /* The default MaxRecursion setting is 100. Generating more than 100 dates using this method will require the Option (MaxRecursion N) segment of the query, where N is the desired MaxRecursion setting. Setting this to 0 will remove the MaxRecursion limitation altogether */
;
Screenshot: