recursive common table expression in SQL Server - sql

I have a table that contains two pieces of information BusinessDate and DailyPerf where DailyPerf shows percentage change in the value of a price I am tracking. The first few rows of the data looks like this
BusinessDate DailyPerf
Jan 3, 2017 -0.0356%
Jan 4, 2017 -0.4325%
Jan 5, 2017 -0.3953%
Jan 6, 2017 -0.8469%
Jan 9, 2017 -0.5050%
What I am trying to do is to calculate another column YearFac that shows the current percentage of my initial value I have left
Done in Excel the data would look like this:
BusinessDate DailyPerf YearFac
Jan 2, 2017 NULL 100%
Jan 3, 2017 -0.0356% 99.9644%
Jan 4, 2017 -0.4325% 99.5321%
Jan 5, 2017 -0.3953% 99.1386%
Jan 6, 2017 -0.8469% 98.2989%
Jan 9, 2017 -0.5050% 97.8025%
So, I need to prime my query with a YearFac of 100% and then recursively calculate the factor. The one caveat is that the dates are not necessarily sequential (there are no entries over weekends or on holidays) - so I cannot assume that next day = this day + 1 just that the next day is the next larger date
I tried the following
WITH cte
AS (
SELECT cast(1 as int) RowCnt,
cast('3 jan 2017' as date) BusinessDate,
cast(1.0 as float) YearFac -- anchor member
UNION ALL
select cast(ROW_NUMBER() OVER(ORDER BY p.BusinessDate ASC) + 1 as int) as RowCnt,
p.BusinessDate, -- recursive member
cast(cte.YearFac*(1.0 + p.DailyPerc) as float) YearFac
from cte
inner join dbo.MsfsDailyPnl p
on
cte.RowCnt = ROW_NUMBER() OVER(ORDER BY p.BusinessDate ASC) + 1
where p.BusinessDate < sysutcdatetime()
)
SELECT RowCnt,BusinessDate, YearFac
FROM cte
But, of course, this fails because I cannot reference the row number in the join. Could anyone suggest a modification that will get this query to work?

Use a cumulative sum:
select BusinessDate, DailyPerf,
1 - exp(sum(log(1 + DailyPerf)) over (order by BusinessDate desc)
from (select cast('2017-01-03' as date) as BusinessDate, cast(0 as float) as DailyPerf
union all
select BusinessDate, DailyPerf from dbo.MsfsDailyPnl
) p;

Use window function
select *, sum(DailyPerf) over (order by BusinessDate) + 100 YearFac
from (
select dateadd(day, -1, min(BusinessDate)) BusinessDate, 0 DailyPerf from data
union all
select * from data) t
dbfiddle demo
RESULT
BusinessDate DailyPerf YearFac
-------------------------------------------
02/01/2017 00:00:00 0.0000 100.0000
03/01/2017 00:00:00 -0.0356 99.9644
04/01/2017 00:00:00 -0.4325 99.5319
05/01/2017 00:00:00 -0.3953 99.1366
06/01/2017 00:00:00 -0.8469 98.2897
09/01/2017 00:00:00 -0.5050 97.7847

Here's what you might want to try. I attached also definition of table, so you know how it looks like and what can be done easily to accomplish your task.
declare #x table(BusinessDate date, DailyPerf float)
insert into #x values
('Jan 2, 2017', NULL),
('Jan 3, 2017', -0.0356),
('Jan 4, 2017', -0.4325),
('Jan 5, 2017', -0.3953),
('Jan 6, 2017', -0.8469),
('Jan 9, 2017', -0.5050)
select *,
100 + SUM(isnull(DailyPerf,0)) over (partition by (select null) order by
BusinessDate rows between unbounded preceding and current row)
from #x

Related

SQL Count of Agents providing profit every month

There are 2 columns in a table. AgentID and BusinessDate.
Query to find the count of Agents everymonth. But the count should only include agents having businessDate in every previous month.
Eg.
AgentID
BusinessDate
1
Jan 2020
2
Jan 2020
3
Jan 2020
4
Jan 2020
5
Feb 2020
1
Feb 2020
2
Feb 2020
3
Feb 2020
6
March 2020
7
March 2020
1
March 2020
2
March 2020
2
March 2020
Output
Month
Count
Jan
4
Feb
3
Mar
2
Only the agents providing business in every continuous month are counted.
I hope that I have explained the problem fine.
I have tried below methods:
select
Case
when (BusDate Between '01/01/2020' and '01/31/2020') and (BusDate Between '02/01/2020' and '02/28/2020') then ID end as 'Month'
,BusDate
from #Temp
group by Case
when (BusDate Between '01/01/2020' and '01/31/2020') and (BusDate Between '02/01/2020' and '02/28/2020') then ID end
BusDate
select ID
from #Temp where BusDate between '01/01/2020' and '01/31/2020' and BusFlg =1
intersect
select ID
from #Temp where BusDate between '02/01/2020' and '02/28/2020' and BusFlg =1
select ID
from #Temp where BusDate between '01/01/2020' and '01/31/2020' BusDate between '02/01/2020' and '02/28/2020' and BusFlg =1
select Month(BusDate), Count(ID) Over (partition by Month(BusDate)) from #Temp
group by BusDate,ID
select Month(BusDate), Sum(case when BusDate between '01/01/2020' and '01/31/2020' then 1
when Month(BusDate)=2 and BusDate between '01/01/2020' and '01/31/2020' and
)
from #Temp where BusFlg=1
Group by Month(BusDate)
select distinct ID, Count(Distinct ID) AS Cnt from #Temp where Month(BusDate)=2 and
ID in (Select Distinct ID from #Temp where Month(BusDate)=1 and BusFlg=1)
And BusFlg = 1 Group by ID
You can go for recursive CTE and only consider the already existing agents in the recursive part of the CTE. I am also going for rank to consider months in order: January, February ,March.
declare #table table(agentId int, businessdate varchar(10))
insert into #table values
(1 ,'Jan 2020')
,(2 ,'Jan 2020')
,(3 ,'Jan 2020')
,(4 ,'Jan 2020')
,(5 ,'Feb 2020')
,(1 ,'Feb 2020')
,(2 ,'Feb 2020')
,(3 ,'Feb 2020')
,(6 ,'March 2020')
,(7 ,'March 2020')
,(1 ,'March 2020')
,(2 ,'March 2020')
,(2 ,'March 2020');
;
with
cte_months as(
SELECT 1 as rnk, 'Jan 2020' as mon
union all
select 2, 'Feb 2020'
union all
select 3, 'March 2020'
),
cte_agents as(
SELECT cm.rnk, agentid, businessdate
from
#table as t
inner join cte_months as cm
on cm.mon = t.businessdate
where cm.rnk = 1
union all
-- consider only new months
SELECT cm.rnk, t.agentid, t.businessdate
from
#table as t
inner join cte_Agents as c
on c.agentid = t.agentid
--
inner join cte_months as cm
on
cm.mon = t.businessdate
and cm.rnk = c.rnk + 1
)
-- SELECT * FROM cte_agents
SELECT
businessdate,
count(distinct AgentId) as countOfAgents
FROM CTE_AGENTS
group by businessdate
businessdate
countOfAgents
Feb 2020
3
Jan 2020
4
March 2020
2
You can dense rank the months and the agents by month. Then take only rows where the ranks are equal. Then aggregate:
select v.yyyymm, count(distinct agentid)
from (select t.*, v.yyyymm,
dense_rank() over (order by yyyymm) as seqnum,
dense_rank() over (partition by agentid order by yyyymm) as seqnum_agentid
from t cross apply
(values (datefromparts(year(businessdate), month(businessdate), 1))
) v(yyyymm)
) t
where seqnum = seqnum_agentid

Ways to find Percentage increase in sales Month over Month

I want to find the percentage increase in Month over month sales amount using SQL Server. I want to find % MoM increase in sales by using self join and also using partition with rows unbounded preceding. I do not want to use lag(). Can anyone let me know about the the ways to generate this solution.
Here is my table.
create table growth_new(slno bigint,mon varchar(30),sales_amount bigint)
insert into growth_new values(1, 'Jan', 5000)
insert into growth_new values(2, 'Feb', 12000)
insert into growth_new values(3, 'Mar', 32000)
insert into growth_new values(4, 'Apr', 20000)
Slno Mon sales_amount
1 Jan 5000
2 Feb 12000
3 Mar 32000
4 Apr 20000
You can use lag(). If slno orders the rows, then:
select gn.*,
(gn.sales_amount * 1.0 / lag(gn.sales_amount) over (order by slno)) - 1 as increase
from growth_new gn;
A self-join doesn't really make sense for this problem. But if you really needed to with this data structure:
with gn as (
select gn.*, convert(date, month + ' 2000') as mm
from growth_new
)
select gn.*,
(gn.sales_amount * 1.0 / gnprev.sales_amount) - 1
from gn left join
gn gnprev
on gnprev.mm = dateadd(month, -1, gn.mm);
You should, however, really fix the data so the month is in a reasonable format.
If you don't want to use the LEAD or LAG, you can use the following:
I assumed that you can compare using the ids, otherwise, you can have a table to store the months Ids
selecT g.*, growth = 100*cast(iif(p.sales_amount is null,0,(g.sales_amount-p.sales_amount)*1.0/p.sales_amount) as money)
from growth_new g
left join growth_new p on p.slno=g.slno-1
the output is:
slno mon sales_amount growth
1 Jan 5000 0.00
2 Feb 12000 140.00
3 Mar 32000 166.67
4 Apr 20000 -37.50
Hope this helps you
You could use the lag function really unless you want to try other alternatives. Also as mentioned above your month format is not ideal and not scalable at all.
WITH growth_new(slno ,mon ,sales_amount)
AS (SELECT 1, 'Jan', 5000 UNION
SELECT 2, 'Feb', 12000 UNION
SELECT 3, 'Mar', 32000 UNION
SELECT 4, 'Apr', 20000
)
SELECT cur.*, prev.mon as prev_month,
ISNULL(prev.sales_amount,0) AS prev_month_sales_amount,
[%MoM Change] = ((cur.sales_amount -
ISNULL(prev.sales_amount,0))/CAST(prev.sales_amount as float))*100
FROM growth_new cur
LEFT JOIN growth_new prev ON prev.slno = cur.slno - 1
slno mon sales_amount prev_month prev_month_sales_amount %MoM Change
1 Jan 5000 NULL 0 NULL
2 Feb 12000 Jan 5000 140
3 Mar 32000 Feb 12000 166.666666666667
4 Apr 20000 Mar 32000 -37.5

Reshaping a table SQL

I am using SQL and currently have a large table that contains data for 1000's of accounts sorted by date:
ID July 2018 August 2018 September 2018 …
1 10 20 30
2 50 40 10
3 20 10 80
I need to reshape the table so the table is displayed like this:
ID Month Value
1 July 2018 10
1 August 2018 20
1 September 2018 30
: : :
I don't know how to do this or if this is even possible. I have tried to use the pivot function in SQL but I have not been successful. Is there a way to do this?
You can use APPLY :
SELECT t.id, tt.*
FROM table t CROSS APPLY
( VALUES ('July 2018', [July 2018]),
('August 2018', [August 2018]),
('September 2018', [September 2018])
) tt (Month, Val);
You can use unpivot - it will work in MSSQL
select t.id, up.months, up.value
from tablename t
unpivot
(
value
for months in ([July 2018], [August 2018], [September 2018])
) up;
You can try with the help of UNION operator:
select id, 'July 2018' as Month, July2018 as value from <table>
UNION
select id, 'August 2018' as Month, August2018 as value from <table>
UNION
select id, 'September 2018' as Month, September2018 as value from <table>

How to get all month names and need to show month data

Pnum Fdate description
==== ========== ===========
1024 2018-02-17 A
1024 2018-05-17 B
1024 2018-05-17 C
1024 2018-09-17 D
MY table PW have fields looks like this.
--> I want to show the result as
**Month Name Description**
January -
February A
March -
April -
May B
June -
July -
August C
September D
October -
November -
December -
Please help me how to achive this.
Join with a list of month names, there is only twelve of them:
SELECT monthname, description
FROM (VALUES
(1, 'January'),
(2, 'February'),
(3, 'March'),
(4, 'April'),
(5, 'May'),
(6, 'June'),
(7, 'July'),
(8, 'August'),
(9, 'September'),
(10, 'October'),
(11, 'November'),
(12, 'December')
) AS va(monthnumber, monthname)
LEFT JOIN yourdata ON DATEPART(MONTH, fdate) = va.monthnumber
ORDER BY monthnumber
Try this
;WITH CTE(Pnum, Fdate,description)
AS
(
SELect 1024,'2018-02-17','A' union all
SELect 1024,'2018-05-17','B' union all
SELect 1024,'2018-08-17','C' union all
SELect 1024,'2018-09-17','D'
)
SELECT MonthNames,ISNULL([Description],'-') AS [Description]
FROM CTE RIGHT JOIN
(
SELECT DATENAME(MONTH,DATEADD(MONTH,number-datepart(month,GETDATE()),GETDATE())) as MonthNames
FROM MASTER.DBO.spt_values
WHERE TYPE ='P'
AND number BETWEEN 1 AND 12
) dt
ON dt.MonthNames=DATENAME(MONTH,Fdate)
Result
MonthNames Description
--------------------------
January -
February A
March -
April -
May B
June -
July -
August C
September D
October -
November -
December -
You can try below
DEMO
with cte1 as (
select cast('2018-01-01' as date) dt
union all
select dateadd(month, 1, dt)
from cte1
where dateadd(month, 1, dt) < cast('2018-12-31' as date)
)
select DateName(month,dt),coalesce(Description,'-') as Description from cte1 a left join yourtable b
on month(a.dt)=month(b.Fdate)
This solution will allow an index on Fdate to be used (MONTH(column) will force a scan every time).
DECLARE #year int = 2018;
;WITH m AS
(
SELECT m = 1 UNION ALL SELECT m + 1 FROM m WHERE m < 12
),
months(b,e) AS
(
SELECT b = DATEFROMPARTS(#year, m, 1)
FROM m
)
SELECT DATENAME(MONTH, m.b), PW.Description
FROM months AS m
LEFT OUTER JOIN dbo.PW
ON PW.Fdate >= m.b AND PW.Fdate < DATEADD(MONTH, 1, m.b)
ORDER BY m.b;
Try the following query:
SELECT MONTHNAME(fdate), description FROM table
For more reference go through
https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_month

SQL to get list of dates as well as days before and after without duplicates

I need to display a list of dates, which I have in a table
SELECT mydate AS MyDate, 1 AS DateType
FROM myTable
WHERE myTable.fkId = #MyFkId;
Jan 1, 2010 - 1
Jan 2, 2010 - 1
Jan 10, 2010 - 1
No problem. However, I now need to display the date before and the date after as well with a different DateType.
Dec 31, 2009 - 2
Jan 1, 2010 - 1
Jan 2, 2010 - 1
Jan 3, 2010 - 2
Jan 9, 2010 - 2
Jan 10, 2010 - 1
Jan 11, 2010 - 2
I thought I could use a union
SELECT MyDate, DateType
FROM (
SELECT mydate - 1 AS MyDate, 2 AS DateType
FROM myTable
WHERE myTable.fkId = #MyFkId;
UNION
SELECT mydate + 1 AS MyDate, 2 AS DateType
FROM myTable
WHERE myTable.fkId = #MyFkId;
UNION
SELECT mydate AS MyDate, 1 AS DateType
FROM myTable
WHERE myTable.fkId = #MyFkId;
) AS myCombinedDateTable
This however includes duplicates of the original dates.
Dec 31, 2009 - 2
Jan 1, 2010 - 2
Jan 1, 2010 - 1
Jan 2, 2010 - 2
Jan 2, 2010 - 1
Jan 3, 2010 - 2
Jan 9, 2010 - 2
Jan 10, 2010 - 1
Jan 11, 2010 - 2
How can I best remove these duplicates? I am considering a temporary table, but am unsure if that is the best way to do it.
This also appears to me that it may provide performance issues as I am running the same query three separate times.
What would be the best way to handle this request?
This should work for you:
SELECT MyDate, min(DateType) as DateType
FROM (
SELECT mydate - 1 AS MyDate, 2 AS DateType
FROM myTable
WHERE myTable.fkId = #MyFkId;
UNION
SELECT mydate + 1 AS MyDate, 2 AS DateType
FROM myTable
WHERE myTable.fkId = #MyFkId;
UNION ALL
SELECT mydate AS MyDate, 1 AS DateType
FROM myTable
WHERE myTable.fkId = #MyFkId;
) AS myCombinedDateTable
group by MyDate
Note: I changed the second UNION to a UNION ALL for better performance; the last subquery will never have duplicates with the first two subqueries, since DateType is always 2 for the first two, and 1 for the last UNIONed query.
Although you have accepted the solution, let me give this solution for the reference:
SELECT MyDate, Min(DateType)
From
(
SELECT MyDate + T1.RecordType AS MyDate, T1.DateType
FROM
(
Select 1 AS RecordType, 2 AS DateType
Union ALL
Select 0 AS RecordType, 1 AS DateType
Union ALL
Select -1 AS RecordType, 2 AS DateType
) AS T1
CROSS JOIN myTable
Where myTable.fkId = #MyFkId
) AS CombinedTable
Group By MyDate
Advantage of this solution, myTable is queried only once, current case we are having a filter on fkID so right now performance will not matter, but if we have to evaluate complex query then this technique can work fine with respect to Union.
Tried this and it works. Note my use of DATEADD to get it to work with my local copy of SQL which is SQL2008.
SELECT MyDate, Min(DateType)
FROM (
SELECT DATEADD(DAY,-1,mydate) AS MyDate, 2 AS DateType
FROM myTable
WHERE myTable.fkId = #MyFkId
UNION
SELECT DATEADD(DAY,1,mydate) as MyDate, 2 AS DateType
FROM myTable
WHERE myTable.fkId = #MyFkId
UNION
SELECT mydate AS MyDate, 1 AS DateType
FROM myTable
WHERE myTable.fkId = #MyFkId
) AS myCombinedDateTable
group by Mydate