SQL function to populate a simple date table - sql-server

I need to populate a table with simple (Year, month, days) columns for upto 10 years and I am roughly taking every month to have 30 days.It looks like as below.
I have written the below code to populate the table but I have error on the second 'while' it say 'Expecting '(',or select'. Any clue why?
Simple Date table
declare #month varchar(20)
set #month ='1'
declare #day varchar(20)
set #day='1'
declare #sql nvarchar(1000)
while(#Year <=10)
(
while(#month<=12)
(
while(#day<= #day+30)
(
insert into simple_table values(#Year,#month,#day)
#day=#day+1
)
#month+1
)
#year = #year+1
)
)

With the help of an ad-hoc tally table and a Cross Join (or two)
Your sample was a little unclear. This assumes the Day column is not 1-30 but 1-3600 for 10 years. I'm assuming you are building some sort of amortization schedule of 30/360
Select Year = 'Year'+cast(Y as varchar(25))
,Month = M
,Day = Row_Number() over (Order by Y,M,D)
From (Select Top (10) Y=Row_Number() Over (Order By (Select NULL)) From master..spt_values) Y
Cross Join (Select Top (12) M=Row_Number() Over (Order By (Select NULL)) From master..spt_values) M
Cross Join (Select Top (30) D=Row_Number() Over (Order By (Select NULL)) From master..spt_values) D
Returns

Seems like it would be a lot easier to use one loop instead of 3.
Declare #D DateTime;
Set #D = GetDate();
while #D < DateAdd(Year, 10, GetDate())
Begin
Insert Into simple_table(Year, Month, Day) Select Year(#D), Month(#D), Day(#D)
Set #D = DateAdd(Day, 1, #D)
End
Basically, just use a date as your loop variable, increment one day each time through the loop. Additional benefit, you get exactly the right number of days in each month (including leap years).

Related

Get data from the last day of the month without the use of loops or variables

I wrote a query that should select the last record of each month in a year. I'd like to create a View based on this select, that I could run later in my project, but unfortunately I can't use any while loops or variables in a view command. Is there a way to select all these records - last days of a month in a View that I can use later?
My desired effect of the view:
The query that I'm trying to implement in a view:
DECLARE #var_day01 DATETIME;
DECLARE #month int;
SET #month = 1;
DROP TABLE IF EXISTS #TempTable2;
CREATE TABLE #TempTable2 (ID int, date datetime, INP2D float, INP3D float, ID_device varchar(max));
WHILE #month < 13
BEGIN
SELECT #var_day01 = CONVERT(nvarchar, date) FROM (SELECT TOP 1 * FROM data
WHERE DATEPART(MINUTE, CONVERT(nvarchar, date)) = '59'
AND
MONTH(CONVERT(nvarchar, date)) = (CONVERT(nvarchar, #month))
ORDER BY date DESC
) results
ORDER BY date DESC;
INSERT INTO #TempTable2 (ID, date, INP2D,INP3D,ID_device)
SELECT * FROM data
WHERE DATEPART(MINUTE, CONVERT(nvarchar, date)) = '59'
AND
MONTH(CONVERT(nvarchar, date)) = (CONVERT(nvarchar, #month))
AND
DAY(CONVERT(nvarchar, date)) = CONVERT(datetime, DATEPART(DAY, #var_day01))
ORDER BY date DESC
PRINT #var_day01
SET #month = #month +1;
END
SELECT * FROM #TempTable2;
If you are actually just after the single most recent row for each month, there is no need for a while loop to achieve this. You just need to identify the max date value for each month and then filter your source data for those for those rows.
One way to achieve this is via a row_number window function:
declare #t table(id int,dt datetime2);
insert into #t values(1,getdate()-40),(2,getdate()-35),(3,getdate()-25),(4,getdate()-10),(5,getdate());
select id
,id_device
,dt
from(select id
,id_device
,dt
,row_number() over (partition by id_device, year(dt), month(dt) order by dt desc) as rn
from #t
) as d
where rn = 1;
You can add a simple where to your select statement, in where clause you will add one day to the date field and then select the day from the resultant date. If the result date is 1 then only you will select that record
the where clause for your query will be : Where Day(DATEADD(d,1,[date])) = 1

SQL Server stored procedure to add days to a date

I need to write a stored procedure that given a date and a number of working days, adds those given days to that date, and returns the new date, without counting the non-working days and the weekends. the non-working day are stored in another table.
It's my second stored procedure so I'm not quite familiar with the lexic, so, sorry in advance if you find obvious mistakes.
So far I've gotten to this:
CREATE PROCEDURE DateAdd
(#GivenDate DATE, #DaysToAdd int)
DECLARE #ReturnDate DATE,
DECLARE #Counter int,
DECLARE #NextDate DATE
AS
SET #Counter = 0
SET #ReturnDate = #GivenDate
SET #NextDate = #GivenDate
GO
WHILE (#Counter < #DaysToAdd)
#Counter + 1
IF(datepart(weekday, #FechaVariable) !=6 &&
datepart(weekday, #FechaVariable) != 7)
IF(#TODO-- call the query and check it with #NextDate)
#FechaRetorno = DateAdd(dd, 1, #FechaRetorno)
ELSE IF #NextDate = DateAdd(dd, 1, #NextDate)
EN IF
END WHILE
-- I don't know where to put this query, or how to call ir from the IF
SELECT Date
FROM non_working_days
WHERE Date = $Variable
RETURN #FechaRetorno
A Tally/Calendar table would to the trick as well, but you can to do this with an ad-hoc tally table. Also, this approach would be faster than an recursive cte.
One additonal option is that you can exclude HOLIDAYS by adding the following to the WHERE clause.
and D not in ('2017-12-25','2018-01-01')
The SQL
Declare #Date date = '2017-04-01'
Declare #Days int = 5
Select D
From (
Select D,RN=Row_Number() over (Order by D)
From (Select Top ((#Days*2)+10) D=DateAdd(DAY,-1+Row_Number() Over (Order By Number),#Date) From master..spt_values ) A
Where DateName(WEEKDAY,D) not in ('Saturday','Sunday')
) A
Where RN=#Days
Returns
2017-04-07
Here. Assuming the other table is called OtherTable and the column of dates to avoid is called DatesToAvoid, this will add one day at a time to your date, and if that date is not a weekend or in the DatesToAvoid, it will decrement #DaysToAdd. Once #DaysToAdd reaches 0, it stops.
CREATE PROCEDURE DateAddsp(#GivenDate DATE, #DaysToAdd int)
AS
BEGIN
WHILE #DaystoAdd > 0
BEGIN
SET #GivenDate = DATEADD(DAY,1,#GivenDate)
SET #DaysToAdd = CASE
WHEN #GivenDate IN (SELECT DatesToAvoid FROM OtherTable) OR DATEPART(DW,#GivenDate) IN (1,7) /* Saturday or Sunday*/
THEN #DaysToAdd
ELSE #DaysToAdd + 1
END
END
RETURN DATEADD(DW, #DaysToAdd, #GivenDate);
END
You're are overcomplicating things. The function which you need to use is dateadd: https://learn.microsoft.com/en-us/sql/t-sql/functions/dateadd-transact-sql
Try something along the lines of the following:
CREATE PROCEDURE DateAddsp(#GivenDate DATE, #DaysToAdd int)
AS
BEGIN
RETURN DATEADD(DW, #DaysToAdd, #GivenDate);
END
You cannot use dateadd as the name for the stored proc, as it is a reserved word in SQL Server - it is the name of the function which I just utilized above.
Instead of a procedure, this is an in-line table-valued function to get the result of adding working days to a date.
create function dbo.udf_add_working_days (#Date date, #Days int)
returns table with schemabinding as return (
with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, days as (
select top (1000)
[Date]=convert(date,dateadd(day,row_number() over(order by (select 1))-1,#date))
from n as deka cross join n as hecto cross join n as kilo
order by [Date]
)
, working_days as (
select top (#Days)
[Date]
from days
where datename(weekday,[date]) not in ('Saturday','Sunday')
/* -- put your non working days table info here and uncomment this clause
and not exists (
select 1
from dbo.HolidayTable h
where days.[Date] = h.HolidayDate
)
--*/
order by [Date]
)
select top 1 [date]
from working_days
order by [Date] desc
);
go
and you would call it like so:
select [date]
from dbo.udf_add_working_days('20170401',10)
rextester demo: http://rextester.com/ITXB6884
returns:
+------------+
| date |
+------------+
| 2017-04-14 |
+------------+
Or you can call it using dates from a query using cross apply()
select ...
, x.Date as NewWorkDate
from t
dbo.udf_add_working_days (t.[Date], t.[NumberOfDays]) as x
Reference on inline table valued functions
When is a SQL function not a function? "If it’s not inline, it’s rubbish." - Rob Farley
Inline Scalar Functions - Itzik Ben-Gan
Scalar functions, inlining, and performance: An entertaining title for a boring post - Adam Machanic
TSQL User-Defined Functions: Ten Questions You Were Too Shy To Ask - Robert Sheldon

How to efficiently loop through using Sql query

As I have From and To date. Something like below,
BeginDate End Date
1989-01-01 00:00:00.000 2015-12-31 00:00:00.000
I need to loop through until i get the list of all the Date's between those 2 (Begin & End Date's) records. I need to know what will be the efficient way of doing this. I have no clue on how to do this. Any help to this will be highly appreciated.
Thanks
This method uses a generated numbers table and is probably faster than looping.
DECLARE #BeginDate DATETIME = '19890101';
DECLARE #EndDate DATETIME = '20151231';
WITH
E1(N) AS ( SELECT 1 FROM ( VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) DT(N)),
E2(N) AS ( SELECT 1 FROM E1 A, E1 B),
E4(N) AS ( SELECT 1 FROM E2 A, E2 B),
Numbers(N) AS
(
SELECT ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL)) - 1 FROM E4
)
SELECT
N,
DATEADD(D, N, #BeginDate) AS TheDate
FROM Numbers
WHERE N <= DATEDIFF(D, #BeginDate, #EndDate)
You can do this with WHILE loop:
DECLARE #sdt DATE = '1989-01-01'
DECLARE #edt DATE = '2015-12-31'
WHILE #sdt <= #edt
BEGIN
PRINT #sdt
SET #sdt = DATEADD(dd, 1, #sdt )
END
Or with recursive CTE:
DECLARE #sdt DATE = '1989-01-01'
DECLARE #edt DATE = '2015-12-31';
WITH cte
AS ( SELECT #sdt AS sdt
UNION ALL
SELECT DATEADD(dd, 1, sdt)
FROM cte
WHERE DATEADD(dd, 1, sdt) <= #edt
)
SELECT *
FROM cte
OPTION ( MAXRECURSION 10000 )
There is also tally table method as in link provided by #Bridge
Actually the answer is tally tables. But if there is not a big interval the difference will be insignificant.
Something like this should work for your purposes:
DECLARE #sd date = '1989-01-01 00:00:00.000'
, #ed date = '2015-12-31 00:00:00.000'
DECLARE #tt TABLE(
[Date] date
)
WHILE(#sd <= #ed) --Loop which checks each iteration if the date has reached the end
BEGIN
INSERT INTO #tt
SELECT #sd AS Date
SET #sd = DATEADD(dd,1,#sd) --This willl increment the date so you actually advance the loop
END
SELECT * FROM #tt

Passing in Week Day name to get nearest date in SQL

I'm working on a query that deals with a frequency value (i.e. Mondays, Tuesdays, etc. - Think assignments).
So in my query I currently have a result of
jobId:1, personId:100, frequencyVal: 'Mondays'
jobId:2, personId:101, frequencyVal: 'Saturdays'
What I need is the next the 4 future(or current) dates for the frequencyVal.
So if today is 1/3/2015
I would need my result set to be
jobId:1, personId:100, frequencyVal: 'Mondays', futureDates: '1/5,1/12,1/19,1/26'
jobId:2, personId:102, frequencyVal: 'Saturdays', futureDates: '1/3,1/10,1/17,1/24'
I was looking at the following post:
How to find the Nearest (day of the week) for a given date
But that sets it for a specific date. And I'm looking at this being a web application and I want the dates for the current date. So if I try to run this query next Tuesday the future dates for jobId:1 would remove the 1/5 and add the 2/2.
Is there a way to pass in a weekday value to get the next nearest date?
I prefer a calendar table for this kind of query. Actually, I prefer a calendar table over date functions for most queries. Here's a minimal one. The one I use in production has more columns and more rows. (100 years of data is only 37k rows.)
create table calendar (
cal_date date not null primary key,
day_of_week varchar(15)
);
insert into calendar (cal_date) values
('2015-01-01'), ('2015-01-02'), ('2015-01-03'), ('2015-01-04'),
('2015-01-05'), ('2015-01-06'), ('2015-01-07'), ('2015-01-08'),
('2015-01-09'), ('2015-01-10'), ('2015-01-11'), ('2015-01-12'),
('2015-01-13'), ('2015-01-14'), ('2015-01-15'), ('2015-01-16'),
('2015-01-17'), ('2015-01-18'), ('2015-01-19'), ('2015-01-20'),
('2015-01-21'), ('2015-01-22'), ('2015-01-23'), ('2015-01-24'),
('2015-01-25'), ('2015-01-26'), ('2015-01-27'), ('2015-01-28'),
('2015-01-29'), ('2015-01-30'), ('2015-01-31'),
('2015-02-01'), ('2015-02-02'), ('2015-02-03'), ('2015-02-04'),
('2015-02-05'), ('2015-02-06'), ('2015-02-07'), ('2015-02-08'),
('2015-02-09'), ('2015-02-10'), ('2015-02-11'), ('2015-02-12'),
('2015-02-13'), ('2015-02-14'), ('2015-02-15'), ('2015-02-16'),
('2015-02-17'), ('2015-02-18'), ('2015-02-19'), ('2015-02-20'),
('2015-02-21'), ('2015-02-22'), ('2015-02-23'), ('2015-02-24'),
('2015-02-25'), ('2015-02-26'), ('2015-02-27'), ('2015-02-28')
;
update calendar
set day_of_week = datename(weekday, cal_date);
alter table calendar
alter column day_of_week varchar(15) not null;
alter table calendar
add constraint cal_date_matches_dow
check (datename(weekday, cal_date) = day_of_week);
create index day_of_week_ix on calendar (day_of_week);
Set the privileges so that
everyone can select, but
almost nobody can insert new rows, and
even fewer people can delete rows.
(Or write a constraint that can guarantee there are no gaps. I think you can do that in SQL Server.)
You can select the next four Mondays after today with a very simple SQL statement. (The current date is 2015-01-05, which is a Monday.)
select top 4 cal_date
from calendar
where cal_date > convert(date, getdate())
and day_of_week = 'Monday'
order by cal_date;
CAL_DATE
--
2015-01-12
2015-01-19
2015-01-26
2015-02-02
For me, this is a huge advantage. No procedural code. Simple SQL that is obviously right. Big win.
Your sample table
create table #t
(
jobId int,
personId int,
frequencyVal varchar(10)
);
insert into #t values (1,100,'Mondays'),(2,101,'Saturdays');
QUERY 1 : Select nearest 4 week of days in current month for particular week day
-- Gets first day of month
DECLARE #FIRSTDAY DATE=DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0)
;WITH CTE as
(
-- Will find all dates in current month
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 2 : Select next 4 week's dates for particular week day irrelevant of month
;WITH CTE as
(
-- Will find the next 4 week details
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
There's no built-in function to do it. But you can try this, you may place it inside a Scalar-Valued Function:
DECLARE #WeekDay VARCHAR(10) = 'Monday';
DECLARE #WeekDayInt INT;
SELECT #WeekDayInt = CASE #WeekDay
WHEN 'SUNDAY' THEN 1
WHEN 'MONDAY' THEN 2
WHEN 'TUESDAY' THEN 3
WHEN 'WEDNESDAY' THEN 4
WHEN 'THURSDAY' THEN 5
WHEN 'FRIDAY' THEN 6
WHEN 'SATURDAY' THEN 7 END
SELECT CONVERT(DATE, DATEADD(DAY, (DATEPART(WEEKDAY, GETDATE()) + #WeekDayInt) % 7, GETDATE())) AS NearestDate
UPDATE:
Looks like radar was right, here's the solution:
DECLARE #WeekDay VARCHAR(10) = 'Monday';
DECLARE #WeekDayInt INT;
DECLARE #Date DATETIME = GETDATE();
SELECT #WeekDayInt = CASE #WeekDay
WHEN 'SUNDAY' THEN 1
WHEN 'MONDAY' THEN 2
WHEN 'TUESDAY' THEN 3
WHEN 'WEDNESDAY' THEN 4
WHEN 'THURSDAY' THEN 5
WHEN 'FRIDAY' THEN 6
WHEN 'SATURDAY' THEN 7 END
DECLARE #Diff INT = DATEPART(WEEKDAY, #Date) - #WeekDayInt;
SELECT CONVERT(DATE, DATEADD(DAY, CASE WHEN #Diff >= 0 THEN 7 - #Diff ELSE ABS(#Diff) END, #Date)) AS NearestDate
Try this - based on king.code's answer to get the nearest date.
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;
Try this,
DECLARE #YEAR INT=2015
DECLARE #MONTH INT=1
DECLARE #DAY INT=1
DECLARE #DATE DATE = (SELECT DateFromParts(#Year, #Month, #Day))
DECLARE #TOTAL_DAYS INT =(SELECT DatePart(DY, #DATE));
WITH CTE1
AS (SELECT T_DAY=(SELECT DateName(DW, #DATE)),
#DATE AS T_DATE,
#DAY AS T_DDAY
UNION ALL
SELECT T_DAY=(SELECT DateName(DW, DateAdd(DAY, T_DDAY + 1, #DATE))),
DateAdd(DAY, T_DDAY + 1, #DATE) AS T_DATE,
T_DDAY + 1
FROM CTE1
WHERE T_DDAY + 1 <= 364)
SELECT DISTINCT T_DAY,
Stuff((SELECT ',' + CONVERT(VARCHAR(30), T_DATE)
FROM CTE1 A
WHERE A.T_DAY=CTE1.T_DAY AND A.T_DATE > GetDate() AND A.T_DATE<(DATEADD(WEEK,4,GETDATE()))
FOR XML PATH('')), 1, 1, '') AS FUTURE
FROM CTE1
ORDER BY T_DAY
OPTION (MAXRECURSION 365)
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.
declare #t table
(
jobId int,
personId int,
--frequencyVal varchar(10)
frequency_val int,
frequency_day as datename(weekday,frequency_val -1) + 's'
);
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
insert into #t
values
(1,100,1),--'Mondays'),
(2,101,6),--'Saturdays');
(3,101,7),--'Saturdays');
(4,100,2)--'Mondays'),
--select * from #t
;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
)
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
Having seen the use of DATENAME in some of the answers already given, I'd like to point out that return values of DATENAME might vary depending on your current language setting, but you can save the current language setting and ensure usage of us_english so you can be confident to use English weekday names.
Now here is my slightly different approach to get the 4 next dates that fall on a certain (known) weekday, using a user defined table valued function that allows to create a number sequence table (yes this is a pretty dull function, you have to pass MaxValue greater MinValue, but that could be easily enhanced, if needed, but hey, it does the job). Using that function span a table over 28 values (next 28 days should indeed include the next 4 relevant weekdays ;)), apply DATEADD on GETDATE and reduce the result set with WHERE to only those values that have the right weekday:
CREATE FUNCTION GetIntSequence(#MinValue INT, #MaxValue INT)
RETURNS #retSequence TABLE
(
IntValue INT NOT NULL
)
BEGIN
DECLARE #i INT = (SELECT #MinValue)
WHILE #i <= #MaxValue
BEGIN
INSERT INTO #retSequence (IntValue) SELECT #i
SELECT #i = #i + 1
END
RETURN
END
GO
DECLARE #weekDay NVARCHAR(MAX) = 'Monday' --(or Tuesday, wednesday, ...)
--save current language setting
DECLARE #languageBackup NVARCHAR(MAX) = (SELECT ##LANGUAGE)
--ensure us english language setting for reliable weekday names
SET LANGUAGE us_english;
SELECT FourWeeks.SomeDay FROM
(
SELECT
DATEADD(DAY, IntValue, GETDATE()) AS SomeDay
FROM dbo.GetIntSequence(1, 28)
) AS FourWeeks
WHERE DATENAME(WEEKDAY, SomeDay) = #weekDay
--restore old language setting
SET LANGUAGE #languageBackup;
GO
DROP FUNCTION dbo.GetIntSequence

Count days in date range with set of exclusions which may overlap

Given the following example query, what is a sound and performant approach to counting the total days in a date range when also given a set of ranges to exclude, given that those ranges may have dates which overlap?
More simply, I have a table with a set of date ranges where the billing is turned off, I start with a date range (say Jan1 - Jan31) and I need to determine how many billable days occured in that range. Simply a datediff of the days minus a sum of the datediff on the disabled days. However, there is a chance that the disabled date ranges overlap, ie disabled Jan5-Jan8 in one record and Jan7-Jan10 in another record - thus a simple sum would double count Jan7. What is the best way to exclude these overlaps and get an accurage count.
Declare #disableranges table (disableFrom datetime, disableTo datetime)
insert into #disableranges
select '01/05/2013', '01/08/2013' union
select '01/07/2013', '01/10/2013' union
select '01/15/2013', '01/20/2013'
declare #fromDate datetime = '01/01/2013'
declare #toDate datetime = '01/31/2013'
declare #totalDays int = DATEDIFF(day,#fromDate,#toDate)
declare #disabledDays int = (0 /*not sure best way to calc this*/)
select #totalDays - #disabledDays
You can use a recursive CTE to generate dates between #dateFrom and #dateTo. Then compare the dates with the ranges, and find all dates that are in any range. Finally, count the number of rows in the result to get the count of disabled dates (DEMO):
-- recursive CTE to generate dates
;with dates as (
select #fromDate as date
union all
select dateadd(day, 1, date)
from dates
where date < #toDate
)
-- join with disable ranges to find dates in any range
, disabledDates as (
select date from dates D
left join #disableranges R
on D.date >= R.disableFrom and d.Date < R.disableTo
group by date
having count(R.disablefrom) >= 1
)
-- count up the total disabled dates
select #disabledDays=count(*) from disabledDates;
Tried this and working okay as far as I am concerned.
Declare #disableranges table (disableFrom datetime, disableTo datetime)
insert into #disableranges
select '01/05/2013', '01/08/2013' union
select '01/07/2013', '01/10/2013' union
select '01/15/2013', '01/20/2013'
declare #fromDate datetime = '01/01/2013'
declare #toDate datetime = '01/31/2013'
declare #totalDays int = DATEDIFF(day,#fromDate,#toDate) + 1 /*Without +1 it is giving 30 instead of 31*/
declare #disabledDays int = (0 /*not sure best way to calc this*/)
/*Fill temporary table with the given date range.*/
SELECT DATEADD(DAY, nbr - 1, #fromDate) TempDate INTO #Temp
FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY c.object_id ) AS Nbr
FROM sys.columns c
) nbrs
WHERE nbr - 1 <= DATEDIFF(DAY, #fromDate, #toDate)
/*Check how many dates exists in the disableranges table*/
SELECT #disabledDays=count(*) from #Temp t WHERE
EXISTS(SELECT * FROM #disableranges
WHERE t.TempDate BETWEEN disableFrom AND DATEADD(d, -1, disableTo))
select #totalDays /*Output:31*/
select #disabledDays /*Output:10*/
select #totalDays - #disabledDays /*Output:21*/
drop table #Temp
Taken help from the answer https://stackoverflow.com/a/7825036/341117 to fill table with date range

Resources