SQL use CTE and PIVOT to make Calender of Months - sql

I am trying to build a CTE to give me a Calendar of the days between a start date and GETDATE() i.e the number of Mondays, Tuesdays, Wednesdays, etc.
I have a case statement which has conditions that need to be met before the calendar count can complete
My effective start date if is a 1 2 or 3 will add the number of days to it using Date-Add and should do this continuously until GETDATE().
I Know i need to use the pivot but seem to be having a lot of trouble getting the PIVOT to work with the CTE,
WITH CTE
AS (
select MR.ContractId B2,Name A1, EffectiveStartDate A ,swd2.Id B
FROM [VES.DES].dbo.MasterRound mr LEFT JOIN [VES.DES].dbo.ScheduledWeekDay swd2
ON MR.RoundRecurrenceId = SWd2.Id
UNION ALL
SELECT
B2,A1,
CASE
When B = 2 Then DATEADD(D,+7,A)
When B = 1 Then DATEADD(D,+1,A)
When B = 3 Then DATEADD(D,+14,A) END AS A, B
FROM CTE WHERE A < GETDATE())
SELECT * FROM CTE
WHERE CTE.B2 = '12'
AND DATEPART(m, A) = DATEPART(m, DATEADD(m, -1, getdate()))
option (maxrecursion 0)
The output I want in the end is
NAME(A1), Month, MON, Tues, WED, Thu, Fri, Sat, Sun
---------------------------------------------------
FW1 JAN 4 3 4 5 3 4 2
FW3 FEB 4 5 4 4 4 4 5
Any help would be great as i appreciate this is quite complicated

I think it will be easier with the CTE combo to not use PIVOT.
Use a GROUP BY like
SELECT
A1,
SUM(
CASE WHEN DATEPART(DW, A) = 1 THEN 1 ELSE 0 END
) AS Sunday
,
SUM(
CASE WHEN DATEPART(DW, A) = 2 THEN 1 ELSE 0 END
) AS Monday
FROM CTE
WHERE CTE.B2 = '12'
AND DATEPART(m, A) = DATEPART(m, DATEADD(m, -1, getdate()))
option (maxrecursion 0)
GROUP BY A1

Related

Split date into month and year based on number of months passed in stored procedure into a temp table

I have a stored procedure, where takes number of numbers as a parameter. I do my query with where clause like this
select salesrepid, month(salesdate), year(salesdate), salespercentage
from SalesRecords
where salesdate >= DATEADD(month, -#NumberOfMonths, getdate())
So for example, if #NumberOFmonths passed = 3 and based on todays date,
It should bring, september 9, october 10 and november 11 in my resultset. My query brings it but request is I need to return null for those salesrep who doesnt have a value for a month,
for example:
salerepid month year salespercentage
232 9 2020 80%
232 10 2020 null
232 11 2020 90%
how can I achieve this ? Right now the query brings back only two records and does not bring october data as no value is there, but i want it to return october with null value.
If I follow you correctly, you can generate all start of months within the target interval, and cross join that with the table to generate all possible combinations. Then you can bring the table with a left join:
with all_dates as (
select datefromparts(year(getdate()), month(getdate()), 1) salesdate, 0 lvl
union all
select dateadd(month, - lvl - 1, salesdate), lvl + 1
from all_dates
where lvl < #NumberOfMonths
)
select r.salesrepid, d.salesdate , s.salespercentage
from all_dates d
cross join (select distinct salesrepid from salesrecords) r
left join salesrecord s
on s.salesrepid = r.salesrepid
and s.salesdate >= d.salesdate
and s.salesdate < dateadd(month, 1, d.salesdate )
Your original query and result imply that there is at most one record per sales rep and month, so this works under the same assumption. If that's not the case (which would somehow make more sense), you would need aggregation in the outer query.
Declare #numberofmonths int = 3;
with all_dates as (
select datefromparts(year(getdate()), month(getdate()), 1) dt, 0 lvl
union all
select dateadd(month, - lvl - 1, dt), lvl + 1
from all_dates
where lvl < 3
)
select * from all_dates
This gives me following result:
2020-11-01 0
2020-10-01 1
2020-08-01 2
2020-05-01 3
I want only:
2020-11-01 0
2020-10-01 1
2020-09-01 2

Join table with dates list - include reference on NULLs

More brain freeze moments from me. I'm sure this will be an easy one.
I have two tables. One is a list of part usage by week. This is called TransactionsPerWeek and looks like this:
ItemPK xWeek xYear TotalQty
1234 2 2019 65
1234 4 2019 15
1234 5 2019 50
I also have a DateList table that has week numbers and years in it
xWeek xYear
1 2019
2 2019
3 2019
etc.
When I right join the two together on week and year I get
ItemPK xWeek xYear TotalQty
NULL 1 2019 0
1234 2 2019 65
NULL 3 2019 0
1234 4 2019 15
1234 5 2019 50
What I need is to have the ItemPK on every line, even if the TotalQty is 0. So in effect, I need:
ItemPK xWeek xYear TotalQty
1234 1 2019 0
1234 2 2019 65
1234 3 2019 0
1234 4 2019 15
1234 5 2019 50
This is my code...
SELECT itemfk,
dates.year,
dates.week,
isnull(transactionsperweek.TotalQty,0) as TotalQty
from (
SELECT iit.ItemFK,
year(iit.transactiondate) xYear,
datepart(wk,iit.transactiondate) xWeek,
abs(sum(iit.quantity)) TotalQty
from iteminventorytransaction iit
INNER JOIN ItemInventoryTransactionType iitt on ItemInventoryTransactionTypePK = iit.ItemInventoryTransactionTypeFK
where iit.itemfk = 5311
and iit.ItemInventoryTransactionTypeFK in (10,8)
and iit.TransactionDate BETWEEN
-- 1 year up to the sunday of last week
DateAdd(wk,-51,DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE()))
AND DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE())
AND Quantity < 0
group by iit.itemfk,
year(iit.transactiondate),
datepart(wk,iit.transactiondate)
) transactionsPerWeek
RIGHT JOIN (
select year,
week
from DatesList
where date > DateAdd(wk,-51,DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE()))
AND date < DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE())
group by year,
week
) Dates ON dates.week = transactionsPerWeek.xWeek
AND dates.year = transactionsPerWeek.xYear
where week not in (52,53)
Hope this is clear enough. Thanks in advance.
You can use recursive cte :
with cte as (
select 1 as id, max(xWeek) as maxwk
from TransactionsPerWeek
union all
select id + 1, maxwk
from cte c
where c.id < maxwk
)
select coalesce(wk.ItemPK, wk1.ItemPK) as ItemPK, c.id as xWeek, wk.xYear, wk.TotalQty
from cte c left join
TransactionsPerWeek wk
on wk.xWeek = c.id outer apply
( select top (1) wk1.ItemPK
from TransactionsPerWeek wk1
where wk1.xWeek >= c.id and wk1.xWeek is not null
order by wk1.xWeek
) wk1;
Ok, so I did what #larnu suggested and cross joined the item with the dates, then left joined it to the transactionsperweek table and it worked. Thank you.
This is my code now;
SELECT itempk, week, year
, ISNULL(transactionsPerWeek.TotalQty,0) as TotalQty
from item
CROSS JOIN
(
select year, week from DatesList where date >
DateAdd(wk,-51,DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE()))
AND date <
DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE())
group by year, week
) dates
LEFT JOIN
(
SELECT iit.ItemFK, year(iit.transactiondate) xYear, datepart(wk,iit.transactiondate) xWeek, abs(sum(iit.quantity)) TotalQty from iteminventorytransaction iit
INNER JOIN ItemInventoryTransactionType iitt on ItemInventoryTransactionTypePK = iit.ItemInventoryTransactionTypeFK
where iit.itemfk = 5311 and iit.ItemInventoryTransactionTypeFK in (10,8)
and iit.TransactionDate BETWEEN
-- 1 year up to the sunday of last week
DateAdd(wk,-51,DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE()))
AND
DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE())
AND Quantity < 0
group by iit.itemfk, year(iit.transactiondate), datepart(wk,iit.transactiondate)
) transactionsPerWeek
ON itempk = transactionsperweek.ItemFK and transactionsPerWeek.xYear = dates.year and transactionsPerWeek.xWeek = dates.week
where itempk = 5311
Use a cross join to generate the rows and a left join to bring in the results you already have.
Your question explicitly states that you have two tables. Hence, I don't know what your SQL code is doing, because it is not referencing those tables. So, based on the description:
select i.ItemPK, d.xWeek, d.xYear,
coalesce(TotalQty, 0) as TotalQty
from (select distinct itemPK from TransactionsPerWeek
) i cross join
DateList d left join
TransactionsPerWeek t
on t.itemPK = i.itemPK and
t.xWeek = d.xWeek and
t.xYear = d.xYear;
Of course if the "tables" are really subqueries, then I would recommend using CTEs and still this basic query structure.

DATEDIFF excluding summer months

We are running reports for a seasonal business, with expected lulls during the summer months. For some metrics, we'd essentially like to pretend that those months don't even exist.
Thus consider the default behavior of:
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-06-01') -- answer = 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-07-01') -- 2
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-08-01') -- 3
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-09-01') -- 4
We want to ignore June and July, so we would like those answers to look like this:
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-06-01') -- answer = 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-07-01') -- 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-08-01') -- 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-09-01') -- 2
What is the easiest way to accomplish this? I'd like a pure SQL solution, rather than something using TSQL, but writing a custom function such as NOSUMMER_DATEDIFF could also work.
Also, keep in mind the reports will span multiple years, so the solution should be able to handle that.
If you are only interested month differences, then I would suggest a trick here. Count the number of months since some date 0, but ignore the summer months. For example:
'2015-05-01' --> 2015 * 10 + 5 = 20155
'2015-06-01' --> 2015 * 10 + 6 = 20156
'2015-07-01' --> 2015 * 10 + 6 = 20156
'2015-08-01' --> 2015 * 10 + 6 = 20156
'2015-09-01' --> 2015 * 10 + 7 = 20157
This is a fairly easy calculation:
select (case when month(date2) <= 6 then year(date2) * 10 + month(date2)
when month(date2) in (7, 8) then year(date2) * 10 + 6
else year(date2) * 10 + (month(date2) - 2)
end)
For the difference:
select ((case when month(date2) <= 6 then year(date2) * 10 + month(date2)
when month(date2) in (7, 8) then year(date2) * 10 + 6
else year(date2) * 10 + (month(date2) - 2)
end) -
(case when month(date1) <= 6 then year(date1) * 10 + month(date1)
when month(date1) in (7, 8) then year(date1) * 10 + 6
else year(date1) * 10 + (month(date1) - 2)
end)
)
To able to achieve that, you have to "split" dates ranges to an "array" of dates for every single range of dates. CTE might be helpful in this case.
See:
--your table which holds dates ranges
DECLARE #dates TABLE(id INT IDENTITY(1,1), dFrom DATE, dTo DATE)
INSERT INTO #dates (dFrom, dTo)
VALUES('2015-05-01', '2015-06-01'),
('2015-05-01', '2015-07-01'),
('2015-05-01', '2015-08-01'),
('2015-05-01', '2015-09-01')
--summer month table
DECLARE #summermonths TABLE(summMonth INT)
INSERT INTO #summermonths(summMonth)
VALUES(6), (7)
--here Common Table Expressions is in action to "split" dates ranges to an array of dates for every single date range
;WITH CTE AS
(
SELECT id, DATEADD(MM, 0, dFrom) AS ndFrom, dTo, CASE WHEN MONTH(DATEADD(MM, 0, dFrom)) = 6 OR MONTH(DATEADD(MM, 0, dFrom)) = 7 THEN 0 ELSE 1 END AS COfMonth
FROM #dates
WHERE DATEADD(MM, 1, dFrom)<=dTo
UNION ALL
SELECT id, DATEADD(MM, 1, ndFrom) AS ndFrom, dTo, CASE WHEN MONTH(DATEADD(MM, 1, ndFrom)) = 6 OR MONTH(DATEADD(MM, 1, ndFrom)) = 7 THEN 0 ELSE 1 END AS COfMonth
FROM CTE
WHERE DATEADD(MM, 1, ndFrom)<=dTo
)
SELECT t1.id, t2.dFrom, t2.dTo, SUM(t1.COfMonth) AS MyDateDiff
FROM CTE AS t1 INNER JOIN #dates AS t2 ON t1.id = t2.id
GROUP BY t1.id, t2.dFrom , t2.dTo
Result:
id dFrom dTo MyDateDiff
1 2015-05-01 2015-06-01 1
2 2015-05-01 2015-07-01 1
3 2015-05-01 2015-08-01 2
4 2015-05-01 2015-09-01 3 --not 2, because of 5, 8, 9
Got it?
Note: a solution might be differ in case of dFrom and dTo is not the first date of month.

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)

How can I determine the date of a WeekDay column in a result set with a fixed layout given the start date?

I have a result set that always appears in this format:
LocationId Wed Thu Fri Sat Sun Mon Tue
The input to the function that generates these columns accepts a start date, and it generates a 7 day quantity report. So if I input '2014-02-01' the columns will always appear in that order even though that specific date falls on a Saturday (the dates "wrap around").
I need the date for each column for the purpose of a calculating another value (called 'Fee') that is based on a start + end date for each location. For example, location 21 might have a value of 50 associated with it for dates '2014-01-01' to '2014-02-03', but from '2014-02-04' it has a value of 53. The values under the day columns refer to Sales. So if there's a value (even 0), it means the SalesPerson was present and he should receive an AppearanceFee. One of the difficulties is calculating exactly what Fee the person should receive on a particular day as the report doesn't generate dates. The only information you have is the start date.
eg.
LocationId | Value | StartDate | EndDate
-----------+-------+------------+-----------
21 | 50 | 2014-01-01 | 2014-02-03
21 | 53 | 2014-02-04 | null
To simulate one record, one can use this query:
declare #startdate datetime
select #startdate = '2014-02-01'
select *
, 0 as Fee -- How do I calculate this value?
from
(
select 21 as LocationId
, 30 as Wed
, 33 as Thu
, 36 as Fri
, NULL as Sat
, NULL as Sun
, 19 as Mon
, 24 as Tue
) record
I've thought of using a complex case statement for each day but is there a simpler method?
CASE Left(DATENAME(dw, #startdate), 3)
WHEN 'Wed' THEN
(
(SELECT IsNull(Wed, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND #startdate BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Thu, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 1, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Fri, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 2, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Sat, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 3, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Sun, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 4, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Mon, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 5, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Tue, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 6, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31'))
)
As you can see this case statement is rather unwieldy.
I managed to solve this using a combination of PIVOTs and UNPIVOTs, and a query that generates a date range.
DECLARE #startDate DATETIME = '2014-02-01'
DECLARE #endDate DATETIME = #startDate + 7
SELECT
LocationId,
Sum(Wed) Wed,
Sum(Thu) Thu,
Sum(Fri) Fri,
Sum(Sat) Sat,
Sum(Sun) Sun,
Sum(Mon) Mon,
Sum(Tue) Tue,
Sum(Fee) Fee
FROM
(
SELECT
af.LocationId,
Calendar.Day,
Date,
Sales,
IsNumeric(Sales) * Value AS Fee
FROM
(
SELECT
Left(DateName(DW, datetable.Date), 3) Day,
Convert(DATE, datetable.Date) Date
FROM (
SELECT DATEADD(DAY, -(a.a + (10 * b.a) + (100 * c.a)), getdate()) AS Date
FROM (SELECT 0 AS a
UNION ALL 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) AS a
CROSS JOIN (SELECT 0 AS a
UNION ALL 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) AS b
CROSS JOIN (SELECT 0 AS a
UNION ALL 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) AS c
) datetable
WHERE datetable.Date BETWEEN #startDate AND #endDate
) Calendar LEFT JOIN
(
SELECT
LocationId,
Day,
Sales
FROM dbo.f_FakeReport(#startDate) AS Report
UNPIVOT
(
Sales
FOR Day IN (Wed, Thu, Fri, Sat, Sun, Mon, Tue)
) U) AS Report
ON Calendar.Day = Report.Day
LEFT JOIN AppearanceFee af
ON af.LocationId = Report.LocationId
AND date BETWEEN af.StartDate AND IsNull(af.EndDate, '2099-12-21')
) data
PIVOT
(
Sum(Sales)
FOR Day IN (Wed, Thu, Fri, Sat, Sun, Mon, Tue)
) pvt
WHERE LocationId IS NOT NULL
GROUP BY LocationId
http://sqlfiddle.com/#!3/98759/52