Include missing months in Group By query - sql-server

I think I have a tough one here... :(
I am trying to get an order count by month, even when zero. Here's the problem query:
SELECT datename(month, OrderDate) as Month, COUNT(OrderNumber) AS Orders
FROM OrderTable
WHERE OrderDate >= '2012-01-01' and OrderDate <= '2012-06-30'
GROUP BY year(OrderDate), month(OrderDate), datename(month, OrderDate)
What I'm looking to get is something like this:
Month Orders
----- ------
January 10
February 7
March 0
April 12
May 0
June 5
...but my query skips a row for March and May. I've tried COALESCE(COUNT(OrderNumber), 0) and ISNULL(COUNT(OrderNumber), 0) but I'm pretty sure the grouping is causing that not to work.

This solution doesn't require you to hard-code the list of months you might want, all you need to do is provide any start date and any end date, and it will calculate the month boundaries for you. It includes year in the output so that it will support more than 12 months and so that your start and end dates can cross a year boundary and still order correctly and show the correct month and year.
DECLARE #StartDate SMALLDATETIME, #EndDate SMALLDATETIME;
SELECT #StartDate = '20120101', #EndDate = '20120630';
;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
[Month] = DATENAME(MONTH, d.d),
[Year] = YEAR(d.d),
OrderCount = COUNT(o.OrderNumber)
FROM d LEFT OUTER JOIN dbo.OrderTable AS o
ON o.OrderDate >= d.d
AND o.OrderDate < DATEADD(MONTH, 1, d.d)
GROUP BY d.d
ORDER BY d.d;

Since your query Just Can't guess the months you want, you will need to have the Months that you want stored in somewhere, Join them with your table, and then group.
Something like:
;With Months (Month)
AS
(
select 'January' as Month
UNION
select 'February' as Month
UNION
select 'March' as Month
UNION
select 'April' as Month
UNION
select 'May' as Month
UNION
select 'June' as Month
UNION
select 'July' as Month
UNION
select 'August' as Month
UNION
select 'September' as Month
UNION
select 'October' as Month
UNION
select 'November' as Month
UNION
select 'December' as Month
)
--Also you could have them in a "Months" Table
Then Just JOIN this table with your table:
Select
SELECT datename(month, OrderDate) as Month, COUNT(OrderNumber)
FROM Months T1
LEFT JOIN OrderTable T2 on datename(month, T2.OrderDate) = T2.Month
WHERE (T2.OrderDate >= '2012-01-01' and T2.OrderDate <= '2012-06-30')
OR T2.OrderDate IS NULL --So will show you the months with no rows
GROUP BY year(T2.OrderDate), month(T2.OrderDate), datename(month, T2.OrderDate)
Hope it works!

Here is one using recursive CTE:
declare #StartDate datetime = '2015-04-01';
declare #EndDate datetime = '2015-06-01';
-- sample data
declare #orders table (OrderNumber int, OrderDate datetime);
insert into #orders
select 11, '2015-04-02'
union all
select 12, '2015-04-03'
union all
select 13, '2015-05-03'
;
-- recursive CTE
with dates
as (
select #StartDate as reportMonth
union all
select dateadd(m, 1, reportMonth)
from dates
where reportMonth < #EndDate
)
select
reportMonth,
Count = count(o.OrderNumber)
from dates
left outer join #orders as o
on o.OrderDate >= reportMonth
and o.OrderDate < dateadd(MONTH, 1, reportMonth)
group by
reportMonth
option (maxrecursion 0);
;

Related

Adding the total number of distinct customers to a cte table in Microsoft SQL Server

I am using Microsoft SQL Server and am trying to achieve the following
Date
Distinct Customers last 30Days
2020-12-01
20000
2020-12-02
23000
What I am trying to get is that between 2020-11-01 and 2020-12-01 I had 20000 distinct customers.
I have created a cte table with the List of Dates as can be seen below:
WITH listdate AS
(
SELECT CAST('2020-11-01' AS datetime) DateValue
UNION ALL
SELECT DateValue + 1
FROM listdate
WHERE DateValue + 1 < getdate()
)
SELECT
cast(DateValue as date) as DateValue
FROM listdate d
Now I am trying to join the customer and usage table with the list of dates table, however, I am not getting the correct end result. The following is what I have tried doing:
WITH listdate AS
(
SELECT CAST('2020-11-01' AS datetime) DateValue
UNION ALL
SELECT DateValue + 1
FROM listdate
WHERE DateValue + 1 < getdate()
)
SELECT
cast(DateValue as date) as DateValue
,count(distinct case when m.CallDate between dateadd(dd,-30,cast(d.datevalue as date)) and cast(d.datevalue as date) then m.Customerid end) as Distinct_CID
FROM listdate d
join Usage m on d.DateValue=m.CallDate
left join Customer c on c.CustomerID=m.Customer
where c.customertype = 'type A'
group by d.DateValue
OPTION (MAXRECURSION 0)
Can someone maybe suggest a different way of how to solve such a query?
Thanks
I would go for a lateral join to bring the count of distinct customers for the last 30 days:
with listdate as (
select cast('20201101' as date) as datevalue
union all
select dateadd(day, 1, datevalue) from listdate where datevalue < cast(getdate() as date)
)
select ld.datevalue, x.cnt
from listdate ld
cross apply (
select count(distinct c.customerid) as cnt
from usage u
inner join customer c on c.customerid = u.customerid
where
c.customertype = 'type A'
and c.calldate >= dateadd(day, -29, datevalue)
and c.calldate < dateadd(day, 1, datevalue)
) x
option (maxrecursion 0)
Note that I simplified the parts related to dates: this uses proper literal dates and date arithmetics in the recursive query; the where clause of the subquery implements what I understand as the last 30 days (today + the preceding 29 days), and properly handles the time portion of calldate, if any.

Generate counts of records for each day in a week

I use SQL Server 2014 for my project. I have the following code to produce the number of registrations of each day:
SELECT
DATEADD(DAY, DATEDIFF(DAY, 0, createTime), 0) AS createdOn,
COUNT(*) AS Count
FROM
Registration
GROUP BY
DATEADD(DAY, DATEDIFF(DAY, 0, createTime), 0)
ORDER BY
createdOn
Now I would like to get the numbers for each day in a week (so there will be max 7 rows in output). How can I do it?
Here is the solution I have based on George's comment. Thank you, George!
SELECT
DATEPART(weekday, createTime) AS createdOn,
COUNT(*) AS Count
FROM
Registration
GROUP BY
DATEPART(weekday, createTime)
ORDER BY
createdOn
One way to return all days within a range returned with your data joined on matching days is to use a "calendar" table and LEFT JOIN your data by date.
DECLARE #StartDate DATETIME = '01/01/2015'
DECLARE #EndDate DATETIME = '12/01/2016'
//By Day In Year
;WITH Calender as
(
SELECT CalendarDate = #StartDate
UNION ALL
SELECT CalendarDate = DATEADD(DAY, 1, CalendarDate)
FROM Calender WHERE DATEADD (DAY, 1, CalendarDate) <= #EndDate
)
SELECT
C.CalendarDate,
COUNT(*) AS Count
FROM
Calender C
LEFT JOIN Regsitration R ON R.createdOn = C.CalendarDate
GROUP BY
C.CalendarDate
OPTION (MAXRECURSION 0)
//By Week In Year
;WITH Calender as
(
SELECT CalendarDate = #StartDate, WeekNumber=DATEPART(WEEK, #StartDate)
UNION ALL
SELECT CalendarDate = DATEADD(WEEK, 1, CalendarDate), WeekNumber=DATEPART(WEEK, #StartDate)
FROM Calender WHERE DATEADD (WEEK, 1, CalendarDate) <= #EndDate
)
SELECT
C.WeekNumber,
COUNT(*) AS Count
FROM
Calender C
LEFT JOIN Regsitration R ON DATEPART(WEEK,R.createdOn) = C.WeekNumber
GROUP BY
C.WeekNumber
OPTION (MAXRECURSION 0)

When an UPDATE statement has access to multiple values, which one does it choose?

I didn't think this code would work, and now that is seems to be working I'm worried that it might not always work:
IF OBJECT_ID('tempdb..#Dates') IS NOT NULL DROP TABLE #Dates
; WITH Dates
AS (
SELECT CAST(DATEADD(yy, DATEDIFF(yy, 0, GETDATE()), 0) AS DATE) AS CDate
UNION ALL
SELECT DATEADD(dd, 1, CDate) AS CDate
FROM Dates
WHERE DATEADD(dd, 1, CDate) < CAST(GETDATE() AS DATE)
)
SELECT d.CDate
, d.CDate AS BDate
INTO #Dates
FROM Dates d
OPTION (MAXRECURSION 400)
; WITH BDate
AS (
SELECT CDate
FROM #Dates d
WHERE CDate NOT IN ('2018-01-01', '2018-01-15', '2018-02-19') -- New Years, MLK Day, Presidents Day
AND DATEPART(dw, d.CDate) NOT IN (1,7)
)
UPDATE d
SET d.BDate = b.CDate
FROM #Dates d
JOIN BDate b
ON d.CDate <= b.CDate
SELECT * FROM #Dates
What I don't understand is how it knows which value to choose for the UPDATE statement. If we choose a random date like January 24th, a SELECT statement would give us multiple values (since that's a Wednesday and we're avoiding weekends, that list would include January 24th, 25th and 26th, and then jump to 29th, 30th, 31st, etc.). So why is it choosing the one I actually want (the minimum value that meets the criteria)? It won't let me use ORDER BY to force the order, of course.
Normally I would write my code more like this:
IF OBJECT_ID('tempdb..#Dates') IS NOT NULL DROP TABLE #Dates
; WITH Dates
AS (
SELECT CAST(DATEADD(yy, DATEDIFF(yy, 0, GETDATE()), 0) AS DATE) AS CDate
UNION ALL
SELECT DATEADD(dd, 1, CDate) AS CDate
FROM Dates
WHERE DATEADD(dd, 1, CDate) < CAST(GETDATE() AS DATE)
)
SELECT d.CDate
, d.CDate AS BDate
INTO #Dates
FROM Dates d
OPTION (MAXRECURSION 400)
; WITH BDate
AS (
SELECT CDate
FROM #Dates d
WHERE CDate NOT IN ('2018-01-01', '2018-01-15', '2018-02-19') -- New Years, MLK Day, Presidents Day
AND DATEPART(dw, d.CDate) NOT IN (1,7)
)
, BDMin
AS (
SELECT d.CDate
, MIN(b.CDate) AS BDate
FROM #Dates d
JOIN BDate b
ON d.CDate <= b.CDate
GROUP BY d.CDate
)
UPDATE d
SET d.BDate = b.BDate
FROM #Dates d
JOIN BDMin b
ON d.CDate = b.CDate
SELECT * FROM #Dates
It certainly seems safer, but now I'm wondering if it's necessary.

How To Count Rows Based on Values of Two Variables in SSIS

I am fairly new to SSIS, and now I have this requirement to exclude weekends in order to do a performance management. Now I have created a calendar and marked the weekends; what I am trying to do, using SSIS, is get the start and end date of every status and count how many weekends are there. I am kind of struggling to know which component to use to achieve this task.
So I have mainly two tables:
1- Table Calendar
2- Table History-Log
Calendar has the following columns:
1- ID
2- date
3- year
4- month
5- day of week
6- isweekend
History-Log has the following:
1- ID
2- Status
3- startdate
4- enddate
Your help is really appreciated.
I'm not an SSIS user, so apologies if this answer does not help, but if I wanted to get the result you describe, based on some test data:
DECLARE #Calendar TABLE (
ID INT,
[Date] DATETIME,
[Year] INT,
[Month] INT,
[DayOfWeek] VARCHAR(10),
IsWeekend BIT
)
DECLARE #HistoryLog TABLE (
ID INT,
[Status] INT,
StartDate DATETIME,
EndDate DATETIME
)
DECLARE #StartDate DATE = '20100101', #NumberOfYears INT = 10
DECLARE #CutoffDate DATE = DATEADD(YEAR, #NumberOfYears, #StartDate);
INSERT INTO #Calendar
SELECT ROW_NUMBER() OVER (ORDER BY d) AS ID,
d AS [Date],
DATEPART(YEAR,d) AS [Year],
DATEPART(MONTH,d) AS [Month],
DATENAME(WEEKDAY,d) AS [DayOfWeek],
CASE WHEN DATENAME(WEEKDAY,d) IN ('Saturday','Sunday') THEN 1 ELSE 0 END AS IsWeekend
FROM
(
SELECT d = DATEADD(DAY, rn - 1, #StartDate)
FROM
(
SELECT TOP (DATEDIFF(DAY, #StartDate, #CutoffDate))
rn = ROW_NUMBER() OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
ORDER BY s1.[object_id]
) AS x
) AS y;
INSERT INTO #HistoryLog
SELECT 1, 3, '2016-01-05', '2016-01-20'
UNION
SELECT 2, 7, '2016-01-08', '2016-01-25'
UNION
SELECT 3, 4, '2016-01-01', '2016-02-03'
UNION
SELECT 4, 3, '2016-02-09', '2016-02-10'
I would use a query like this to return all of the HistoryLog records with a count of the number of weekend days between their StartDate and EndDate:
SELECT h.ID,
h.[Status],
h.StartDate,
h.EndDate,
COUNT(c.ID) AS WeekendDays
FROM #HistoryLog h
LEFT JOIN #Calendar c ON c.[Date] >= h.StartDate AND c.[Date] <= h.EndDate AND c.IsWeekend = 1
GROUP BY h.ID, h.[Status], h.StartDate, h.EndDate
ORDER BY 1
If you wanted to know the number of weekends, rather than the number of weekend days, we'd need to slightly amend this logic (and define how a range containing only one weekend day - or one starting on a Sunday and ending on a Saturday inclusive - should be handled). Assuming you just want to know how many distinct weekends are at least partially within the date range, you could do:
SELECT h.ID,
h.[Status],
h.StartDate,
h.EndDate,
COUNT(weekends.ID) AS Weekends
FROM #HistoryLog h
LEFT JOIN
(
SELECT c.ID,
c.[Date] AS SatDate,
DATEADD(DAY,1,c.[Date]) AS SunDate
FROM #Calendar c
WHERE c.[DayOfWeek] = 'Saturday'
) weekends ON h.StartDate BETWEEN weekends.SatDate AND weekends.SunDate
OR h.EndDate BETWEEN weekends.SatDate AND weekends.SunDate
OR (h.StartDate <= weekends.SatDate AND h.EndDate >= weekends.SunDate)
GROUP BY h.ID, h.[Status], h.StartDate, h.EndDate

TSQL get last day of previous months upto a specified month

I need to get last day of all previous months including current month, upto a specified month. For example, I need last days of september, aug, july, june, may, april, march, feb, jan, dec 2015 like so:
temptable_mytable:
last_day_of_month
-----------------
2016-09-30
2016-08-31
2016-07-31
2016-06-30
2016-05-31
2016-04-30
2016-03-31
2016-02-30
2016-01-31
2015-12-31
I need to specify the month and year to go back to - in above case it's December 2015, but it could also be September 2015 and such. Is there a way that I can do a loop and do this instead of having to calculate separately for each month end?
Use a recursive CTE with the EOMONTH function.
DECLARE #startdate DATE = '2016-01-01'
;WITH CTE
AS
(
SELECT EOMONTH(GETDATE()) as 'Dates'
UNION ALL
SELECT EOMONTH(DATEADD(MONTH, -1, [Dates]))
FROM CTE WHERE Dates > DATEADD(MONTH, 1, #startdate)
)
SELECT * FROM CTE
with temp as (select -1 i union all
select i+1 i from temp where i < 8)
select DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE())+i*-1,0)) from temp
declare #LASTMONTH date = '2018-10-01';
WITH MTHS AS (
SELECT dateadd(month,month(getdate()),dateadd(year,year(getdate()) - 1900, 0)) aday
UNION ALL
SELECT DATEADD(month,1,aday) from MTHS WHERE aday <= #LASTMONTH
),
LASTDAYS AS (SELECT DATEADD(day,-1,aday) finaldayofmonth from MTHS)
select * from LASTDAYS
Here is a version that goes forward or backwards as appropriate
declare #LASTMONTH date = '2013-10-01';
WITH DIF AS (SELECT CASE WHEN
YEAR(#LASTMONTH) * 12 + MONTH(#LASTMONTH)
>= YEAR(GETDATE()) * 12 + MONTH(getdate()) THEN 1 ELSE -1 END x),
MTHS AS (
SELECT dateadd(month,month(getdate()),dateadd(year,year(getdate()) - 1900, 0)) aday
UNION ALL
SELECT DATEADD(month,(SELECT X from dif),aday) from MTHS
WHERE month(aday) != month(dateadd(month,1,#LASTMONTH)) or YEAR(aday) != YEAR(dateadd(month,1,#LASTMONTH))
),
LASTDAYS AS (SELECT DATEADD(day,-1,aday) finaldayofmonth from MTHS)
select * from LASTDAYS order by finaldayofmonth
Here's one approach, using a CTE to generate a list of incrementing numbers to allow us to then have something to select from and use in a DATEADD to go back for the appropriate number of months.
Typically, if you're doing this quite frequently, instead of generating numbers on the fly like this with the CROSS JOIN, I'd recommend just creating a "Numbers" table that just holds numbers from 1 to "some number high enough to meet your needs"
DECLARE #Date DATE = '20151201'
DECLARE #MonthsBackToGo INTEGER
SELECT #MonthsBackToGo = DATEDIFF(mm, #Date, GETDATE()) + 1;
WITH _Numbers AS
(
SELECT TOP (#MonthsBackToGo) ROW_NUMBER() OVER (ORDER BY o.object_id) AS Number
FROM sys.objects o
CROSS JOIN sys.objects o2
)
SELECT EOMONTH(DATEADD(mm, -(Number- 1), GETDATE())) AS last_day_of_month
FROM _Numbers
This should scale out no matter how far you go back or forward for your originating table or object.
SET NOCOUNT ON;
DECLARE #Dates TABLE ( dt DATE)
DECLARE #Start DATE = DATEADD(YEAR, DATEDIFF(YEAR, 0, GETDATE()), 0)
DECLARE #End DATE = DATEADD(YEAR, 1, #Start)
WHILE #Start <= #End
BEGIN
INSERT INTO #Dates (dt) VALUES (#Start)
SELECT #Start = DATEADD(DAY, 1, #Start)
END
; With x as
(
Select
dt
, ROW_NUMBER() OVER(PARTITION BY DATEPART(YEAR, Dt), DATEPART(MONTH, Dt) ORDER BY Dt Desc) AS rwn
From #Dates
)
Select *
From x
WHERE rwn = 1
ORDER BY Dt
This was cribbed together quick based on a couple different SO answers for the parts:
DECLARE #startdate datetime, #enddate datetime
set #startdate = '2015-12-01'
set #enddate = getdate()
;WITH T(date)
AS
(
SELECT #startdate
UNION ALL
SELECT DateAdd(day,1,T.date) FROM T WHERE T.date < #enddate
)
SELECT DISTINCT
DATEADD(
day,
-1,
CAST(CAST(YEAR(date) AS varchar) + '-' + CAST(MONTH(date)AS varchar) + '-01' AS DATETIME))
FROM T OPTION (MAXRECURSION 32767);

Resources