SQL Group and Sum By Month - Default to Zero - sql

I am currently grouping and summing inventory usage by month:
SELECT Inventory.itemid AS ItemID,
SUM(Inventory.Totalunits) AS Individual_MonthQty,
MONTH(Inventory.dadded) AS Individual_MonthAsNumber,
DATENAME(MONTH, Inventory.dadded) AS Individual_MonthAsString
FROM Inventory
WHERE Inventory.invtype = 'Shipment'
AND Inventory.dadded >= #StartRange
AND Inventory.dadded <= #EndRange
GROUP BY Inventory.ItemID,
MONTH(Inventory.dadded),
DATENAME(MONTH, Inventory.dadded)
This gives me the results that I'm expecting:
ItemID Kit_MonthQty Kit_MonthAsNumber Kit_MonthAsString
13188 234 8 August
13188 45 9 September
13188 61 10 October
13188 20 12 December
Question
What must I do to return zero for months where no data exsits, like this:
ItemID Kit_MonthQty Kit_MonthAsNumber Kit_MonthAsString
13188 0 1 January
13188 0 2 February
13188 0 3 March
13188 0 4 April
13188 0 5 May
13188 0 6 June
13188 0 7 July
13188 234 8 August
13188 45 9 September
13188 61 10 October
13188 0 11 November
13188 20 12 December

In the past, I've solved a problem like this by creating a temporary table which will hold all dates needed:
CREATE TABLE #AllDates (ThisDate datetime null)
SET #CurrentDate = #StartRange
-- insert all dates into temp table
WHILE #CurrentDate <= #EndRange
BEGIN
INSERT INTO #AllDates values(#CurrentDate)
SET #CurrentDate = dateadd(mm, 1, #CurrentDate)
END
Then, modify your query to join against this table:
SELECT ALLItems.ItemId,
SUM(COALESCE(Inventory.Qty, 0)) AS Individual_MonthQty,
MONTH(#AllDates.ThisDate) AS Individual_MonthAsNumber,
DATENAME(MONTH, #AllDates.ThisDate) AS Individual_MonthAsString
FROM #AllDates
JOIN (SELECT DISTINCT dbo.Inventory.ItemId FROM dbo.Inventory) AS ALLItems ON 1 = 1
LEFT JOIN Inventory ON DATEADD(dd, - DAY(Inventory.dadded) +1, Inventory.dadded) = #AllDates.ThisDate AND ALLItems.ItemId = dbo.Inventory.ItemId
WHERE
#AllDates.ThisDate >= #StartRange
AND #AllDates.ThisDate <= #EndRange
GROUP BY ALLItems.ItemId,
#AllDates.ThisDate
Then you should have a record for each month, regardless of whether it exists in Inventory.

You could prepare a 'calendar' table like so:
DECLARE #d datetime
SET #d = #StartRange
DECLARE #calendar TABLE (Date datetime)
WHILE (#d <= #EndRange) BEGIN
INSERT INTO #Calendar VALUES (#d)
SET #d = DATEADD(month, 1, #d)
END
and then do a LEFT JOIN with your table on the month and year date parts. This way you'll always have all the months between the start and end date as rows.

Here's one way. This assumes that #StartRange and #EndRange fall within the same calendar year, otherwise you will get some funny results.
WITH n(n) AS
(
SELECT TOP 12 ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.objects
),
months(m, mn) AS
(
SELECT n, DATENAME(MONTH, DATEADD(MONTH, m-1, '19000101'))
FROM n
)
SELECT
ItemID = i.itemid,
MonthQty = COALESCE(SUM(TotalUnits), 0),
MonthAsNumber = m.m,
MonthAsString = m.mn
FROM
months AS m
LEFT OUTER JOIN
Inventory AS i
ON
MONTH(i.dadded) = m.m
WHERE
i.invtype = 'Shipment'
AND i.dadded >= #StartRange
AND i.dadded <= #EndRange
GROUP BY
m.m, m.mn, i.itemid
ORDER BY m.m;

Related

Get the COUNT(*) records for each month in the year

I have Start_Date(which is '2021-12-01') and End_Date(which is current calendar month)
I need to get COUNT(*) for each month from 2021-12-01 to the current month
Result set should look like this:
December, 2021 - 21
January, 2022 - 44
Feb, 2023 - 11
etc.
So what I tried(I only how do that manually):
DECLARE #datestamp DATE = '12/01/2021'
DECLARE #CurrentDate VARCHAR(100)
SET #CurrentDate = CONVERT(varchar, GETDATE(), 101)
SELECT COUNT(*) AS 'December, 2021'
FROM #TMP
WHERE Start_Date IS NOT NULL AND [Signup_Date] IS NOT NULL
AND Start_Date BETWEEN #datestamp AND '2021-12-31'
But I don't want to copy this query for each of 12 months. How do that dynamically?

Calculate quarter for dates given an example end date and quarter number

I have an issue where I need to determine fiscal quarters, but won't always know the start/end dates for the quarters. They will, however, always be 3 months long. What I will know is the ending date of the current quarter, and what quarter and year that refers to. For example, I might be given:
Current Quarter: Q4
Current Year: 2021
Current Quarter End Date: 1/31/2021
How can I get the quarter for any other date? If any of those 3 values were to change, the query still needs to provide the quarter for any given date based on those 3 parameters.
I came up with the following, which puts the last 4 years into a temp table:
DECLARE #QuarterEnd DATE = '1/31/2022'
, #CurrentQuarter INT = 1
, #CurrentYear INT = 2022
, #Counter INT = 16
, #qs INT = 0
, #qe INT = 2
, #DateToTest DATE = '12/15/2021'
CREATE TABLE #Quarters (
StartDate DATE
, EndDate DATE
, Qtr INT
, Yr INT
)
WHILE #Counter <> 0
BEGIN
INSERT INTO #Quarters VALUES (
cast(DATEADD(MONTH, DATEDIFF(MONTH, 0, #QuarterEnd)-#qe , 0) as date)
, cast(DATEADD(MONTH, DATEDIFF(MONTH, -1, #QuarterEnd)-#qs, -1) as date)
, #CurrentQuarter
, #CurrentYear
)
SET #Counter = #Counter - 1
SET #qs = #qs + 3
SET #qe = #qe + 3
SET #CurrentQuarter = CASE WHEN #CurrentQuarter = 1 THEN 4 ELSE #CurrentQuarter - 1 END
SET #CurrentYear = CASE WHEN #CurrentQuarter = 4 THEN #CurrentYear - 1 ELSE #CurrentYear END
END
SELECT #DateToTest
, (SELECT CONCAT('Q', Qtr, ' ', Yr) FROM #Quarters WHERE #DateToTest BETWEEN StartDate and EndDate)
FROM #Quarters
However, this doesn't seem to be practical when I'm running queries that will return hundreds of thousands of records.
I suppose I can throw that into a function and call it with:
SELECT MyQuarter = dbo.MyQuarterFunction(#QuarterEnd, #CurrentQuarter, #CurrentYear, #DateToTest)
There has to be a more efficient way to do this. Any suggestions?
Just create a permanent table called Quarters.
CREATE TABLE dbo.Quarters
(
StartDate date,
QuarterNumber tinyint,
FiscalYear int,
NextQuarterStartDate AS (DATEADD(MONTH, 3, StartDate))
);
INSERT dbo.Quarters(StartDate, QuarterNumber, FiscalYear)
VALUES('20200201',1,2020),
('20200501',2,2020),
('20200801',3,2020),
('20201101',4,2020),
('20210201',1,2021),
('20210501',2,2021),
('20210801',3,2021),
('20211101',4,2021),
('20220201',1,2022),
('20220501',2,2022),
('20220801',3,2022),
('20221101',4,2022);
Now any time you are given a date (like GETDATE()) you can find the other information easily:
DECLARE #date date = GETDATE();
SELECT * FROM dbo.Quarters
WHERE #date >= StartDate
AND #date < NextQuarterStartDate;
Example db<>fiddle
If you need to support multiple fiscal calendars simultaneously, just add a column (like CalendarID or CompanyID or CustomerID).
And really, you don't even need a calendar or quarters table for this. You already have a table of clients, right? Just add a column to store what month their fiscal year starts. That's really all you need.
CREATE TABLE dbo.Clients
(
ClientID int NOT NULL CONSTRAINT PK_Clients PRIMARY KEY,
Name nvarchar(200) NOT NULL CONSTRAINT UQ_ClientName UNIQUE,
FiscalYearStart tinyint NOT NULL CONSTRAINT CK_ValidMonth
CHECK (FiscalYearStart BETWEEN 1 AND 12)
);
Now let's insert a few rows with some clients with different fiscal years:
INSERT dbo.Clients(ClientID, Name, FiscalYearStart)
VALUES(1, N'ClientFeb', 2), -- fiscal year starts in February
(2, N'ClientMay', 5), -- fiscal year starts in May
(3, N'ClientNormal', 1); -- fiscal year matches calendar
Now, yes, we need a function, but let's not do any while loops or counters or #temp tables.
CREATE FUNCTION dbo.GetLast16Quarters
(
#DateToTest date,
#ClientID int
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
(
WITH n(n) AS
(
SELECT n = 1 UNION ALL
SELECT n + 1 FROM n WHERE n < 20
),
Last20Quarters(QuarterStart, FiscalYearStart) AS
(
SELECT QuarterStart = DATEADD(QUARTER, 1-n,
DATEFROMPARTS(YEAR(#DateToTest)+1, FiscalYearStart, 1)),
FiscalYearStart
FROM dbo.Clients CROSS JOIN n WHERE ClientID = #ClientID
),
Last16Quarters AS
(
SELECT TOP (16) QuarterStart,
y = YEAR(DATEADD(MONTH, 1-FiscalYearStart, QuarterStart))
FROM Last20Quarters WHERE QuarterStart < #DateToTest
ORDER BY QuarterStart DESC
)
SELECT QuarterStart,
QuarterEnd = EOMONTH(QuarterStart, 2),
FiscalYear = y,
QuarterNumber = ROW_NUMBER() OVER
(PARTITION BY y ORDER BY QuarterStart)
FROM Last16Quarters);
Then to call it:
DECLARE #DateToTest date = '20211215';
SELECT * FROM dbo.GetLast16Quarters(#DateToTest, 1);
Output:
QuarterStart
QuarterEnd
FiscalYear
QuarterNumber
2018-02-01
2018-04-30
2018
1
2018-05-01
2018-07-31
2018
2
2018-08-01
2018-10-31
2018
3
2018-11-01
2019-01-31
2018
4
2019-02-01
2019-04-30
2019
1
2019-05-01
2019-07-31
2019
2
2019-08-01
2019-10-31
2019
3
2019-11-01
2020-01-31
2019
4
2020-02-01
2020-04-30
2020
1
2020-05-01
2020-07-31
2020
2
2020-08-01
2020-10-31
2020
3
2020-11-01
2021-01-31
2020
4
2021-02-01
2021-04-30
2021
1
2021-05-01
2021-07-31
2021
2
2021-08-01
2021-10-31
2021
3
2021-11-01
2022-01-31
2021
4
Example db<>fiddle
Assuming that you have two input variables:
declare #quarter_end date = '2021-01-31';
declare #current_quarter int = 4;
You can calculate the first month of financial year:
declare #first_month_of_fy int = (month(#quarter_end) - #current_quarter * 3 + 12) % 12 + 1;
-- 2 i.e. February
And use that value to calculate the quarter and year for any date using some math:
select *
from (values
('2020-12-15'),
('2021-01-15'),
('2021-12-15'),
('2022-01-15')
) as t(testdate)
cross apply (select
(month(testdate) - #first_month_of_fy + 12) % 12 + 1
) as ca1(month_of_fy)
cross apply (select
(month_of_fy - 1) / 3 + 1,
year(dateadd(month, 12 - month_of_fy, dateadd(day, - day(testdate) + 1, testdate)))
) as ca2(fy_quarter, fy_year)
DB<>Fiddle
I ended up creating a function to handle this. Since I'm given the last day of the quarter, which quarter it is, and which year it is, I can determine the start and end date of that fiscal year. Since a quarter is always 3 months, I can also determine which months fall into which quarter.
The first 4 variables, #qa, #qb, #qc, #qd hold a comma separated list of the months within each quarter (#qa is current quarter, #qb is current quarter -1, #qc is current quarter -2, and #qd is current quarter -3)
The second 2 variables determine the first day and last day of the fiscal calendar
To get the quarter & year, I first get the month from the supplied date (#Date) and see if it's in #qa, #qb, #qc or #qd. That tells me the fiscal quarter.
Finally, I compare the given date to the start and end date of the current fiscal year, and to the 6 years prior (going back 6 years is enough for what I need)
CREATE FUNCTION [dbo].[FunctionNameHere]
(
#Date DATE
, #QuarterEnd DATE
, #CurrentQuarter INT
, #CurrentYear INT
)
RETURNS VARCHAR(7)
AS
BEGIN
DECLARE #qa VARCHAR(8) = (concat(datepart(m, dateadd(m, 0, #QuarterEnd)),',', datepart(m, dateadd(m, -1, #QuarterEnd)),',', datepart(m, dateadd(m, -2, #QuarterEnd))))
DECLARE #qb VARCHAR(8) = (concat(datepart(m, dateadd(m, -3, #QuarterEnd)),',', datepart(m, dateadd(m, -4, #QuarterEnd)),',', datepart(m, dateadd(m, -5, #QuarterEnd))))
DECLARE #qc VARCHAR(8) = (concat(datepart(m, dateadd(m, -6, #QuarterEnd)),',', datepart(m, dateadd(m, -7, #QuarterEnd)),',', datepart(m, dateadd(m, -8, #QuarterEnd))))
DECLARE #qd VARCHAR(8) = (concat(datepart(m, dateadd(m, -9, #QuarterEnd)),',', datepart(m, dateadd(m, -10, #QuarterEnd)),',', datepart(m, dateadd(m, -11, #QuarterEnd))))
DECLARE #YearStart DATE = DATEADD(d, 1, DATEADD(q, -#CurrentQuarter, #QuarterEnd))
DECLARE #YearEnd DATE = DATEADD(q, 4-#CurrentQuarter, #QuarterEnd)
DECLARE #Qtr VARCHAR(8) = CONCAT('Q', CASE WHEN DATEPART(m, #Date) IN (SELECT value FROM string_split(#qa, ',')) THEN #CurrentQuarter
WHEN DATEPART(m, #Date) IN (SELECT value FROM string_split(#qb, ',')) THEN CASE WHEN #CurrentQuarter = 1 THEN 4
WHEN #CurrentQuarter = 2 THEN 1
WHEN #CurrentQuarter = 3 THEN 2
WHEN #CurrentQuarter = 4 THEN 3 END
WHEN DATEPART(m, #Date) IN (SELECT value FROM string_split(#qc, ',')) THEN CASE WHEN #CurrentQuarter = 1 THEN 3
WHEN #CurrentQuarter = 2 THEN 4
WHEN #CurrentQuarter = 3 THEN 1
WHEN #CurrentQuarter = 4 THEN 2 END
WHEN DATEPART(m, #Date) IN (SELECT value FROM string_split(#qd, ',')) THEN CASE WHEN #CurrentQuarter = 1 THEN 2
WHEN #CurrentQuarter = 2 THEN 3
WHEN #CurrentQuarter = 3 THEN 4
WHEN #CurrentQuarter = 4 THEN 1 END
END,
' ',
CASE WHEN #Date BETWEEN #YearStart AND #YearEnd THEN #CurrentYear
WHEN #Date BETWEEN dateadd(Year, -1, #YearStart) AND dateadd(Year, -1, #YearEnd) THEN #CurrentYear - 1
WHEN #Date BETWEEN dateadd(Year, -2, #YearStart) AND dateadd(Year, -2, #YearEnd) THEN #CurrentYear - 2
WHEN #Date BETWEEN dateadd(Year, -3, #YearStart) AND dateadd(Year, -3, #YearEnd) THEN #CurrentYear - 3
WHEN #Date BETWEEN dateadd(Year, -4, #YearStart) AND dateadd(Year, -4, #YearEnd) THEN #CurrentYear - 4
WHEN #Date BETWEEN dateadd(Year, -5, #YearStart) AND dateadd(Year, -5, #YearEnd) THEN #CurrentYear - 5
WHEN #Date BETWEEN dateadd(Year, -6, #YearStart) AND dateadd(Year, -6, #YearEnd) THEN #CurrentYear - 6
ELSE 9999 END)
return #Qtr
END

Get Quarter and Year between two dates

I'd like to retrieve the list of years and quarters between two dates.
For example, from 25/12/2015 to 06/30/2017, the result should look like:
Year Quarter
2015 4
2016 1
2016 2
2016 3
2016 4
2017 1
2017 2
2017 3
You can use a tally table to do this.
declare #start date='2015-12-25';
declare #end date = '2017-06-30';
select distinct year(dateadd(day,rnum,#start)) yr,
datepart(quarter,dateadd(day,rnum,#start)) qtr
from (select row_number() over(order by (select null)) as rnum
from master..spt_values) t
where dateadd(day,rnum,#start) <= #end;
If you need to span more than 6 years... virtually identical to vkp (he's so fast!)
Declare #Date1 date = '2015-12-25'
Declare #Date2 date = '2017-06-30'
Select Distinct
[Year] =DatePart(YEAR,D)
,[Quarter]=DatePart(QUARTER,D)
From (
Select Top (DateDiff(DD,#Date1,#Date2)+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),#Date1)
From master..spt_values n1,master..spt_values n2
) A
Returns
Year Quarter
2015 4
2016 1
2016 2
2016 3
2016 4
2017 1
2017 2
Declare #StartDate Date='2016-01-01'
, #EndDate Date='2017-05-01'
DECLARE #Date TABLE
(
[Year] INT,
[Quarter] INT
)
WHILE #StartDate <= #EndDate
BEGIN
INSERT INTO #Date
([Year],
[Quarter])
SELECT DATEPART(YEAR,#StartDate) AS [Year],
CASE WHEN DATEPART(MM,#StartDate) BETWEEN 1 AND 3 THEN 1
WHEN DATEPART(MM,#StartDate) BETWEEN 4 AND 6 THEN 2
WHEN DATEPART(MM,#StartDate) BETWEEN 7 AND 9 THEN 3
WHEN DATEPART(MM,#StartDate) BETWEEN 10 AND 12 THEN 4
END AS [Quarter]
SET #StartDate = DATEADD(DAY,1,#StartDate)
END
SELECT DISTINCT [Year],[Quarter] FROM #Date

SQL - Create a temp table or CTE of first day of the month and month names

I need to create a temp table or common table expression based on 2 paremters in a SQL Server 2012 environment
#calYear
#currentYear
so if #calYear = 5 and #currentYear='2014'
I would like to generate a temp table of 5 years starting from current year with 4 columns like
YearDesc MonthName MonthNum FirstDayOfMonth
2014 Jan 1 1/1/2014
2014 Feb 2 2/1/2014
2014 Mar 3 3/1/2014
...
...
...
2018 Oct 10 10/1/2018
2018 Nov 11 11/1/2018
2018 Dec 12 12/1/2018
Is it possible to do a Do While loop efficently? How would I account for the month names?
I'm using a really cumbersome Do While loop to iterate all the months of the year then iterate all the years.
One way using a recursive cte:
declare #calYear int = 5, #currentYear char(4) = '2014'
;with cte (dt) as (
select DATEFROMPARTS(#currentyear,1,1) dt
union all
select dateadd(month,1,dt)
from cte where dt < dateadd(year,#calyear,DATEFROMPARTS(#currentyear,1,1))
)
select year(dt) YearDesc, datename(month, dt) MonthName, month(dt) MonthNum, dt FirstDayOfMonth
from cte
order by dt
Or using a numbers table: (in this case master..spt_values)
declare #calYear int = 5, #currentYear char(4) = '2014'
;with cte2 (dt) as (
select dateadd(month,number,DATEFROMPARTS(#currentyear,1,1)) dt
from master..spt_values where type = 'p'
and number <= 12*#calYear
)
select year(dt) YearDesc, datename(month, dt) MonthName, month(dt) MonthNum, dt FirstDayOfMonth
from cte2
order by dt

SQL days available using a not exists on current financial year

I have a table called 'holiday table' which basically contains dates for all days where employees will not be in work (e.g bank holidays etc)
The below query is basically looking at the current financial year and working out how many days are available firstly by month, and then using the unuion all cummulatively, (e.g April-May, April-June) I dont need one for April though as I can use the non-cumulative for this.
See query:
DECLARE #StartDate DATETIME,
#EndDate DATETIME
--available days
--current – start of this financial year
SELECT #StartDate = (select
case when month(getdate()) >= 4 then
convert(datetime, cast(year(getdate()) as varchar) + '-4-1')
else
convert(datetime, cast(year(getdate())-1 as varchar) + '-4-1')
end),
--current – end of this financial year
#EndDate = (select
case when month(getdate()) < 4 then
convert(datetime, cast(year(getdate()) as varchar) + '-3-31')
else
convert(datetime, cast(year(getdate())+1 as varchar) + '-3-31')
end)
CREATE TABLE #data
(
firstday DATETIME NOT NULL PRIMARY KEY,
workingdays INT NOT NULL
);
WITH dayscte ([Date])
AS (SELECT #StartDate
UNION ALL
SELECT Dateadd(DAY, 1, [Date])
FROM dayscte
WHERE [Date] <= #Enddate)
INSERT INTO #data
SELECT MIN([Date]),
COUNT(*) [Day]
FROM dayscte
LEFT JOIN dbo.Holiday_Table
ON [Date] BETWEEN dbo.Holiday_Table.sch_cal_d AND dbo.Holiday_Table.sch_cal_ed
where
NOT EXISTS (
SELECT sch_id,sch_cal_d,sch_cal_ed FROM dbo.Holiday_Table WHERE
sch_id ='66291100Ks'
AND
[date] <= sch_cal_d
AND
[date] >= sch_cal_ed
)
AND Datename(weekday, [Date]) NOT IN ( 'Saturday', 'Sunday' )
GROUP BY Datepart(MONTH, [Date]),
Datepart(YEAR, [Date])
OPTION (MAXRECURSION 366)
DECLARE #Date DATETIME
SET #Date = (SELECT MIN(firstday)
FROM #data)
SELECT Period,
workingdays [Days_Available_Minus_Holidays] ,
year (firstday) AS [Year]
FROM (SELECT Datename(MONTH, firstday) [Period],
workingdays,
0 [SortField],
firstday
FROM #data
UNION
SELECT Datename(MONTH, #Date) + '-' + Datename(MONTH, firstday),
(SELECT SUM(workingdays)
FROM #data b
WHERE b.firstday <= a.firstday ) [WorkingDays],
1 [SortField],
firstday
FROM #data a
WHERE
firstday > #Date) data
ORDER BY sortfield,
firstday
DROP TABLE #data
GO
The results for this are as follows:
Period Days_Available_Minus_Holidays Year
April 19 2012
May 22 2012
June 19 2012
July 22 2012
August 22 2012
September 20 2012
October 23 2012
November 22 2012
December 19 2012
January 23 2013
February 20 2013
March 21 2013
April 1 2013
April-May 41 2012
April-June 60 2012
April-July 82 2012
April-August 104 2012
April-September 124 2012
April-October 147 2012
April-November 169 2012
April-December 188 2012
April-January 211 2013
April-February 231 2013
April-March 252 2013
April-April 253 2013
My problem is when I get to the cumulative, it does another 'April' and then at the bottom it does an 'April-April' I do not need a cumulative for April as it is only one month do basically I dont want the first or last cumulative values as April is covered by the non-cumulatives, or if the second 'April' must stay, then it should not read '1' as days available, by be the same as the non-cumulative, which is 19 as this is how many days are actually available.
Try removing the equals in your WITH clause
Change WHERE [Date] <= #Enddate to WHERE [Date] < #Enddate
It seems your adding a day to the date before the WHERE clause therefore it is overstepping by a day.