calculating percent change over time - sql

I've created the structure and sample data here. I'm not sure how to go about calculating the change over time.
My desired result set is:
a | % growth
abc | 4.16
def | 0.83
hig | -0.2
The % change being (last value - first value) / days:
a | % growth
abc | (30-5) / 6
def | (6-1) / 6
hig | (4-5) / 5
I'm trying:
SELECT a.*,
b.val,
c.val
FROM (SELECT a,
Min(dt) AS lowerDt,
Max(dt) AS upperDt
FROM tt
GROUP BY a) a
LEFT JOIN tt b
ON b.dt = a.lowerdt
AND b.a = a.a
LEFT JOIN tt c
ON c.dt = a.upperdt
AND b.a = a.a
If possible, I'd like to avoid a CTE.

You don't want min and max, you really want first and last.
One way I do that is to use ROW_NUMBER() to tell me the position from the begining or the end. Then use MAX(CASE WHEN pos=1 THEN x ELSE null END) to get the values I want.
SELECT
a,
MAX(CASE WHEN pos_from_first = 1 THEN dt ELSE NULL END) AS first_date,
MAX(CASE WHEN pos_from_final = 1 THEN dt ELSE NULL END) AS final_date,
MAX(CASE WHEN pos_from_first = 1 THEN val ELSE NULL END) AS first_value,
MAX(CASE WHEN pos_from_final = 1 THEN val ELSE NULL END) AS final_value,
100
*
CAST(MAX(CASE WHEN pos_from_final = 1 THEN val ELSE NULL END) AS DECIMAL(9,6))
/
CAST(MAX(CASE WHEN pos_from_first = 1 THEN val ELSE NULL END) AS DECIMAL(9,6))
-
100 AS perc_change
FROM
(
SELECT
ROW_NUMBER() OVER (PARTITION BY a ORDER BY dt ASC) AS pos_from_first,
ROW_NUMBER() OVER (PARTITION BY a ORDER BY dt DESC) AS pos_from_final,
*
FROM
tt
)
AS ordered
GROUP BY
a
http://sqlfiddle.com/#!6/ad95d/11

Related

Compare the same id with 2 values in string in one table

I have a table like this:
id
status
grade
123
Overall
A
123
Current
B
234
Overall
B
234
Current
D
345
Overall
C
345
Current
A
May I know how can I display how many ids is fitting with the condition:
The grade is sorted like this A > B > C > D > F,
and the Overall grade must be greater than or equal to the Current grade
Is it need to use CASE() to switch the grade to a number first?
e.g. A = 4, B = 3, C = 2, D = 1, F = 0
In the table, there should be 345 is not match the condition. How can I display the tables below:
qty_pass_the_condition
qty_fail_the_condition
total_ids
2
1
3
and\
fail_id
345
Thanks.
As grade is sequential you can do order by desc to make the number. for the first result you can do something like below
select
sum(case when GradeRankO >= GradeRankC then 1 else 0 end) AS
qty_pass_the_condition,
sum(case when GradeRankO < GradeRankC then 1 else 0 end) AS
qty_fail_the_condition,
count(*) AS total_ids
from
(
select * from (
select Id,Status,
Rank() over (partition by Id order by grade desc) GradeRankO
from YourTbale
) as a where Status='Overall'
) as b
inner join
(
select * from (
select Id,Status,
Rank() over (partition by Id order by grade desc) GradeRankC
from YourTbale
) as a where Status='Current'
) as c on b.Id=c.Id
For second one you can do below
select
b.Id fail_id
from
(
select * from (
select Id,Status,
Rank() over (partition by Id order by grade desc) GradeRankO
from Grade
) as a where Status='Overall'
) as b
inner join
(
select * from (
select Id,Status,
Rank() over (partition by Id order by grade desc) GradeRankC
from Grade
) as a where Status='Current'
) as c on b.Id=c.Id
where GradeRankO < GradeRankC
You can use pretty simple conditional aggregation for this, there is no need for window functions.
A Pass is when the row of Overall has grade which is less than or equal to Current, with "less than" being in A-Z order.
Then aggregate again over the whole table, and qty_pass_the_condition is simply the number of non-nulls in Pass. And qty_fail_the_condition is the inverse of that.
SELECT
qty_pass_the_condition = COUNT(t.Pass),
qty_fail_the_condition = COUNT(*) - COUNT(t.Pass),
total_ids = COUNT(*)
FROM (
SELECT
t.id,
Pass = CASE WHEN MIN(CASE WHEN t.status = 'Overall' THEN t.grade END) <=
MIN(CASE WHEN t.status = 'Current' THEN t.grade END)
THEN 1 END
FROM YourTable t
GROUP BY
t.id
) t;
To query the actual failed IDs, simply use a HAVING clause:
SELECT
t.id
FROM YourTable t
GROUP BY
t.id
HAVING MIN(CASE WHEN t.status = 'Overall' THEN t.grade END) >
MIN(CASE WHEN t.status = 'Current' THEN t.grade END);
db<>fiddle

Case Statement and Aggregation within a Group By classe in proc sql

I am having some trouble aggregating and using case within a group.
The objective is to check Indicator for each transaction key. If '1' indicator exists then we have to select the max(Change_Date). If all zeros then min(Change_Date). Along with that the Initial_key associated with that Change_date has to be populated as a Final_key.
The output looks like this
You can get the last two columns using aggregation. If I understand correctly:
select trxn_key,
coalesce(max(case when indicator = 1 then change_date end),
min(change_date)
) as final_date,
coalesce(max(case when indicator = 1 then initial_key end),
min(initial_key)
) as final_key
from t
group by trxn_key;
Then join this in:
proc sql;
select t.*, tt.final_date, tt.final_key
from t join
(select trxn_key,
coalesce(max(case when indicator = 1 then change_date end),
min(change_date)
) as final_date,
coalesce(max(case when indicator = 1 then initial_key end),
min(initial_key)
) as final_key
from t
group by trxn_key
) tt
on tt.trxn_key = t.trxn_key;
Could you try with below,
If i observe the test data provided by you
First we try to find the the max(indicator) within group by name and trxn_key.
Second based on the value above , we decide whether to take min(change_date) and min(initial_key) or max(change_date) and max(initial_key)
Because you don't need aggregated result we need to use analytic function which will not affect the final output rows.
SELECT t1.name
,t1.initial_key
,t1.change_date
,t1.indicator
,t1.trxn_key
,t1.trxn_date
,CASE
WHEN max_ind = 1
THEN
MAX(CASE WHEN indicator = 1 THEN change_date END) OVER (PARTITION BY NAME,trxn_key)
WHEN max_ind = 0
THEN
MIN(CASE WHEN indicator = 0 THEN change_date END) OVER (PARTITION BY NAME,trxn_key)
END final_date
,CASE
WHEN max_ind = 1
THEN
MAX(CASE WHEN indicator = 1 THEN initial_key END) OVER (PARTITION BY NAME,trxn_key)
WHEN max_ind = 0
THEN
MIN(CASE WHEN indicator = 0 THEN initial_key END) OVER (PARTITION BY NAME,trxn_key)
END final_key
FROM
(
SELECT NAME
,initial_key
,change_date
,indicator
,trxn_key
,trxn_date
,MAX(indicator) OVER (PARTITION BY NAME,trxn_key) max_ind
FROM table1
) t1
ORDER BY trxn_key,trxn_date,initial_key,change_date;
You can process groups with DOW loops (do until loops with SET & BY statement inside)
A DATA Step program with serial DOW loops (two in one step) can have the first loop process the group, measuring it in most anyway desired, and the second loop output records with values computed in the first loop.
Example:
data have;
input name $ initial_key change_date indicator trxn_key trxn_date;
attrib change_date trxn_date informat=date9. format=date9.;
datalines;
ABC 1 17feb20 0 1 16feb20
ABC 2 21feb20 0 1 16feb20
ABC 3 25feb20 0 1 16feb20
ABC 1 17feb20 1 2 20feb20
ABC 2 21feb20 0 2 20feb20
ABC 3 25feb20 0 2 20feb20
ABC 1 17feb20 1 3 22feb20
ABC 2 21feb20 1 3 22feb20
ABC 3 25feb20 0 3 22feb20
ABC 1 17feb20 1 4 26feb20
ABC 2 21feb20 1 4 26feb20
ABC 3 25feb20 1 4 26feb20
;
data want;
* first dow loop, compute min and max_ associated values;
do until (last.trxn_key);
set have;
by name trxn_key;
if missing(min_date) or change_date < min_date then do;
min_date = change_date;
min_key = initial_key;
end;
if missing(max_date) or change_date > max_date then
if indicator then do;
max_date = change_date;
max_key = initial_key;
max_flag = 1;
end;
end;
* compute final values per business rules;
if max_flag then do;
final_date = max_date;
final_key = max_key;
end;
else do;
final_date = min_date;
final_key = min_key;
end;
* second dow loop, output with final values;
do until (last.trxn_key);
set have;
by name trxn_key;
OUTPUT;
end;
format final_date min_date max_date date9.;
drop min_: max_:;
run;

Spliting GROUP BY results into different columns

I have a column containing date ranges and the number of days passed associated to a specific ID (one to many), based on the number of records associated to it, I want those results split into columns instead of individual rows, so from this:
id_hr dd beg end
----------------------------------------
1 10 05/01/2019 15/01/2019
1 5 03/02/2019 08/02/2019
2 8 07/03/2019 15/03/2019
Could become this:
id_hr dd beg end dd beg end
--------------------------------- ---------------------
1 10 05/01/2019 15/01/2019 5 03/02/2019 08/02/2019
2 8 07/03/2019 15/03/2019
I did the same in a worksheet (pivot table) but the table became as slow as it could get, so I'm looking for a more friendly approach in SQL, I did a CTE which number the associated rows and then select each one and display them in new columns.
;WITH CTE AS(
SELECT PER_PRO, ID_HR, NOM_INC, rut_dv, dias_dur, INI, FIN,
ROW_NUMBER()OVER(PARTITION BY ID_HR ORDER BY SUBIDO) AS RN
FROM dbo.inf_vac WHERE PER_PRO = 201902
)
SELECT ID_HR, NOM_INC, rut_dv,
(case when rn = 1 then DIAS_DUR end) as DIAS_DUR1,
(case when rn = 1 then INI end) as INI1,
(case when rn = 1 then FIN end) as FIN1,
(case when rn = 2 then DIAS_DUR end) as DIAS_DUR2,
(case when rn = 2 then INI end) as INI2,
(case when rn = 2 then FIN end) as FIN2,
(case when rn = 3 then DIAS_DUR end) as DIAS_DUR3,
(case when rn = 3 then INI end) as INI3,
(case when rn = 3 then FIN end) as FIN3
FROM CTE
Which gets me each column on where it should be but not grouped. Using GROUP BY displays an error on the CTE select.
rn id_hr dd beg end dd beg end
----------------------------------- ------------------------
1 1 10 05/01/2019 15/01/2019 NULL NULL NULL
2 1 NULL NULL NULL 5 03/02/2019 08/02/2019
1 2 8 07/03/2019 15/03/2019 NULL NULL NULL
Is there any way to group them on the second select?
You have additional columns in the result set that are not in the query. However, this should work:
SELECT ID_HR,
max(case when rn = 1 then DIAS_DUR end) as DIAS_DUR1,
max(case when rn = 1 then INI end) as INI1,
max(case when rn = 1 then FIN end) as FIN1,
max(case when rn = 2 then DIAS_DUR end) as DIAS_DUR2,
max(case when rn = 2 then INI end) as INI2,
max(case when rn = 2 then FIN end) as FIN2,
max(case when rn = 3 then DIAS_DUR end) as DIAS_DUR3,
max(case when rn = 3 then INI end) as INI3,
max(case when rn = 3 then FIN end) as FIN3
FROM CTE
GROUP BY ID_HR;
Yes, you can GROUP BY all the non-CASE columns, and apply MAX to each of the CASE-expression columns.

SQL- group by to find out whether a value followed by certain other value only

I have this below table
App No EventCode Event Date
---------------------------------------------------------
1 A 2010-01-01
1 B 2010-01-02
1 C 2010-01-03
1 A 2010-01-10
2 A 2010-01-04
2 C 2010-01-05
2 B 2010-01-06
3 A 2010-01-01
3 D 2010-01-11
4 A 2011-01-01
4 D 2011-01-02
4 C 2011-03-03
I need to find out whether the App No has atleast an event A followed by Event C without having Event B in between. Please advise if this can be done using group by App No. I already have a complex query that groups by app no for calculating various values. I need to integrate this one to that.
The result table should look like
[App No] [A Immediately Followed By C] [Max A date]
-------------------------------------------------------
1 0 2010-01-10
2 1 2010-01-04
3 0 2010-01-01
4 1 2011-01-01
You should use lead() for this with aggregation and filtering:
select appno,
(case when max(case when eventcode = 'A' and next_ec = 'C' then 1 else 0 end) > 0
then 1 else 0
end) as flag,
max(case when eventcode = 'A' then date end) as max_a_date
from (select t.*,
lead(eventcode) over (partition by appno order by date) as next_ec
from t
where eventcode <> 'B'
) t
group by appno;
EDIT:
In SQL Server 2008, you can do:
select appno,
(case when max(case when eventcode = 'A' and next_ec = 'C' then 1 else 0 end) > 0
then 1 else 0
end) as flag,
max(case when eventcode = 'A' then date end) as max_a_date
from (select t.*,
t2.eventcode as next_ec
from t outer apply
(select top (1) t2.*
from t t2
where t2.appno = t.appno and t2.date > t.date
order by t2.date desc
) tnext
where eventcode <> 'B'
) t
group by appno;
I can't remember a way to solve this with aggregating, but you can use LEAD() (which you can use in SQL SERVER 2012+) or ROW_NUMBER() for SQL Server 2008+.
So here is sample data and temporary table.
...after reading through comments added on question, updated version:
CREATE TABLE #table_name (
app_no int,
event_code char(1),
event_date date
);
insert into #table_name
values
(1,'A',GETDATE()-100),
(1,'B',GETDATE()-10),
(1,'C',GETDATE()-1),
(2,'A',GETDATE()+10),
(2,'A',GETDATE()+1),
(2,'D',GETDATE()),
(2,'C',GETDATE()+10),
(3,'A',GETDATE()),
(3,'C',GETDATE()+100)
select *
,ROW_NUMBER() over (partition by 1 order by app_no) as rowid
into #table_name2
from #table_name
where event_code in ('A','B','C')
select org.app_no
,org.event_code
,case
when rez2.event_code is not null then 1
else 0
end as 'A followed by C'
,case
when rez2.max_date is not null then rez2.max_date
else org.event_date
end as 'Max A date'
from #table_name2 org
left outer join (
select t1.*,d.max_date
from #table_name2 t1
inner join #table_name2 t2 on t2.rowid=t1.rowid+1
left outer join
(
select app_no,event_code,MAX(event_date) as max_date
from #table_name
group by app_no,event_code
)d on d.app_no=t1.app_no and d.event_code=t1.event_code
where t1.event_code='A' and t2.event_code='C'
)rez2 on rez2.app_no=org.app_no and rez2.event_code=org.event_code and rez2.event_date=org.event_date
where org.event_code='A'
drop table #table_name
drop table #table_name2
;WITH cte
AS
(
SELECT tn.app_no
,tn.event_code
,tn.event_date
,LEAD(tn.event_code) OVER (PARTITION BY tn.app_no ORDER BY tn.event_date) NextEvent
FROM <Your_Table> AS tn
)
SELECT app_no
,MAX(CASE WHEN cte.NextEvent='C'
THEN 1
ELSE 0
END) AS [A Immediately Followed By C]
,MAX(event_date) event_date
FROM cte
WHERE event_code='A'
GROUP BY app_no

Performing additions within multiple columns of different rows in a SQL Server table based on the text in a column

I am trying to figure a query which performs some additions and subtractions of data in different rows and different columns based on the text/data in some other column in the same table.
Problem can be clearly addressed with the following example
Consider, I have table named Outright with four fields/columns with several records as follows
Product Term Bid Offer
------------------------------
A Aug14 P Q
A/B Aug14 R S
B Aug14 X Y
B Sep14 ab xy
B/C Sep14 pq rs
C Sep14 wx yz
When I run the query it should look for the Products that is separated by / in the above case there are two products of that type A/B and B/Cand then it should look for individual products based the those that are separated by / like we have a product A/B which is separated by a /, so it should look for product A and B with same term as A/B and perform some operations and return the data as follows
Product Term Bid Offer
------------------------------
A Aug14 a b
B Aug14 c d
B Sep14 ab cd
C Sep14 abc cde
in the above results
a=R+Y b=S+X
c=Q-S d=P-R
where P,Q,R,S,X,Y are Bid and Offer values from the table Outright
similar calculations are applied for all other data too like for B/C Sep14.. and many other
Example
Table Outright
A Oct14 -175 -75
B Oct14 125 215
A/B Oct14 NULL -150
Result should be
A Oct14 NULL -150+125=-25
B Oct14 -75-(-150)=75 NULL
The above values are calculated using the equations mentioned earlier
May I know a better way to solve it in SQL Server 2012?
Ok lets create some test data:
DECLARE #Outright TABLE
(
Product VARCHAR(10),
Term VARCHAR(10),
Bid VARCHAR(10),
Offer VARCHAR(10)
)
INSERT INTO #Outright
VALUES
('A', 'Aug14','P','Q'),
('A/B','Aug14','R','S'),
('B', 'Aug14','X','Y');
Making a cte to try to figure out the logic posted above and match the single product line to the multiproduct line
;WITH t AS
(
SELECT
a.*,
d.DRN,
d.Bid dBid,
d.Product dProduct,
d.Offer dOffer,
ROW_NUMBER() OVER (ORDER BY a.Product) RN
FROM #Outright a
OUTER APPLY
(
SELECT *,
ROW_NUMBER() OVER (ORDER BY d.Product) DRN
FROM #Outright d
WHERE d.Product LIKE (a.Product + '/%')
OR d.Product LIKE ('%/' + a.Product)
) d
WHERE d.Product IS NOT NULL
)
Now we try to implement the + - rules as stated above (bids to offers, offers to bids, etc)
SELECT
*,
CASE WHEN RN = 1 THEN FE1_1 + '+' + FE1_2 ELSE FE1_1 + '-' + FE1_2 END Col1,
CASE WHEN RN = 1 THEN FE2_1 + '+' + FE2_2 ELSE FE2_1 + '-' + FE2_2 END Col2
FROM
(
SELECT
MAX(CASE WHEN RN = 1 THEN Product END) Prod1,
MAX(CASE WHEN RN = 1 THEN Term END) Term1,
MAX(CASE WHEN RN = 1 THEN dBid END) FE1_1,
MAX(CASE WHEN RN = 2 THEN Offer END) FE1_2,
MAX(CASE WHEN RN = 2 THEN dOffer END) FE2_1,
MAX(CASE WHEN RN = 2 THEN Bid END) FE2_2,
1 RN
FROM t
UNION ALL
SELECT
MAX(CASE WHEN RN = 2 THEN Product END) Prod2,
MAX(CASE WHEN RN = 2 THEN Term END) Term2,
MAX(CASE WHEN RN = 1 THEN Offer END) FE3_1,
MAX(CASE WHEN RN = 2 THEN dOffer END) FE3_2,
MAX(CASE WHEN RN = 1 THEN Bid END) FE4_1,
MAX(CASE WHEN RN = 2 THEN dBid END) FE4_2,
2 RN
FROM t
) d
Here is the output, with some extra columns to show the data being pulled
Prod1 Term1 FE1_1 FE1_2 FE2_1 FE2_2 RN Col1 Col2
A Aug14 R Y S X 1 R+Y S+X
B Aug14 Q S P R 2 Q-S P-R