How can I generate Week ending dates (Saturdays) within a date range - sql

I need to generate either a column in a query or a temp table (not sure which one is required)
so that I can have a list of dates that are on Saturday that fall within a given date range.
This list will be used in a join to associate records with weeks.
What are my options?
Sample Input:
From: 03/01/2013
To: 04/30/2013
Results:
Week Ending
- 03/02/2013
- 03/09/2013
- 03/16/2013
- 03/23/2013
- 03/30/2013
- 04/06/2013
- 04/13/2013
- 04/20/2013
- 04/27/2013
- 05/04/2013
Current code:
create table #TBL7(YEAR INT, WEEKNUMBER INT, STARTDATE DATETIME, ENDDATE DATETIME)
begin
declare #startdate datetime
, #enddate datetime
, #ctr int
SET #startdate = CAST(2013 AS VARCHAR)+ '/01/01'
SET #enddate = CAST(2013 AS VARCHAR) + '/12/31'
SET #ctr = 0
WHILE #enddate >= #startdate
BEGIN
SET #ctr = #ctr + 1
INSERT INTO #TBL7
values(year(#startdate), #ctr, #startdate, #startdate + 6)
SET #startdate = #startdate + 7
END
end
select * from #TBL7

First, create a calendar table. Then you have a very simple query:
select [Date]
from dbo.Calendar
where DayOfWeek = 'Saturday' and [Date] between '20130301' and '20130430'
A calendar table is almost always the best approach to working with dates because you're working with data, not code, so you can see it's correct and there's no cryptic code to maintain.

This is Oracle code. Sorry I do not know how to convert this to SQL SERVER. Should not be very hard. All you need is to use proper functions in place of to_date() and to_char(), and calc the difference between start and end date, e.g. (end_date-start_date)+1:
WITH data(r, some_date) AS
(
SELECT 1 r, to_date('03/01/2013', 'MM/DD/YYYY') some_date FROM dual
UNION ALL
SELECT r+1, to_date('03/01/2013', 'MM/DD/YYYY')+r FROM data WHERE r < 61 -- (end_date-start_date)+1
)
SELECT some_date
, To_Char(some_date, 'DY') wk_day
FROM data
WHERE To_Char(some_date, 'DY') = 'SAT'
/
SOME_DATE WK_DAY
--------------------
3/2/2013 SAT
3/9/2013 SAT
3/16/2013 SAT
3/23/2013 SAT
3/30/2013 SAT
4/6/2013 SAT
4/13/2013 SAT
4/20/2013 SAT
4/27/2013 SAT

This should work:
WITH cteWeeks (WeekEnding) As
(
-- Find the Saturday of the first week.
-- Need to allow for different DATEFIRST settings:
SELECT
CASE
WHEN DatePart(dw, DateAdd(day, ##datefirst, #StartDate)) = 7 THEN #StartDate
ELSE DateAdd(day, 7 - DatePart(dw, DateAdd(day, ##datefirst, #StartDate)), #StartDate)
END
UNION ALL
SELECT
DateAdd(day, 7, WeekEnding)
FROM
cteWeeks
WHERE
WeekEnding < #EndDate
)
SELECT
WeekEnding
FROM
cteWeeks
;
http://www.sqlfiddle.com/#!3/d41d8/12095

Related

difference between two dates without weekends and holidays Sql query ORACLE

I have 2 tables: the 1st one contains the start date and the end date of a purchase order,
and the 2nd table contains year hollidays
-purchase order
-Holidays
I'm tryign to calculate the number of business days between 2 dates without the weekends and the holidays.
the output should be like this:
Start Date | End Date | Business Days
Could you please help me
You can remove the non-weekend holidays with a query like this:
select (t.end_date - t.start_date) - count(c.date)
from table1 t left join
calendar c
on c.date between t1.start_date and t1.end_date and
to_char(c.date, 'D') not in ('1', '7')
group by t.end_date, t.start_date;
Removing the weekend days is then more complication. Full weeks have two weekend days, so that is easy. So a good approximation is:
select (t.end_date - t.start_date) - (count(c.date) +
2 * floor((t.end_date - t.start_date) / 7))
from table1 t left join
calendar c
on c.date between t1.start_date and t1.end_date and
to_char(c.date, 'D') not in ('1', '7')
group by t.end_date, t.start_date;
This doesn't get the day of week, which is essentially if the end date is before the start date, then it is in the following week. However, this logic gets rather complicated the way that Oracle handles day of the week, so perhaps the above approximation is sufficient.
EDIT: I ignored the presence of the Oracle tag and jumped into scripting this for SQL Server. The concept doesn't change though.
To be super accurate, I would create a table whit the following format.
Year int, month int, DaysInMonth int, firstOccuranceOfSunday int
Create a Procedure to extract the weekends from a specific year and month on that table.
CREATE FUNCTION [dbo].[GetWeekendsForMonthYear]
(
#year int,
#month int
)
RETURNS #weekends TABLE
(
[Weekend] date
)
AS
BEGIN
declare #firstsunday int = 0
Declare #DaysInMonth int = 0
Select #DaysInMonth = DaysInMonth, #firstsunday = FirstSunday from Months
Where [Year] = #year and [month] = #month
Declare #FirstSaterday int = #firstsunday - 1
declare #CurrentDay int = 0
Declare #CurrentDayIsSunday bit = 0
if #FirstSaterday !< 1
Begin
insert into #Weekends values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #Firstsaterday -1, 0))))
insert into #Weekends values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #FirstSunday -1, 0))))
set #CurrentDayIsSunday = 1
set #CurrentDay = #firstsunday
END
else
begin
insert into #Weekends values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #FirstSunday -1, 0))))
set #FirstSaterday = #firstsunday + 6
insert into #Weekends values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #Firstsaterday -1, 0))))
set #CurrentDayIsSunday = 0
set #CurrentDay = #FirstSaterday
end
declare #done bit = 0
while #done = 0
Begin
if #CurrentDay <= #DaysInMonth
Begin
If #CurrentDayIsSunday = 1
begin
set #CurrentDay = #CurrentDay + 6
set #CurrentDayIsSunday = 0
if #CurrentDay <= #DaysInMonth
begin
insert into #Weekends Values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #CurrentDay -1, 0))))
end
end
else
begin
set #CurrentDay = #CurrentDay + 1
set #CurrentDayIsSunday = 1
if #CurrentDay <= #DaysInMonth
begin
insert into #Weekends Values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #CurrentDay -1, 0))))
end
end
end
ELSE
begin
Set #done = 1
end
end
RETURN
END
When called and provided with a year and month this will return a list of dates that represent weekends.
Now, using that function, create a procedure to call this function once for every applicable row in a specific date rang and return the values in a temptable.
Note, I'm posting this now so you can see what's going on but I am continuing to work on the code. I will post updates as they arise.
More to come: Get list of weekends(formatted) for a specific daterange, remove any dates from that list which can be found on your holidays table.
Unfortunately, I have to work tomorrow and am off to bed.
This query should produce exact number of business days for each range in purchase table:
with days as (
select rn, sd + level - 1 dt, sd, ed
from (select row_number() over (order by start_date) rn,
start_date sd, end_date ed from purchase_order)
connect by prior rn = rn and sd + level - 1 <= ed
and prior dbms_random.value is not null)
select sd start_date, ed end_date, count(1) business_days
from days d left join holidays h on holiday_date = d.dt
where dt - trunc(dt, 'iw') not in (5, 6) and h.holiday_date is null
group by rn, sd, ed
SQLFiddle demo
For each row in purchase_orders query generates dates from this range (this is done by subquery dates).
Main query checks if this is weekend day or holiday day and counts rest of dates.
Hierarchical query used to generate dates may cause slowdowns if there is big number of data in purchase_orders
or periods are long. In this case preferred way is to create calendar table, as already suggested in comments.
Since you already have a table of holidays you can count the holidays between the starting and ending date and subtract that from the difference in days between your ending and starting date. For the weekends, you either need a table containing weekend days similar to your table of holidays, or you can generate them as below.
with sample_data(id, start_date, end_date) as (
select 1, date '2015-03-06', date '2015-03-7' from dual union all
select 2, date '2015-03-07', date '2015-03-8' from dual union all
select 3, date '2015-03-08', date '2015-03-9' from dual union all
select 4, date '2015-02-07', date '2015-06-26' from dual union all
select 5, date '2015-04-17', date '2015-08-16' from dual
)
, holidays(holiday) as (
select date '2015-01-01' from dual union all -- New Years
select date '2015-01-19' from dual union all -- MLK Day
select date '2015-02-16' from dual union all -- Presidents Day
select date '2015-05-25' from dual union all -- Memorial Day
select date '2015-04-03' from dual union all -- Independence Day (Observed)
select date '2015-09-07' from dual union all -- Labor Day
select date '2015-11-11' from dual union all -- Veterans Day
select date '2015-11-26' from dual union all -- Thanks Giving
select date '2015-11-27' from dual union all -- Black Friday
select date '2015-12-25' from dual -- Christmas
)
-- If your calendar table doesn't already hold weekends you can generate
-- the weekends with these next two subfactored queries (common table Expressions)
, firstweekend(weekend, end_date) as (
select next_day(min(start_date),'saturday'), max(end_date) from sample_data
union all
select next_day(min(start_date),'sunday'), max(end_date) from sample_data
)
, weekends(weekend, last_end_date) as (
select weekend, end_date from firstweekend
union all
select weekend + 7, last_end_date from weekends where weekend+7 <= last_end_date
)
-- if not already in the same table combine distinct weekend an holiday days
-- to prevent double counting (in case a holiday is also a weekend).
, days_off(day_off) as (
select weekend from weekends
union
select holiday from holidays
)
select id
, start_date
, end_date
, end_date - start_date + 1
- (select count(*) from days_off where day_off between start_date and end_date) business_days
from sample_data;
ID START_DATE END_DATE BUSINESS_DAYS
---------- ----------- ----------- -------------
1 06-MAR-2015 07-MAR-2015 1
2 07-MAR-2015 08-MAR-2015 0
3 08-MAR-2015 09-MAR-2015 1
4 07-FEB-2015 26-JUN-2015 98
5 17-APR-2015 16-AUG-2015 85

Get all dates between two dates in SQL Server

How to get all the dates between two dates?
I have a variable #MAXDATE which is storing the maximum date from the table. Now I want to get the all dates between #Maxdate and GETDATE() and want to store these dates in a cursor.
So far I have done as follows:
;with GetDates As
(
select DATEADD(day,1,#maxDate) as TheDate
UNION ALL
select DATEADD(day,1, TheDate) from GetDates
where TheDate < GETDATE()
)
This is working perfectly but when I am trying to store these values in a cursor
SET #DateCurSor = CURSOR FOR
SELECT TheDate
FROM GetDates
Compilation Error
Incorrect syntax near the keyword 'SET'.
How to solve this?
My first suggestion would be use your calendar table, if you don't have one, then create one. They are very useful. Your query is then as simple as:
DECLARE #MinDate DATE = '20140101',
#MaxDate DATE = '20140106';
SELECT Date
FROM dbo.Calendar
WHERE Date >= #MinDate
AND Date < #MaxDate;
If you don't want to, or can't create a calendar table you can still do this on the fly without a recursive CTE:
DECLARE #MinDate DATE = '20140101',
#MaxDate DATE = '20140106';
SELECT TOP (DATEDIFF(DAY, #MinDate, #MaxDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #MinDate)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b;
For further reading on this see:
Generate a set or sequence without loops – part 1
Generate a set or sequence without loops – part 2
Generate a set or sequence without loops – part 3
With regard to then using this sequence of dates in a cursor, I would really recommend you find another way. There is usually a set based alternative that will perform much better.
So with your data:
date | it_cd | qty
24-04-14 | i-1 | 10
26-04-14 | i-1 | 20
To get the quantity on 28-04-2014 (which I gather is your requirement), you don't actually need any of the above, you can simply use:
SELECT TOP 1 date, it_cd, qty
FROM T
WHERE it_cd = 'i-1'
AND Date <= '20140428'
ORDER BY Date DESC;
If you don't want it for a particular item:
SELECT date, it_cd, qty
FROM ( SELECT date,
it_cd,
qty,
RowNumber = ROW_NUMBER() OVER(PARTITION BY ic_id
ORDER BY date DESC)
FROM T
WHERE Date <= '20140428'
) T
WHERE RowNumber = 1;
You can use this script to find dates between two dates. Reference taken from this Article:
DECLARE #StartDateTime DATETIME
DECLARE #EndDateTime DATETIME
SET #StartDateTime = '2015-01-01'
SET #EndDateTime = '2015-01-12';
WITH DateRange(DateData) AS
(
SELECT #StartDateTime as Date
UNION ALL
SELECT DATEADD(d,1,DateData)
FROM DateRange
WHERE DateData < #EndDateTime
)
SELECT DateData
FROM DateRange
OPTION (MAXRECURSION 0)
GO
Just saying...here is a more simple approach to this:
declare #sdate date = '2017-06-25'
, #edate date = '2017-07-24';
with dates_CTE (date) as (
select #sdate
Union ALL
select DATEADD(day, 1, date)
from dates_CTE
where date < #edate
)
select *
from dates_CTE;
Easily create a Table Value Function that will return a table with all dates.
Input dates as string
You can customize the date in the the format you like '01/01/2017' or '01-01-2017' in string formats (103,126 ...)
Try this
CREATE FUNCTION [dbo].[DateRange_To_Table] ( #minDate_Str NVARCHAR(30), #maxDate_Str NVARCHAR(30))
RETURNS #Result TABLE(DateString NVARCHAR(30) NOT NULL, DateNameString NVARCHAR(30) NOT NULL)
AS
begin
DECLARE #minDate DATETIME, #maxDate DATETIME
SET #minDate = CONVERT(Datetime, #minDate_Str,103)
SET #maxDate = CONVERT(Datetime, #maxDate_Str,103)
INSERT INTO #Result(DateString, DateNameString )
SELECT CONVERT(NVARCHAR(10),#minDate,103), CONVERT(NVARCHAR(30),DATENAME(dw,#minDate))
WHILE #maxDate > #minDate
BEGIN
SET #minDate = (SELECT DATEADD(dd,1,#minDate))
INSERT INTO #Result(DateString, DateNameString )
SELECT CONVERT(NVARCHAR(10),#minDate,103), CONVERT(NVARCHAR(30),DATENAME(dw,#minDate))
END
return
end
To execute the function do this:
SELECT * FROM dbo.DateRange_To_Table ('01/01/2017','31/01/2017')
The output will be
01/01/2017 Sunday
02/01/2017 Monday
03/01/2017 Tuesday
04/01/2017 Wednesday
05/01/2017 Thursday
06/01/2017 Friday
07/01/2017 Saturday
08/01/2017 Sunday
09/01/2017 Monday
10/01/2017 Tuesday
11/01/2017 Wednesday
12/01/2017 Thursday
13/01/2017 Friday
14/01/2017 Saturday
15/01/2017 Sunday
16/01/2017 Monday
17/01/2017 Tuesday
18/01/2017 Wednesday
19/01/2017 Thursday
20/01/2017 Friday
21/01/2017 Saturday
22/01/2017 Sunday
23/01/2017 Monday
24/01/2017 Tuesday
25/01/2017 Wednesday
26/01/2017 Thursday
27/01/2017 Friday
28/01/2017 Saturday
29/01/2017 Sunday
30/01/2017 Monday
31/01/2017 Tuesday
This can be considered as bit tricky way as in my situation, I can't use a CTE table, so decided to join with sys.all_objects and then created row numbers and added that to start date till it reached the end date.
See the code below where I generated all dates in Jul 2018. Replace hard coded dates with your own variables (tested in SQL Server 2016):
select top (datediff(dd, '2018-06-30', '2018-07-31')) ROW_NUMBER()
over(order by a.name) as SiNo,
Dateadd(dd, ROW_NUMBER() over(order by a.name) , '2018-06-30') as Dt from sys.all_objects a
You can try this:
SET LANGUAGE SPANISH
DECLARE #startDate DATE = GETDATE() -- Your start date
DECLARE #endDate DATE = DATEADD(MONTH, 16, GETDATE()) -- Your end date
DECLARE #years INT = YEAR(#endDate) - YEAR(#startDate)
CREATE TABLE #TMP_YEARS (
[year] INT
)
-- Get all posible years between the start and end date
WHILE #years >= 0
BEGIN
INSERT INTO #TMP_YEARS
([year])
SELECT YEAR(#startDate) + #years
SET #years = #years - 1
END
;WITH [days]([day]) AS -- Posible days at a month
(
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL -- days lower than 10
SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 UNION ALL SELECT 17 UNION ALL SELECT 18 UNION ALL SELECT 19 UNION ALL -- days lower than 20
SELECT 20 UNION ALL SELECT 21 UNION ALL SELECT 22 UNION ALL SELECT 23 UNION ALL SELECT 24 UNION ALL SELECT 25 UNION ALL SELECT 26 UNION ALL SELECT 27 UNION ALL SELECT 28 UNION ALL SELECT 29 UNION ALL -- days lower than 30
SELECT 30 UNION ALL SELECT 31 -- days higher 30
),
[months]([month]) AS -- All months at a year
(
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
)
SELECT CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, d.[day]))) + CONVERT(VARCHAR, d.[day]) as [date]
FROM #TMP_YEARS a
CROSS JOIN [months] n -- Join all years with all months
INNER JOIN [days] d on DAY(EOMONTH(CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + CONVERT(VARCHAR, DAY(EOMONTH(CAST(CONVERT(VARCHAR, a.[year]) + '-' + CONVERT(varchar, n.[month]) + '-15' AS DATE)))))) >= d.[day] AND -- The number of the day can't be higher than the last day of the current month and the current year
CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, d.[day]))) + CONVERT(VARCHAR, d.[day]) <= ISNULL(#endDate, GETDATE()) AND -- The current date can't be higher than the end date
CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, d.[day]))) + CONVERT(VARCHAR, d.[day]) >= ISNULL(#startDate, GETDATE()) -- The current date should be higher than the start date
ORDER BY a.[year] ASC, n.[month] ASC, d.[day] ASC
The output will be something like this, you can format the date as you like:
2019-01-24
2019-01-25
2019-01-26
2019-01-27
2019-01-28
2019-01-29
2019-01-30
2019-01-31
2019-02-01
2019-02-02
2019-02-03
2019-02-04
2019-02-05
2019-02-06
2019-02-07
2019-02-08
2019-02-09
...
create procedure [dbo].[p_display_dates](#startdate datetime,#enddate datetime)
as
begin
declare #mxdate datetime
declare #indate datetime
create table #daterange (dater datetime)
insert into #daterange values (#startdate)
set #mxdate = (select MAX(dater) from #daterange)
while #mxdate < #enddate
begin
set #indate = dateadd(day,1,#mxdate)
insert into #daterange values (#indate)
set #mxdate = (select MAX(dater) from #daterange)
end
select * from #daterange
end
I listed dates of 2 Weeks later. You can use variable #period OR function datediff(dd, #date_start, #date_end)
declare #period INT, #date_start datetime, #date_end datetime, #i int;
set #period = 14
set #date_start = convert(date,DATEADD(D, -#period, curent_timestamp))
set #date_end = convert(date,current_timestamp)
set #i = 1
create table #datesList(dts datetime)
insert into #datesList values (#date_start)
while #i <= #period
Begin
insert into #datesList values (dateadd(d,#i,#date_start))
set #i = #i + 1
end
select cast(dts as DATE) from #datesList
Drop Table #datesList
This is the method that I would use.
DECLARE
#DateFrom DATETIME = GETDATE(),
#DateTo DATETIME = DATEADD(HOUR, -1, GETDATE() + 2); -- Add 2 days and minus one hour
-- Dates spaced a day apart
WITH MyDates (MyDate)
AS (
SELECT #DateFrom
UNION ALL
SELECT DATEADD(DAY, 1, MyDate)
FROM MyDates
WHERE MyDate < #DateTo
)
SELECT
MyDates.MyDate
, CONVERT(DATE, MyDates.MyDate) AS [MyDate in DATE format]
FROM
MyDates;
Here is a similar example, but this time the dates are spaced one hour apart to further aid understanding of how the query works:
-- Alternative example with dates spaced an hour apart
WITH MyDates (MyDate)
AS (SELECT #DateFrom
UNION ALL
SELECT DATEADD(HOUR, 1, MyDate)
FROM MyDates
WHERE MyDate < #DateTo
)
SELECT
MyDates.MyDate
FROM
MyDates;
As you can see, the query is fast, accurate and versatile.
You can use SQL Server recursive CTE
DECLARE
#MinDate DATE = '2020-01-01',
#MaxDate DATE = '2020-02-01';
WITH Dates(day) AS
(
SELECT CAST(#MinDate as Date) as day
UNION ALL
SELECT CAST(DATEADD(day, 1, day) as Date) as day
FROM Dates
WHERE CAST(DATEADD(day, 1, day) as Date) < #MaxDate
)
SELECT* FROM dates;
declare #start_dt as date = '1/1/2021'; -- Date from which the calendar table will be created.
declare #end_dt as date = '1/1/2022'; -- Calendar table will be created up to this date (not including).
declare #dates as table (
date_id date primary key,
date_year smallint,
date_month tinyint,
date_day tinyint,
weekday_id tinyint,
weekday_nm varchar(10),
month_nm varchar(10),
day_of_year smallint,
quarter_id tinyint,
first_day_of_month date,
last_day_of_month date,
start_dts datetime,
end_dts datetime
)
while #start_dt < #end_dt
begin
insert into #dates(
date_id, date_year, date_month, date_day,
weekday_id, weekday_nm, month_nm, day_of_year, quarter_id,
first_day_of_month, last_day_of_month,
start_dts, end_dts
)
values(
#start_dt, year(#start_dt), month(#start_dt), day(#start_dt),
datepart(weekday, #start_dt), datename(weekday, #start_dt), datename(month, #start_dt), datepart(dayofyear, #start_dt), datepart(quarter, #start_dt),
dateadd(day,-(day(#start_dt)-1),#start_dt), dateadd(day,-(day(dateadd(month,1,#start_dt))),dateadd(month,1,#start_dt)),
cast(#start_dt as datetime), dateadd(second,-1,cast(dateadd(day, 1, #start_dt) as datetime))
)
set #start_dt = dateadd(day, 1, #start_dt)
end
-- sample of the data
select
top 50 *
--into master.dbo.DimDate
from #dates d
order by date_id
DECLARE #FirstDate DATE = '2018-01-01'
DECLARE #LastDate Date = '2018-12-31'
DECLARE #tbl TABLE(ID INT IDENTITY(1,1) PRIMARY KEY,CurrDate date)
INSERT #tbl VALUES( #FirstDate)
WHILE #FirstDate < #LastDate
BEGIN
SET #FirstDate = DATEADD( day,1, #FirstDate)
INSERT #tbl VALUES( #FirstDate)
END
INSERT #tbl VALUES( #LastDate)
SELECT * FROM #tbl

tsql: How to retrieve the last date of each month between given date range

I have two date for example 08/08/2013 and 11/11/2013 and I need last date of each month starting from August to November in a table so that i can iterate over the table to pick those dates individually.
I know how to pick last date for any month but i am stucked with a date range.
kindly help, it will be highly appreciated.
Note : I am using Sql 2008 and date rang could be 1 month , 2 month or 6 month or a year or max too..
You can use CTE for getting all last days of the month within the defined range
Declare #Start datetime
Declare #End datetime
Select #Start = '20130808'
Select #End = '20131111'
;With CTE as
(
Select #Start as Date,Case When DatePart(mm,#Start)<>DatePart(mm,#Start+1) then 1 else 0 end as [Last]
UNION ALL
Select Date+1,Case When DatePart(mm,Date+1)<>DatePart(mm,Date+2) then 1 else 0 end from CTE
Where Date<#End
)
Select * from CTE
where [Last]=1 OPTION ( MAXRECURSION 0 )
DECLARE #tmpTable table (LastDates DATE);
DECLARE #startDate DATE = '01/01/2012'; --1 Jan 2012
DECLARE #endDate DATE = '05/31/2012'; --31 May 2012
DECLARE #tmpEndDate DATE;
SET #startDate = DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,#startDate)+1,1));
SET #tmpEndDate = DATEADD(DAY, 1, #endDate);
WHILE (#startDate <= #tmpEndDate)
BEGIN
INSERT INTO #tmpTable (LastDates) values (DATEADD(DAY, -1, #startDate));
SET #startDate = DATEADD(MONTH, 1, #startDate);
END
SELECT [LastDates] FROM #tmpTable;
Output:
Example: 1
#startDate DATE = '01/01/2012'; --1 Jan 2012
#endDate DATE = '05/31/2012'; --31 May 2012
LastDates
----------
2012-01-31
2012-02-29
2012-03-31
2012-04-30
2012-05-31
Example: 2
#startDate DATE = '11/01/2011'; --1 Nov 2011
#endDate DATE = '03/13/2012'; --13 Mar 2012
LastDates
----------
2011-11-30
2011-12-31
2012-01-31
2012-02-29
I've created a table variable, filled it with all days between #startDate and #endDate and searched for max date in the month.
declare #tmpTable table (dates date)
declare #startDate date = '08/08/2013'
declare #endDate date = '11/11/2013'
while #startDate <= #endDate
begin
insert into #tmpTable (dates) values (#startDate)
set #startDate = DATEADD(DAY, 1, #startDate)
end
select max(dates) as [Last day] from #tmpTable as o
group by datepart(YEAR, dates), datepart(MONTH, dates)
Results:
Last day
2013-08-31
2013-09-30
2013-10-31
2013-11-11
To also get last day of November this can be used before loop:
set #endDate = DATEADD(day, -1, DATEADD(month, DATEDIFF(month, 0, #endDate) + 1, 0))
Following script demonstrates the script to find last day of previous, current and next month.
----Last Day of Previous Month
SELECT DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE()),0))
LastDay_PreviousMonth
----Last Day of Current Month
SELECT DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE())+1,0))
LastDay_CurrentMonth
----Last Day of Next Month
SELECT DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE())+2,0))
LastDay_NextMonth
If you want to find last day of month of any day specified use following script.
--Last Day of Any Month and Year
DECLARE #dtDate DATETIME
SET #dtDate = '8/18/2007'
SELECT DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,#dtDate)+1,0))
LastDay_AnyMonth
ResultSet:
LastDay_AnyMonth
Source - SQL Server Central.
You can use a recursive CTE to do this, note the MAXRECURSION OPTION prevents an infinite loop:
DECLARE #StartDate DATE = '2013-08-08'
DECLARE #EndDate DATE = '2013-11-11'
;WITH dateCTE
AS
(
SELECT CAST(DATEADD(M, 1,DATEADD(d, DAY(#StartDate) * -1, #StartDate)) AS DATE) EndOFMonth
UNION ALL
SELECT CAST(DATEADD(M, 2,DATEADD(d, DAY(EndOFMonth) * -1, EndOFMonth)) AS DATE)
FROM dateCTE
WHERE EndOFMonth < DATEADD(d, DAY(#EndDate) * -1, #EndDate)
)
SELECT *
FROM dateCTE
OPTION (MAXRECURSION 30);
This returns
EndOFMonth
----------
2013-08-31
2013-09-30
2013-10-31
try this
the last row(where) is optional for date filtering
declare #table table
(
thisdate date
)
insert into #table values ('12/01/2013'),('05/06/2013'),('04/29/2013'),('02/20/2013')
select *,DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,thisdate)+1,0))
LastDay from #table
where thisdate between 'givendate' and 'givendate'
The Example Below is for all dates
thisdate lastday
2013-12-01 2013-12-31 23:59:59.000
2013-05-06 2013-05-31 23:59:59.000
2013-04-29 2013-04-30 23:59:59.000
2013-02-20 2013-02-28 23:59:59.000
The following CTE gives you the last day of every month from February 1900 until the middle of the 26th century (on my machine):
;with LastDaysOfMonths as (
select DATEADD(month,
ROW_NUMBER() OVER (ORDER BY so.object_id),
'19000131') as Dt
from sys.objects so,sys.objects so1
)
select * from LastDaysOfMonths
It should be easy enough to use it as part of a larger query or to filter it down to just the dates you want. You can adjust the range of years as needed by changing the constant 19000131. The only important thing to do is make sure that you use a month that has 31 days in it and always have the constant be for day 31.
No need to use a common table expression or anything like that - this simple query will do it:
SELECT DATEADD(d, -1, DATEADD(mm, DATEDIFF(m, 0, DATEADD(m, number, '2013-08-08')) + 1, 0)) AS EndOfMonth
FROM master.dbo.spt_values
WHERE 'P' = type
AND DATEADD(m, number, '2013-08-08') < '2013-11-11';
Although the question is about the last day which #bummi has already answered.
But here is the solution for the first date which might be helpful for someone.
Get the first dates of all the months in-between the #FromDate and #ToDate.
DECLARE #FromDate DATETIME = '2019-08-13'
DECLARE #ToDate DATETIME = '2019-11-25'
;WITH CTE
AS
(
SELECT DATEADD(DAY, -(DAY(#FromDate) - 1), #FromDate) AS FirstDateOfMonth
UNION ALL
SELECT DATEADD(MONTH, 1, FirstDateOfMonth)
FROM CTE
WHERE FirstDateOfMonth < DATEADD(DAY, -(DAY(#ToDate) - 1), #ToDate)
)
SELECT * FROM CTE
Here is the result
--Result
2019-08-01 00:00:00.000
2019-09-01 00:00:00.000
2019-10-01 00:00:00.000
2019-11-01 00:00:00.000

How can I rewrite this as a select statement using group by instead of using a loop

I am revisiting some old code I wrote for a report when I was still very new to SQL (MSSQL). It does what it is supposed to but its not the prettiest or most efficient.
The dummy code below mimics what I currently have in place. Here I am trying to get counts for the number of contracts that are open over the last 5 weeks. For this example a contract is considered open if the start date of the contract happens before of during the given week and the end date happens during or after the given week.
dbo.GetWeekStart(#Date DATETIME, #NumOfWeeks INT, #FirstDayOfWeek CHAR(3)) is a function that will return the first day of each week based on the date provided for a specified number of weeks. ie SELECT * FROM dbo.GetWeekStart('20120719', -2, 'MON') will return the 2 mondays prior to July 19, 2012.
How can I simplify this? I think there is someone to do this without a loop but I have not been able to figure it out.
DECLARE #RunDate DATETIME,
#Index INT,
#RowCount INT,
#WeekStart DATETIME,
#WeekEnd DATETIME
DECLARE #Weeks TABLE
(
WeekNum INT IDENTITY(0,1),
WeekStart DATETIME,
WeekEnd DATETIME
)
DECLARE #Output TABLE
(
WeekStart DATETIME,
OpenContractCount INT
)
SET #RunDate = GETDATE()
INSERT INTO #Weeks (WeekStart, WeekEnd)
SELECT WeekStart,
DATEADD(ss,-1,DATEADD(ww,1,WeekStart))
FROM dbo.[GetWeekStart](#RunDate, -5, 'MON')
SET #RowCount = (SELECT COUNT(*) FROM #Weeks)
SET #Index = 0
WHILE #Index < #RowCount
BEGIN
SET #WeekStart = (SELECT WeekStart FROM #Weeks WHERE WeekNum = #Idx)
SET #WeekEnd = (SELECT WeekEnd FROM #Weeks WHERE WeekNum = #Idx)
INSERT INTO #Output (WeekStart, OpenContractCount)
SELECT #WeekStart,
COUNT(*)
FROM Contracts c
WHERE c.StartDate <= #WeekEnd
AND ISNULL(c.EndDate, GETDATE()) >= #WeekStart
SET #Index = #Index + 1
END
SELECT * FROM #Output
I see no reason why this wouldn't work:
DECLARE #RunDate DATETIME = GETDATE()
SELECT WeekStart, COUNT(*)
FROM Contracts c
INNER JOIN dbo.[GetWeekStart](#RunDate, -5, 'MON')
ON c.StartDate < DATEADD(WEEK, 1, WeekStart)
AND (c.EndDate IS NULL OR c.EndDate >= #WeekStart)
GROUP BY WeekStart
I am not sure how you are generating your dates within your function, just in case you are using a loop/recursive CTE I'll include a query that doesn't use loops/cursors etc.
DECLARE #RunDate DATETIME = GETDATE()
-- SET DATEFIRST AS 1 TO ENSURE MONDAY IS THE FIRST DAY OF THE WEEK
-- CHANGE THIS TO SIMULATE CHANGING YOUR WEEKDAY INPUT TO db
SET DATEFIRST 1
-- SET RUN DATE TO BE THE START OF THE WEEK
SET #RunDate = CAST(DATEADD(DAY, 1 - DATEPART(WEEKDAY, #RunDate), #RunDate) AS DATE)
;WITH Weeks AS
( SELECT TOP 5 -- CHANGE THIS TO CHANGE THE WEEKS TO RUN
DATEADD(WEEK, 1 - ROW_NUMBER() OVER(ORDER BY Object_ID), #RunDate) [WeekStart]
FROM sys.All_Objects
)
SELECT WeekStart, COUNT(*)
FROM Contracts c
INNER JOIN Weeks
ON c.StartDate < DATEADD(WEEK, 1, WeekStart)
AND (c.EndDate IS NULL OR c.EndDate >= #WeekStart)
GROUP BY WeekStart
Did this quick but it should work
/*CTE generates Start & End Dates for 5 weeks
Start Date = Sunday of week # midnight
End Date = Sunday of next week # midnight
*/
WITH weeks
AS ( SELECT DATEADD(ww, -4,
CAST(FLOOR(CAST(GETDATE() - ( DATEPART(dw,
GETDATE()) - 1 ) AS FLOAT)) AS DATETIME)) AS StartDate
UNION ALL
SELECT DATEADD(wk, 1, StartDate)
FROM weeks
WHERE DATEADD(wk, 1, StartDate) <= GETDATE()
)
SELECT w.StartDate ,
COUNT(*) AS OpenContracts
FROM dbo.Contracts c
LEFT JOIN weeks w ON c.StartDate < DATEADD(d, 7, w.StartDate)
AND ISNULL(c.EndDate, GETDATE()) >= w.StartDate
GROUP BY w.StartDate

Return number of weekdays between 2 dates in T-SQL

I have 2 dates and want to know how many weekdays (mon-fri) there are
e.g.
thu jan 1 20xx
fri jan 2 20xx
sat jan 3 20xx
sun jan 4 20xx
mon jan 5 20xx
jan 1, jan 5 would return 3
(can ignore public holidays)
Try
DateDiff(day, #DtA, #DtB) - 2 * DateDiff(Week, #DtA, #DtB)
this may not work exactly, but you can see the idea. Some slight modification will work.
Assuming the dates can't be more than five and a half years from each other (or use your own tally table instead of master..spt_values):
DECLARE #date1 datetime, #date2 datetime;
SET #date1 = '20110901';
SET #date2 = '20110905';
SELECT COUNT(*)
FROM (
SELECT
Date = DATEADD(day, number, #date1)
FROM master..spt_values
WHERE type = 'P'
AND number between 0 AND DATEDIFF(day, #date1, #date2)
) s
WHERE DATENAME(DW, Date) NOT IN ('Saturday', 'Sunday')
try this:
SET DATEFIRST 1
DECLARE #StartDate datetime
,#EndDate datetime
SELECT #StartDate='6/21/2011'
,#EndDate='6/28/2011'
;with AllDates AS
(
SELECT #StartDate AS DateOf, datepart(weekday,getdate()) AS WeekDayNumber
UNION ALL
SELECT DateOf+1, datepart(weekday,DateOf+1)
FROM AllDates
WHERE DateOf<#EndDate
)
SELECT COUNT(*) AS WeekDayCount FROM AllDates WHERE WeekDayNumber<=5
OUTPUT:
WeekDayCount
------------
6
(1 row(s) affected)
If you have a holiday table, you can join it in and remove those as well.
EDIT based on #Ross Watson comment:
SET DATEFIRST 1
DECLARE #StartDate datetime
,#EndDate datetime
SELECT #StartDate='6/21/2011'
,#EndDate='6/28/2011'
;with AllDates AS
(
SELECT #StartDate AS DateOf, datepart(weekday,getdate()) AS WeekDayNumber
UNION ALL
SELECT DateOf+1, (WeekDayNumber+1) % 7
FROM AllDates
WHERE DateOf<#EndDate
)
SELECT COUNT(*) AS WeekDayCount FROM AllDates WHERE WeekDayNumber>0 AND WeekDayNumber<6
--I don't like using "BETWEEN", ">", ">=", "<", and "<=" are more explicit in defining end points
produces same output as original query.
Try the following:
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2011/06/01'
SET #EndDate = '2011/06/31'
SELECT
(DATEDIFF(dd, #StartDate, #EndDate) + 1) -
(DATEDIFF(wk, #StartDate, #EndDate) * 5) -
(
CASE
WHEN DATENAME(dw, #StartDate) in
('Sunday', 'Tuesday', 'Wednesday', 'Turesday', 'Saturday')
THEN 1
ELSE 0
END
) -
(
CASE
WHEN DATENAME(dw, #EndDate) in
('Sunday', 'Tuesday', 'Wednesday', 'Turesday', 'Saturday')
THEN 1
ELSE 0
END
)
Also see if this is helpful
http://blog.sqlauthority.com/2007/06/08/sql-server-udf-function-to-display-current-week-date-and-day-weekly-calendar/
This approach is limited to ~100 days due to recursion. This works for the date ranges i've tested. Same idea above, removed the math and simplified:
BEGIN
SET DATEFIRST 1
DECLARE #StartDate datetime
,#EndDate datetime
SELECT #StartDate='12/16/2015'
,#EndDate='1/8/2016'
;with AllDates AS
(
SELECT #StartDate AS DateOf
UNION ALL
SELECT DateOf+1
FROM AllDates
WHERE DateOf<#EndDate
)
SELECT COUNT(*) AS WeekDayCount
FROM
AllDates
WHERE
datepart(weekday,DateOf) between 1 AND 5
--SELECT DateOf [date], datepart(weekday,DateOf) [day]
--FROM
-- AllDates
--WHERE
-- datepart(weekday,DateOf) between 1 AND 5
END