I'm wondering if someone might be able to troubleshoot my query. I have a simple table that has project savings per month. There are always 12 consecutive months worth or savings, but the first month can vary (e.g.: start from January for 12 months, start from March for 12 months, etc).
I need a report that gets me all savings (by month) for a given year. This means that for some project savings, if the start month is not January, then some of that project savings will fall in a different report year.
So I need a query that will return all months for the current report year, and have zero haves for where a project doesn't have saving values for that month.
I have some projects starting in July, and I'm only getting back those 6 months with their value. That is, the left join back to the date WITH is not outer joining properly. Can someone tell me where I'm going wrong please?
See code below:
DECLARE #MonthEndSnapshot SMALLDATETIME;
SELECT #MonthEndSnapshot = getdate()
DECLARE #StartDate SMALLDATETIME, #EndDate SMALLDATETIME;
SELECT #StartDate = FORMAT(#MonthEndSnapshot, 'yyyy') + '0101', #EndDate = FORMAT(#MonthEndSnapshot, 'yyyy') + '1231';
;WITH d(d) AS
(
SELECT
DATEADD(MONTH, n, DATEADD(MONTH, DATEDIFF(MONTH, 0, #StartDate), 0))
FROM
(SELECT TOP
(DATEDIFF(MONTH, #StartDate, #EndDate) + 1)
n = ROW_NUMBER() OVER (ORDER BY [object_id]) - 1
FROM
sys.all_objects
ORDER BY [object_id]) AS n
)
select
left(datename(month, d.d), 3) as xAxisValueMon,
datepart(mm, d.d) as xAxisValue,
a.ProjectId as ProjectId,
ISNULL(SUM(a.Saving), 0) as yAxisValue
from
d
LEFT OUTER JOIN
(SELECT
mes.ProjectId, mes.Saving, mes.SavingMonth
FROM
dbo.sf_SnapshotMonthEndSaving() mes) AS a ON d.d = a.SavingMonth
group by
a.ProjectId, datename(month, d.d), datepart(mm, d.d)
order by
a.ProjectId, datepart(mm, d.d)
The WITH d(d) part works, and returns 12 month dates (1st month from Jan to Dec).
I also tried the following structure as the query:
;WITH d(d) AS
(
SELECT DATEADD(MONTH, n, DATEADD(MONTH, DATEDIFF(MONTH, 0, #StartDate), 0))
FROM ( SELECT TOP (DATEDIFF(MONTH, #StartDate, #EndDate) + 1)
n = ROW_NUMBER() OVER (ORDER BY [object_id]) - 1
FROM sys.all_objects ORDER BY [object_id] ) AS n
)
select left(datename(month, d.d), 3) as xAxisValueMon,
datepart(mm, d.d) as xAxisValue,
mes.ProjectId as ProjectId,
ISNULL(SUM(mes.Saving), 0) as yAxisValue
from d LEFT OUTER JOIN
dbo.sf_SnapshotMonthEndSaving() mes
ON d.d = mes.SavingMonth
group by mes.ProjectId, datename(month, d.d), datepart(mm, d.d)
order by mes.ProjectId, datepart(mm, d.d)
But same results. The MonthEndSaving table is as follows:
CREATE TABLE [dbo].[MonthEndSaving]
(
[MonthEndSavingId] [int] IDENTITY(1,1) NOT NULL,
[MonthEndSnapshot] [datetime] NOT NULL,
[ProjectId] [int] NOT NULL,
[SavingMonth] [datetime] NOT NULL,
[Saving] [money] NOT NULL,
[DateCreated] [datetime] NOT NULL,
PRIMARY KEY CLUSTERED (MonthEndSavingId)
)
GO
ALTER TABLE MonthEndSaving
ADD CONSTRAINT [ProjectMonthEndSaving]
FOREIGN KEY (ProjectId) REFERENCES [dbo].[Project](ProjectId)
GO
Dang, Laughing Vergil seems to be a faster typist =)
Anyway, the idea is pretty much the same. Your 'error' was that you join each month to ALL the projects in dbo.sf_SnapshotMonthEndSaving(). If one fits, it gets returned for that one only, if two fit, it will show those two etc... but it will NOT repeat for EVERY project. This should.
DECLARE #StartDate datetime = '1 jan 2016',
#EndDate datetime = '1 dec 2016'
;WITH d(FirstDayOfMonth) AS
(
SELECT DATEADD(MONTH, n, DATEADD(MONTH, DATEDIFF(MONTH, 0, #StartDate), 0))
FROM ( SELECT TOP (DATEDIFF(MONTH, #StartDate, #EndDate) + 1)
n = ROW_NUMBER() OVER (ORDER BY [object_id]) - 1
FROM sys.all_objects ORDER BY [object_id] ) AS n
),
RelevantProjects AS
(
SELECT DISTINCT ProjectId
FROM dbo.sf_SnapshotMonthEndSaving() mes
WHERE mes.SavingMonth BETWEEN #StartDate AND #EndDate -- you could also join to d but I think this is faster
),
ProjectsAndDates AS
(
SELECT ProjectID,
FirstDayOfMonth
FROM d
CROSS JOIN RelevantProjects
)
select left(datename(month, d.FirstDayOfMonth), 3) as xAxisValueMon,
datepart(mm, d.FirstDayOfMonth) as xAxisValue,
d.ProjectId as ProjectId,
ISNULL(SUM(mes.Saving), 0) as yAxisValue
from ProjectsAndDates d
LEFT OUTER JOIN [MonthEndSaving] mes -- dbo.sf_SnapshotMonthEndSaving() mes
ON mes.SavingMonth = d.FirstDayOfMonth
AND mes.Project_id = d.ProjectID
group by d.ProjectId, datename(month, d.FirstDayOfMonth), datepart(mm, d.FirstDayOfMonth)
order by d.ProjectId, datepart(mm, d.FirstDayOfMonth)
This code should do what you need:
DECLARE #MonthEndSnapshot SMALLDATETIME;
SELECT #MonthEndSnapshot = getdate()
DECLARE #StartDate SMALLDATETIME, #EndDate SMALLDATETIME;
SELECT #StartDate = FORMAT(#MonthEndSnapshot, 'yyyy') + '0101', #EndDate = FORMAT(#MonthEndSnapshot, 'yyyy') + '1231';
;WITH d(d) AS
(
SELECT DATEADD(MONTH, n, DATEADD(MONTH, DATEDIFF(MONTH, 0, #StartDate), 0))
FROM ( SELECT TOP (DATEDIFF(MONTH, #StartDate, #EndDate) + 1)
n = ROW_NUMBER() OVER (ORDER BY [object_id]) - 1
FROM sys.all_objects ORDER BY [object_id] ) AS n
)
select left(datename(month, d.d), 3) as xAxisValueMon,
datepart(mm, d.d) as xAxisValue,
prj.ProjectId as ProjectId,
ISNULL(SUM(a.Saving), 0) as yAxisValue
from d
CROSS JOIN
(
SELECT DISTINCT mes.ProjectId
FROM dbo.sf_SnapshotMonthEndSaving() mes
) as prj
LEFT OUTER JOIN
(
SELECT mes.ProjectId, mes.Saving, mes.SavingMonth
FROM dbo.sf_SnapshotMonthEndSaving() mes
) as a
ON d.d = a.SavingMonth
AND prj.ProjectID = a.ProjectID
group by prj.ProjectId, datename(month, d.d), datepart(mm, d.d)
order by prj.ProjectId, datepart(mm, d.d)
Related
I need to calculate using SQL Query, how many days within a given range fall into each calendar month.
I have given 2 dates, which define a date range; for example 2020-01-01 to 2020-08-03. I need to find how many days in that range fall in to each month i.e. how many fall into July, and how many into August.
In the example given, the expected result is 31 days in July and 3 days in August.
One approach uses a recusive query. Using date artithmetics, we can build the query so it performs one iteration per month rather than one per day, so this should be a rather efficient approach:
with cte as (
select
datefromparts(year(#dt_start), month(#dt_start), 1) month_start,
1 - day(#dt_start) + day(
case when #dt_end > eomonth(#dt_start)
then eomonth(#dt_start)
else #dt_end
end
) as no_days
union all
select
dateadd(month, 1, month_start),
case when #dt_end > dateadd(month, 2, month_start)
then day(eomonth(dateadd(month, 1, month_start)))
else day(#dt_end)
end
from cte
where dateadd(month, 1, month_start) <= #dt_end
)
select * from cte
Demo on DB Fiddle.
If we set the boundaries as follows:
declare #dt_start date = '2020-07-10';
declare #dt_end date = '2020-09-10';
Then the query returns:
month_start | no_days
:---------- | ------:
2020-07-01 | 22
2020-08-01 | 31
2020-09-01 | 10
You can refer this
;with dates(thedate) as (
select dateadd(yy,years.number,0)+days.number
from master..spt_values years
join master..spt_values days
on days.type='p' and days.number < datepart(dy,dateadd(yy,years.number+1,0)-1)
where years.type='p' and years.number between 100 and 150
-- note: 100-150 creates dates in the year range 2000-2050
-- adjust as required
)
select dateadd(m,datediff(m, 0, d.thedate),0) themonth, count(1)
from dates d
where d.thedate between '2020-01-01' and '2020-08-03'
group by datediff(m, 0, d.thedate)
order by themonth;
Please refer the link below where RichardTheKiwi user given a clear example for your scenario.
SQL Server query for total number of days for a month between date ranges
You can do all the work at the month level rather than the day level -- which should be a bit faster. Here is a method using a recursive CTE:
with cte as (
select #startdate as startdate, #enddate as enddate,
datefromparts(year(#startdate), month(#startdate), 1) as month
union all
select startdate, enddate, dateadd(month, 1, month)
from cte
where dateadd(month, 1, month) < #enddate
)
select month,
(case when month <= startdate and dateadd(month, 1, month) >= enddate
then day(enddate) - day(startdate) + 1
when month <= startdate
then day(eomonth(month)) - day(startdate) + 1
when dateadd(month, 1, month) < enddate
then day(eomonth(month))
when dateadd(month, 1, month) >= enddate
then day(enddate)
end)
from cte;
And the db<>fiddle.
The logic is simpler at the day level:
with cte as (
select #startdate as dte, #enddate as enddate
union all
select dateadd(day, 1, dte), enddate
from cte
where dte < enddate
)
select datefromparts(year(dte), month(dte), 1) as yyyymm, count(*)
from cte
group by datefromparts(year(dte), month(dte), 1)
order by yyyymm
option (maxrecursion 0)
Here is a solution with recursive CTE.
declare #startDate date = '2020-07-01'
declare #endDate date = '2020-08-03'
; WITH cte (n, year, month, daycnt)
AS (
SELECT
0
, DATEPART(year, #startDate)
, DATENAME(MONTH, #startDate)
, DATEPART(day, EOMONTH( #startDate ) ) - DATEPART(day, #startDate ) + 1
UNION ALL
SELECT
n + 1
, DATEPART(year, DATEADD(month, n + 1, #startDate) )
, DATENAME(MONTH, DATEADD(month, n + 1, #startDate) )
, IIF(
n = ( DATEPART(month, #endDate) - DATEPART(month, #startDate) ) + ( DATEPART(year, #endDate) - DATEPART(year, #startDate) ) * 12 - 1
, DATEPART(day, #endDate )
, DATEPART(day, EOMONTH( DATEADD(month, n + 1, #startDate) ) )
)
FROM
cte
WHERE
n <= ( DATEPART(month, #endDate) - DATEPART(month, #startDate) ) + ( DATEPART(year, #endDate) - DATEPART(year, #startDate) ) * 12 - 1
)
SELECT *
FROM cte
ORDER BY n
OPTION (maxrecursion 0)
This could be further simplified with a number function but that would also be essentially be a recursive CTE, though it would definitely look cleaner. But it requires defining a function on top of this SELECT statement.
I am trying to get all dates existing between the current month and the two last months.
For example: today 10-01-2019
With an sql script, I will get all dates between 2018-10-01 and 2019-01-31.
with cte as
(
select getdate() as n
union all
select dateadd(DAY,-1,n) from cte where month(dateadd(dd,-1,n)) < month(DATEADD(month, -3, getdate())) --and month(DATEADD(month, 0, getdate()))
union all
select dateadd(DAY,-1,n) from cte where month(dateadd(dd,-1,n)) > month(DATEADD(month, 0, getdate()))
)
select * from cte
I get
error Msg 530, Level 16, State 1, Line 1
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.
Recursion is not a good approach to this. Performance wise using a recursive cte to increment a counter is the same thing as a cursor. http://www.sqlservercentral.com/articles/T-SQL/74118/
A much better approach is to do this set based. For this task a tally table is ideal. Here is a great article on the topic.
I keep a tally table as a view in my system. It is lightning fast with zero reads.
create View [dbo].[cteTally] as
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), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select N from cteTally
GO
Then for something like this problem it is super simple to use. This will produce the same results with no looping.
declare #endDate datetime = '2019-01-31'
, #tmpDate datetime = '2018-10-01'
select dateadd(day, t.N - 1, #tmpDate)
from cteTally t
where t.N - 1 <= DATEDIFF(day, #tmpDate, #endDate)
--EDIT--
If you need this to be dynamic you can use a little date math. This will get the data from the beginning of 3 months ago through the end of the current month regardless of when you run this. The date logic might be a little tough to decipher if you haven't seen this kind of thing before. Lynn Pettis has a great article on this topic. http://www.sqlservercentral.com/blogs/lynnpettis/2009/03/25/some-common-date-routines/
select dateadd(day, t.N - 1, dateadd(month, -3, dateadd(month, datediff(month, 0, getdate()), 0)))
from cteTally t
where t.N - 1 < datediff(day,dateadd(month, -3, dateadd(month, datediff(month, 0, getdate()), 0)), dateadd(month, datediff(month, 0, getdate()) + 1, 0))
This will work depending on your version of SQL Server.
with cte as
(
select cast(getdate() as date) as n
union all
select dateadd(DAY,-1,n) from cte where dateadd(DAY,-1,n) > (select eomonth(cast(dateadd(month,-4,getdate()) as date)))
)
select *
from cte
order by n desc
option (maxrecursion 200)
with cte as
(
select dateadd(month,1,dateadd(day, -1* day(getdate()) , cast(getdate() as date) ) ) n
union all
select dateadd(day,-1,n) from cte where month(n) + year(n) * 12 >= month(getdate()) + year(getdate()) * 12 -3
),
final as (select * from cte except select top 1 * from cte order by n)
select * from final order by n
OPTION (MAXRECURSION 1000)
or to use dateadd only and avoid the except
with cte as
(
select dateadd(day,-1,dateadd(month,1,dateadd(day, 1 - day(getdate()) , cast(getdate() as date)))) n
union all
select dateadd(day,-1,n) from cte where n > dateadd(month,-3,dateadd(day , 1 - day(getdate()),cast(getdate() as date)))
)
select * from cte order by n
OPTION (MAXRECURSION 1000)
You're hitting the maxrecursion limit. Increase it as an option:
with cte as
(
select getdate() as n
union all
select dateadd(DAY,-1,n) from cte where month(dateadd(dd,-1,n)) < month(DATEADD(month, -3, getdate())) --and month(DATEADD(month, 0, getdate()))
union all
select dateadd(DAY,-1,n) from cte where month(dateadd(dd,-1,n)) > month(DATEADD(month, 0, getdate()))
)
select * from cte
OPTION (MAXRECURSION 1000)
You can use a temp table for this purpose. With a loop, just add the dates you need to the temp table. Check the query below:
create table #temp (thedate date)
declare #i int = 1
declare #tmpDate datetime = dateadd(month,-2,getdate())
while #tmpDate<=getdate()
begin
insert into #temp
values (#tmpDate)
set #tmpDate = dateadd(day,1,#tmpDate)
end
select * from #temp
EDIT: Based on OP's comment, new query:
create table #temp (thedate date)
declare #i int = 1
declare #endDate datetime = '2019-01-31'
declare #tmpDate datetime = '2018-10-01'
while #tmpDate<=#endDate
begin
insert into #temp
values (#tmpDate)
set #tmpDate = dateadd(day,1,#tmpDate)
end
select * from #temp
If you're using SQL 2012+
SELECT
dateadd(dd, number, (dateadd(dd, 1, dateadd(MM, -4, eomonth(getdate()))))) as TheDate
FROM
master..spt_values m1
WHERE
type = 'P'
AND dateadd(dd, number, (dateadd(dd, 1, dateadd(MM, -4, eomonth(getdate())))) ) <= eomonth(getdate())
And for earlier versions of SQL:
SELECT
cast(dateadd(dd, number, dateadd(MM, -3, dateadd(dd, -day(getdate())+1, getdate()))) as date)
FROM
master..spt_values m1
WHERE
type = 'P'
AND dateadd(dd, number, dateadd(MM, -3, dateadd(dd, -day(getdate())+1, getdate()))) <= dateadd(MM, 1, dateadd(dd, -day(getdate()) , getdate()))
we need to get week start and end dates for the month output shown as below:
Week# StartDate EndDate
Week 1 2017-03-01 2017-03-04
Week 2 2017-03-05 2017-03-11
Week 3 2017-03-12 2017-03-18
Week 4 2017-03-19 2017-03-25
Week 5 2017-03-26 2017-03-31
This should work:
declare #first_day_of_month date = '20170301'
declare #days_in_month int = datediff(day, #first_day_of_month, dateadd(month, 1, #first_day_of_month))
;with x as (
select datepart(week, dateadd(day, n-1, #first_day_of_month))+1 wk, dateadd(day, n-1, #first_day_of_month) dy
from (values (1),(2),(3),(4),(5),(6),(7),(8),(9),(10), (11),(12),(13),(14),(15),(16),(17),(18),(19),(20),(21),(22),(23),(24),(25),(26),(27),(28),(29),(30), (31)) as numbers(n)
where n <= #days_in_month
)
select wk - datepart(week, #first_day_of_month) as [Week#], min(dy) as StartDate, max(dy) as EndDate
from x
group by wk
order by wk
If you have Numbers table in the database, you can get rid of the values.
How about this:
Declare #StartDate Date = '2017-03-01'
;With Nums
AS
(
SELECT *
FROM (VALUES (0),(1),(2),(3),(4)) Nums(n)
),
StartDate
AS
(
SELECT #StartDate As StartDate, DatePart(dw,#StartDate) As DayWeekNumber
)
SELECT DATEADD(Week, n, CASE WHEN n = 0 THEN StartDate ELSE DATEADD(Day, DayWeekNumber * -1 + 1, StartDate) END) As StartDate,
CASE WHEN DATEPART(Month, DATEADD(Week, n+1, DATEADD(Day, DayWeekNumber * -1 , StartDate))) = DATEPART(Month, #StartDate)
THEN DATEADD(Week, n+1, DATEADD(Day, DayWeekNumber * -1 , StartDate))
ELSE EOMONTH(#StartDate) END As EndDate
FROM Nums
CROSS JOIN StartDate
Option 1: Non-UDF version
Declare #Date1 date = '2017-03-01'
Declare #Date2 date = '2017-03-31'
Select [Week#]
,StartDate = min(D)
,EndDate = max(D)
From (
Select *,[Week#] = concat('Week ',Dense_Rank() over (Partition By Year(D),Month(D) Order By DatePart(WK,D)))
From (Select Top (DateDiff(DD,#Date1,#Date2)+1) D=DateAdd(DD,-1+Row_Number() Over (Order By Number),#Date1) From master..spt_values ) A1
) A
Group By [Week#],Year(D),Month(D)
Order By 2
Option 2: TFV Version
I'll often use a TVF to create dynamic date/time ranges. A tally/calendar table would do the trick as well, but the UDF offers some dynamic options. You supply the Date Range, DatePart and Increment
Declare #Date1 date = '2017-03-01'
Declare #Date2 date = '2017-03-31'
Select [Week#]
,StartDate = min(RetVal)
,EndDate = max(RetVal)
From (
Select *,[Week#] = concat('Week ',Dense_Rank() over (Partition By Year(RetVal),Month(RetVal) Order By DatePart(WK,RetVal)))
From [dbo].[udf-Range-Date](#Date1,#Date2,'DD',1)
) A
Group By [Week#],Year(RetVal),Month(RetVal)
Order By 2
Both would Return
Week# StartDate EndDate
Week 1 2017-03-01 2017-03-04
Week 2 2017-03-05 2017-03-11
Week 3 2017-03-12 2017-03-18
Week 4 2017-03-19 2017-03-25
Week 5 2017-03-26 2017-03-31
The UDF if interested
CREATE FUNCTION [dbo].[udf-Range-Date] (#R1 datetime,#R2 datetime,#Part varchar(10),#Incr int)
Returns Table
Return (
with cte0(M) As (Select 1+Case #Part When 'YY' then DateDiff(YY,#R1,#R2)/#Incr When 'QQ' then DateDiff(QQ,#R1,#R2)/#Incr When 'MM' then DateDiff(MM,#R1,#R2)/#Incr When 'WK' then DateDiff(WK,#R1,#R2)/#Incr When 'DD' then DateDiff(DD,#R1,#R2)/#Incr When 'HH' then DateDiff(HH,#R1,#R2)/#Incr When 'MI' then DateDiff(MI,#R1,#R2)/#Incr When 'SS' then DateDiff(SS,#R1,#R2)/#Incr End),
cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (Select M from cte0) Row_Number() over (Order By (Select NULL)) From cte1 a, cte1 b, cte1 c, cte1 d, cte1 e, cte1 f, cte1 g, cte1 h ),
cte3(N,D) As (Select 0,#R1 Union All Select N,Case #Part When 'YY' then DateAdd(YY, N*#Incr, #R1) When 'QQ' then DateAdd(QQ, N*#Incr, #R1) When 'MM' then DateAdd(MM, N*#Incr, #R1) When 'WK' then DateAdd(WK, N*#Incr, #R1) When 'DD' then DateAdd(DD, N*#Incr, #R1) When 'HH' then DateAdd(HH, N*#Incr, #R1) When 'MI' then DateAdd(MI, N*#Incr, #R1) When 'SS' then DateAdd(SS, N*#Incr, #R1) End From cte2 )
Select RetSeq = N+1
,RetVal = D
From cte3,cte0
Where D<=#R2
)
/*
Max 100 million observations -- Date Parts YY QQ MM WK DD HH MI SS
Syntax:
Select * from [dbo].[udf-Range-Date]('2016-10-01','2020-10-01','YY',1)
Select * from [dbo].[udf-Range-Date]('2016-01-01','2017-01-01','MM',1)
*/
For a one-off use, this would work:
rextester: http://rextester.com/WCMZRW61517
declare #FromDate date = '20170301';
with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, d as (
select DateValue=convert(date,dateadd(day
, row_number() over (order by (select 1)) -1, #FromDate))
from n as deka
cross join n as hecto
)
select
[Week#] = dense_rank() over (
partition by dateadd(month, datediff(month, 0, DateValue) , 0)
order by convert(tinyint,datepart(week,DateValue))
)
, StartDate = min(DateValue)
, EndDate = max(DateValue)
from d
where dateadd(month, datediff(month, 0, DateValue) , 0) = #FromDate
group by
dateadd(month, datediff(month, 0, DateValue) , 0)
, convert(tinyint,datepart(week,DateValue))
If you want to have a calendar table you can reference as needed, something like this would work:
if object_id('dbo.Calendar') is not null drop table dbo.Calendar;
create table dbo.Calendar (
[Date] date not null
, [Year] smallint not null
, [Month] tinyint not null
, MonthStart date not null
, MonthEnd date not null
, [Week] tinyint not null
, MonthWeek tinyint not null
, MonthWeekStart date not null
, MonthWeekEnd date not null
, constraint pk_Calendar primary key clustered (date)
);
declare #FromDate date = '20170101';
declare #ThruDate date = '20171231';
with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, d as (
select DateValue=convert(date,dateadd(day
, row_number() over (order by (select 1)) -1, #fromdate))
from n as deka
cross join n as hecto
cross join n as kilo /* 2.73 years */
cross join n as [tenK] /* 27.3 years */
--cross join n as [hundredk] /* 273 years */
)
insert into dbo.Calendar
([Date], [Year], [Month],MonthStart, MonthEnd, [Week]
, MonthWeek, MonthWeekStart, MonthWeekEnd)
select top (datediff(day, #FromDate, #ThruDate)+1)
[Date] = DateValue
, [Year] = convert(smallint,datepart(year,DateValue))
, [Month] = convert(tinyint,datepart(month,DateValue))
, MonthStart = dateadd(month, datediff(month, 0, DateValue) , 0)
, MonthEnd = convert(date,dateadd(day,-1
,dateadd(Month,datediff(Month,0,DateValue) +1,0) ) )
, [Week] = convert(tinyint,datepart(week,DateValue))
, MonthWeek = dense_rank() over (
partition by dateadd(month, datediff(month, 0, DateValue) , 0)
order by convert(tinyint,datepart(week,DateValue))
)
, MonthWeekStart = min(DateValue) over (
partition by dateadd(month, datediff(month, 0, DateValue) , 0)
, convert(tinyint,datepart(week,DateValue))
)
, MonthWeekEnd = max(DateValue) over (
partition by dateadd(month, datediff(month, 0, DateValue) , 0)
, convert(tinyint,datepart(week,DateValue))
)
from d
order by DateValue;
select distinct
MonthWeek
, MonthWeekStart
, MonthWeekEnd
from dbo.Calendar
where MonthStart = '20170301'
Calendar and Numbers Tables
Generate a set or sequence without loops - Aaron Bertrand
Creating a Date Table/Dimension in SQL Server 2008 - David Stein
Calendar Tables - Why You Need One - David Stein
Creating a date dimension or calendar table in SQL Server - Aaron Bertrand
TSQL Function to Determine Holidays in SQL Server - Tim Cullen
F_TABLE_DATE - Michael Valentine Jones
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
I'm a bit stumped how I might go about this.
I have a very basic query, that currently returns sales for each product, by year and month.
It is grouping by year/month, and summing up the quantity.
This returns one row for each product/year/month combo where there was a sale.
If there was no sale for a month, then there is no data.
I'd like my query to return one row of data for each product for each year/month in my date range, regardless of whether there was actually an order.
If there was no order, then I can return 0 for that product/year/month.
Below is my example query.
Declare #DateFrom datetime, #DateTo Datetime
Set #DateFrom = '2012-01-01'
set #DateTo = '2013-12-31'
select
Convert(CHAR(4),order_header.oh_datetime,120) + '/' + Convert(CHAR(2),order_header.oh_datetime,110) As YearMonth,
variant_detail.vad_variant_code,
sum(order_line_item.oli_qty_required) as 'TotalQty'
From
variant_Detail
join order_line_item on order_line_item.oli_vad_id = variant_detail.vad_id
join order_header on order_header.oh_id = order_line_item.oli_oh_id
Where
(order_header.oh_datetime between #DateFrom and #DateTo)
Group By
Convert(CHAR(4),order_header.oh_datetime,120) + '/' + Convert(CHAR(2),order_header.oh_datetime,110),
variant_detail.vad_variant_code
You can generate this by using CTE.
You will find information on this article :
http://blog.lysender.com/2010/11/sql-server-generating-date-range-with-cte/
Especially this piece of code :
WITH CTE AS
(
SELECT #start_date AS cte_start_date
UNION ALL
SELECT DATEADD(MONTH, 1, cte_start_date)
FROM CTE
WHERE DATEADD(MONTH, 1, cte_start_date) <= #end_date
)
SELECT *
FROM CTE
Thank your for your suggestions.
I managed to get this working using another method.
Declare #DateFrom datetime, #DateTo Datetime
Set #DateFrom = '2012-01-01'
set #DateTo = '2013-12-31'
select
YearMonthTbl.YearMonth,
orders.vad_variant_code,
orders.qty
From
(SELECT Convert(CHAR(4),DATEADD(MONTH, x.number, #DateFrom),120) + '/' + Convert(CHAR(2),DATEADD(MONTH, x.number, #DateFrom),110) As YearMonth
FROM master.dbo.spt_values x
WHERE x.type = 'P'
AND x.number <= DATEDIFF(MONTH, #DateFrom, #DateTo)) YearMonthTbl
left join
(select variant_Detail.vad_variant_code,
sum(order_line_item.oli_qty_required) as 'Qty',
Convert(CHAR(4),order_header.oh_datetime,120) + '/' + Convert(CHAR(2),order_header.oh_datetime,110) As 'YearMonth'
FROM order_line_item
join variant_detail on variant_detail.vad_id = order_line_item.oli_vad_id
join order_header on order_header.oh_id = order_line_item.oli_oh_id
Where
(order_header.oh_datetime between #DateFrom and #DateTo)
GROUP BY variant_Detail.vad_variant_code,
Convert(CHAR(4),order_header.oh_datetime,120) + '/' + Convert(CHAR(2),order_header.oh_datetime,110)
) as Orders on Orders.YearMonth = YearMonthTbl.YearMonth
This is what I put together. It will certainly need some debugging, but I think that this will lead you in the right direction. I broke up the queries into different parts in order to attempt to make it easier to read. Hope this helps.
DECLARE #dateFrom DATETIME, #dateTo DATETIME
SELECT #dateFrom = MIN(oh_datetime) FROM order_header
SELECT #dateTo = MAX(oh_datetime) FROM order_header
;WITH
y AS
(
SELECT YEAR(#dateFrom) AS [Year]
UNION ALL
SELECT [Year] + 1
FROM y
WHERE
[Year] < YEAR (GETDATE())
),
m AS
(
SELECT 1 AS [Month]
UNION ALL
SELECT [Month] + 1
FROM m
WHERE
[Month] < 12
),
dates AS
(
SELECT
CAST(y.[Year] AS nvarchar(4)) + N'/' + RIGHT(N'00' + CAST(m.[Month] AS nvarchar(2)), 2) AS YearMonth
FROM
y CROSS JOIN m
),
qty AS
(
SELECT
YEAR(oh.oh_datetime) + N'/' + MONTH(oh.oh_datetime) AS YearMonth,
v.vad_variant_code,
oli.oli_qty_required AS Qty
FROM
variant_Detail AS v
INNER JOIN order_line_item AS oli ON oil.oli_vad_id = v.vad_id
INNER JOIN order_header AS oh ON oh.oh_id = oli.oli_oh_id
)
SELECT
d.YearMonth,
qty.vad_variant_code,
SUM(qty.Qty) AS TotalQty
FROM
dates AS d LEFT OUTER JOIN qty
ON d.YearMonth = qty.YearMonth
GROUP BY
d.YearMonth,
qty.vad_variant_code
Here is another twist, if you find all months of a year
;WITH DateYear AS (
SELECT 0 AS num
UNION ALL
SELECT num + 1 FROM DateYear
WHERE num < 11
)
Select FirstDateOfTheMonth, DATENAME(mm,FirstDateOfTheMonth), num from
(SELECT CONVERT(DATE,DATEADD(MONTH,num,'2017')) AS FirstDateOfTheMonth, num from DateYear)
cte
and the result will be
Another twist:
Declare #dateFrom datetime ='2019-03-21', #dateTo datetime ='2019-12-31'
;WITH CTE AS
(
SELECT #dateFrom AS cte_start_date
UNION ALL
SELECT DATEADD(MONTH, 1, cte_start_date)
FROM CTE
WHERE ( DATEADD(MONTH, 1, cte_start_date) <= EOMONTH( #dateTo) )
--or ( DATENAME(MONTH, cte_start_date) =DATENAME(MONTH, #dateTo) and DATENAME(year, cte_start_date) =DATENAME(year, #dateTo) ) )
)
SELECT *
FROM CTE
This below is work for sqlserver 2012 and above to get the last day of the month :-
Select EOMONTH('2020-02-15')