Create a year/quarter table in SQL without a loop - sql

I have a start year and an end year, say 2017 and 2019 for example.
I'd like to create a table with columns year and quarter (eg, 1, 2, 3, 4) between my stated startYear and endYear, and have quarter for the final, endYear, to stop at 2 (it's always forward looking).
Sample desired output below.
year quarter
2017 1
2017 2
2017 3
2017 4
2018 1
2018 2
2018 3
2018 4
2019 1
2019 2
Seems like it should be simple, nothing occurs to me except somewhat clunky methods relying on a loop or UNION or simply inserting values manually into the table.

Just another option... an ad-hoc tally table in concert with a Cross Join
Example
Declare #Y1 int = 2017
Declare #Y2 int = 2019
Select *
From ( Select Top (#Y2-#Y1+1) Year=#Y1-1+Row_Number() Over (Order By (Select NULL)) From master..spt_values n1 ) A
Cross Join (values (1),(2),(3),(4)) B([Quarter])
Returns
Year Quarter
2017 1
2017 2
2017 3
2017 4
2018 1
2018 2
2018 3
2018 4
2019 1
2019 2
2019 3
2019 4

Use a recursive CTE:
with yq as (
select 2017 as yyyy, 1 as qq
union all
select (case when qq = 4 then yyyy + 1 else yyyy end),
(case when qq = 4 then 1 else qq + 1 end)
from yq
where yyyy < 2019 or yyyy = 2019 and qq < 2
)
select *
from yq;
If the table will have more than 100 rows, you will also need option (maxrecursion 0).
Here is a db<>fiddle.

This solution is very similar to the one by John, but it doesn't depend on a system table.
Declare #Y1 int = 2017;
Declare #Y2 int = 2019;
WITH
E(n) AS(
SELECT n FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))E(n)
),
E2(n) AS(
SELECT a.n FROM E a, E b
),
E4(n) AS(
SELECT a.n FROM E2 a, E2 b
),
cteYears([Year]) AS(
SELECT TOP (#Y2-#Y1+1)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) + #Y1 - 1 AS [Year]
FROM E4
)
SELECT [Year], [Quarter]
FROM cteYears
CROSS JOIN (VALUES (1),(2),(3),(4)) Q([Quarter]);

Let me to propose a recursve query for you:
WITH prepare AS
(
SELECT tbl.year
FROM (VALUES (2017) ) AS tbl(year) -- for example, start year is 2k17
UNION ALL
SELECT year + 1
FROM prepare
WHERE year < 2030 -- and last year is 2030
)
SELECT
year, quarter
FROM prepare
CROSS JOIN ( VALUES (1), (2), (3), (4) ) AS tbl (quarter)

Related

SQL - Splitting a row with week range into multiple rows

I have the following table structure and data in the database table:
ID
Year
StartWeek
EndWeek
AllocationPercent
5
2021
34
35
50
6
2021
1
3
5
I need to split the multi-week rows into multiple single-week rows, and the end result should be:
ID
Year
StartWeek
EndWeek
AllocationPercent
5
2021
34
34
50
5
2021
35
35
50
6
2021
1
1
5
6
2021
2
2
5
6
2021
3
3
5
Any help with this would be highly appreciated! There are a lot of threads regarding splitting date ranges into multiple rows but I cannot seem to modify those to fit my use case. I know that most likely I need a tally table with the week numbers (which I already have).
Another way to think about this is, because we know the max weeknumber is 53, to generate the set of all possible week numbers, then outer join to that set each week in any source row that is within that range.
;WITH n(n) AS
(
SELECT 0 UNION ALL SELECT n+1 FROM n WHERE n <= 53
)
SELECT w.ID,
w.Year,
StartWeek = n.n,
EndWeek = n.n,
w.AllocationPercent
FROM n
INNER JOIN dbo.TableName AS w
ON n.n BETWEEN w.StartWeek AND w.EndWeek
ORDER BY w.ID, w.Year, n.n;
Results:
ID
Year
StartWeek
EndWeek
AllocationPercent
5
2021
34
34
50
5
2021
35
35
50
6
2021
1
1
5
6
2021
2
2
5
6
2021
3
3
5
Example db<>fiddle
You can use recursive cte :
;with cte as (
select t.id, t.year, t.startweek, t.endweek, t.AllocationPercent
from t
union all
select id, year, startweek + 1, endweek, AllocationPercent
from cte c
where startweek < endweek
)
select id, year, startweek, startweek as endweek, AllocationPercent
from cte
order by id, startweek, endweek;
db fiddle

Pivot a Date Range into multiple rows

I have a table T in this format:
ClientName
StartMonth
EndingMonth
X
Dec 2018
Jan 2021
I want the output of my query to be:
ClientName
MonthRange
Year #
X
Dec 2018-Nov 2019
1
X
Dec 2019-Nov 2020
2
X
Dec 2020-Nov 2021
3
Can someone help me what is the best way to tackle this problem?
Try this:
WITH
indata(clientname,startmonth,endmonth) AS(
SELECT 'x',DATE '2018-12-01', DATE '2021-01-01'
)
,
-- a series of at least 3 integers - no other way ...
y(y) AS (
SELECT 1
UNION ALL SELECT 2
UNION ALL SELECT 3
UNION ALL SELECT 4
)
SELECT
clientname
, TO_CHAR(ADD_MONTHS(startmonth,(y-1)*12),'Mon-YYYY')
||'-'
||TO_CHAR(ADD_MONTHS(startmonth,(y-1)*12+11),'Mon-YYYY') AS monthrange
, y AS "year#"
FROM indata CROSS JOIN y
WHERE ADD_MONTHS(startmonth,(y-1)*12) <= endmonth
ORDER BY y;
clientname|monthrange |year#
x |Dec-2018-Nov-2019| 1
x |Dec-2019-Nov-2020| 2
x |Dec-2020-Nov-2021| 3

Filling missing months when calculating year to date

I have a table cumulative year todate
year month qty_ytd
2017 01 20
2017 02 30
2018 01 50
I need to fill gabs missing months in the same year till december:
Result as example:
year month qty_ytd
2017 01 20
2017 02 30
2017 03 30
.....
2017 07 30
2017 12 30
2018 01 50
2018 02 50
....
2018 12 50
How to do it? I did'nt figure out how to fill the missing months?
You can use cross join to generate the rows and cross apply to get the data:
select y.y, v.m, t.qty_ytd
from (select distinct year from t) y cross join
(values (1), (2), (3), (4), . . . (12)) v(m) outer apply
(select top (1) t.*
from t
where t.year = y.year and
t.month <= y.m
order by t.m desc
) t;
Assuming qty_ytd is non-decreasing, it might be more performant to use window functions:
select y.y, v.m,
max(t.qty_ytd) over (partition by y.y order by v.m) as qty_ytd
from (select distinct year from t) y cross join
(values (1), (2), (3), (4), . . . (12)) v(m) left join
t
on t.year = y.year and
t.month = v.m;
Another option is to compute delta, add dummy zero deltas, restore running total. I've changed source data to show more common case
create table #t
(
year int,
month int,
qty_ytd int
);
insert #t(year, month, qty_ytd )
values
(2017, 01, 20),
(2017, 02, 30),
(2018, 04, 50) -- note month
;
select distinct year, month, sum(delta) over(partition by year order by month)
from (
-- real delta
select year, month, delta = qty_ytd - isnull(lag(qty_ytd) over (partition by year order by month),0)
from #t
union all
-- tally dummy delta
select top(24) 2017 + (n-1)/12, n%12 + 1 , 0
from
( select row_number() over(order by a.n) n
from
(values (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) a(n),
(values (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) b(n)
) c
)d
order by year, month;

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: add missing months from different years

SQL SERVER
[CreatedOn] - DATETIME
I get this table:
Year Month Count
2009 7 1
2009 9 1
2010 1 2
2010 3 13
From query:
SELECT
YEAR ([CreatedOn]) AS 'Year',
MONTH ([CreatedOn]) AS 'Month',
COUNT ([CreatedOn]) AS 'Count'
FROM xxx
GROUP BY YEAR ([CreatedOn]), MONTH ([CreatedOn])
How can I get table like this (with missed months and Count 0):
Year Month Count
2009 7 1
2009 8 0
2009 9 1
2009 10 0
2009 11 0
2009 12 0
2010 1 2
2010 2 0
2010 3 13
Syntax says you are using MSSQL. Use Recursive CTE to generate the calender table then do a Left outer join with XXX table
DECLARE #maxdate DATE = (SELECT Max([CreatedOn])
FROM xxx);
WITH calender
AS (SELECT Min([CreatedOn]) dates,
FROM xxx
UNION ALL
SELECT Dateadd(mm, 1, dates)
FROM cte
WHERE dates < #maxdate)
SELECT Year(dates) [YEAR],
Month(dates) [month],
Count ([CreatedOn]) AS 'Count'
FROM calender a
LEFT OUTER JOIN xxx b
ON Year(dates) = Year ([CreatedOn])
AND Month(dates) = Month ([CreatedOn])
GROUP BY Year(dates),
Month(dates)
Note : Instead of Recursive CTE create a physical calender table
This will use a build in table to create the calendar:
;WITH limits as
(
SELECT min([CreatedOn]) mi, max([CreatedOn]) ma
FROM xxx
), months as(
SELECT
dateadd(mm, number, mi) m
FROM
master..spt_values v
JOIN
limits l
ON
number between 0 and datediff(mm, l.mi, l.ma)
WHERE
v.type = 'P'
)
SELECT
year(months.m) year,
month(months.m) month,
count(qry.[CreatedOn]) cnt
FROM
xxx qry
RIGHT JOIN
months
ON
months.m = dateadd(mm, datediff(mm, 0, qry.[CreatedOn]), 0)
GROUP BY
year(months.m),
month(months.m)