How to get correct summaries with analytics? - sql

I want to get summary numbers from the cust_detail table if a specific invoice_code appears in invoice_detail.
In this example, I'd like to report cust_detail summaries only for batches 10 and 20 because they are the ones with invoice_code='9999'. But the duplication in the invoice_detail table is skewing my numbers.
with
invoice_detail as
(
select '10' as invoice_batch, '9999' as invoice_code from dual union all
select '10' as invoice_batch, '9999' as invoice_code from dual union all
select '20' as invoice_batch, '1111' as invoice_code from dual union all
select '30' as invoice_batch, '9999' as invoice_code from dual
),
cust_detail as
(
select '1' as cust_id, '10' as invoice_batch, 40 as points_paid, 30 as points_earned, 30 as points_delivered from dual union all
select '1' as cust_id, '20' as invoice_batch, 10 as points_paid, 10 as points_earned, 10 as points_delivered from dual union all
select '1' as cust_id, '30' as invoice_batch, 20 as points_paid, 15 as points_earned, 5 as points_delivered from dual
)
select cust_id,
sum(points_paid) over (partition by c.invoice_batch
order by cust_id) batch_total
from cust_detail c
inner join invoice_detail i on c.invoice_batch=i.invoice_batch
where i.invoice_code = '9999';
Desired results:
CUST_ID PAID EARNED DELIVERED TOT_PAID TOT_EARNED TOT_DELIVERED
--------- ------ -------- ----------- ---------- ------------ ---------------
1 40 30 30 60 45 40
1 20 15 5 60 45 40

You can remove duplications from invoice_detail with distinct before join:
with invoice_detail as
(
select '10' as invoice_batch, '9999' as invoice_code from dual union all
select '10' as invoice_batch, '9999' as invoice_code from dual union all
select '20' as invoice_batch, '1111' as invoice_code from dual union all
select '30' as invoice_batch, '9999' as invoice_code from dual
),
cust_detail as
(
select '1' as cust_id, '10' as invoice_batch, 40 as points_paid, 30 as points_earned, 30 as points_delivered from dual union all
select '1' as cust_id, '20' as invoice_batch, 10 as points_paid, 10 as points_earned, 10 as points_delivered from dual union all
select '1' as cust_id, '30' as invoice_batch, 20 as points_paid, 15 as points_earned, 5 as points_delivered from dual
)
select cust_id
,points_paid
,points_earned
,points_delivered
,sum(points_paid) over (partition by c.cust_id) as tot_paid
,sum(points_earned) over (partition by c.cust_id) as tot_earned
,sum(points_delivered) over (partition by c.cust_id) as tot_delivered
from cust_detail c
join (select distinct * from invoice_detail) i
on c.invoice_batch=i.invoice_batch
where i.invoice_code = '9999';
Note that summaries include batches 10 and 30 because batch 20 with invoice_code='1111'.
SQL Fiddle

I am not sure what your desired results have to do with your query. But, I would expect your query to look more like this:
select cust_id,
sum(points_paid) over (partition by cust_id) as batch_total
from cust_detail c inner join
invoice_detail i
on c.invoice_batch=i.invoice_batch
where i.invoice_code = '9999' ;

Related

SQL: How to split data from quaterly to monthly with date

I have the data in the sql table in quarterly format. I need to be able to split it into monthly with value split evenly ([value/3) in to each month. Can you please assist on how to achieve this using SQL? Thank you.
start
end
value
2022-01-01
2022-04-01
25629
2022-04-01
2022-07-01
993621
CREATE TABLE #your_tbl
("start_dt" timestamp, "end_dt" timestamp, "values" int)
;
INSERT INTO #your_tbl
("start_dt", "end_dt", "values")
VALUES
('2020-01-01 00:00:00', '2020-04-01 00:00:00', 114625),
('2020-04-01 00:00:00', '2020-07-01 00:00:00', 45216),
('2020-07-01 00:00:00', '2020-10-01 00:00:00', 513574)
DECLARE #datefrom datetime
DECLARE #dateto datetime
SET #datefrom='2022-04-01'
SET #dateto = '2022-07-01'
;WITH cte AS
(
SELECT #datefrom as MyDate
UNION ALL
SELECT DATEADD(month,1,MyDate)
FROM cte
WHERE DATEADD(month,1,MyDate)<#dateto
),
combined AS (
SELECT *
FROM #your_tbl q
JOIN cte m
ON YEAR(m.MyDate) >= q.start_dt
AND MONTH(m.MyDate) < q.end_dt
)
SELECT *, [values]/COUNT(1) OVER(PARTITION BY [start_dt], [end_dt]) as monthly_values
FROM combined
DROP TABLE #your_tbl
In Oracle can you use this script:
with mytable as (
select to_date('2022-01-01', 'YYYY-MM-DD') as startX, to_date('2022-04-01', 'YYYY-MM-DD') as endX, 25629 as valueX from dual union
select to_date('2022-04-01', 'YYYY-MM-DD') as startX, to_date('2022-07-01', 'YYYY-MM-DD') as endX, 993621 as valueX from dual union
select to_date('2022-07-01', 'YYYY-MM-DD') as startX, to_date('2022-10-01', 'YYYY-MM-DD') as endX, 21 as valueX from dual union
select to_date('2022-10-01', 'YYYY-MM-DD') as startX, to_date('2023-01-01', 'YYYY-MM-DD') as endX, 7777 as valueX from dual
),
mymonths as (
select '01' as month_n from dual union
select '02' as month_n from dual union
select '03' as month_n from dual union
select '04' as month_n from dual union
select '05' as month_n from dual union
select '06' as month_n from dual union
select '07' as month_n from dual union
select '08' as month_n from dual union
select '09' as month_n from dual union
select '10' as month_n from dual union
select '11' as month_n from dual union
select '12' as month_n from dual
)
select month_n, startX, valueX/3
from mytable, mymonths
where month_n between to_char(startX, 'MM') and to_char(endX-1, 'MM');
MONTHS_N STARTX VALUEX/3
-------- ---------- ----------
01 01/01/2022 8543
02 01/01/2022 8543
03 01/01/2022 8543
04 01/04/2022 331207
05 01/04/2022 331207
06 01/04/2022 331207
07 01/07/2022 7
08 01/07/2022 7
09 01/07/2022 7
10 01/10/2022 2592,33333
11 01/10/2022 2592,33333
12 01/10/2022 2592,33333
Thank you.
Assuming you can figure out how to generate monthly dates, which is RDBMS dependent, here's a solution that might work depending on if you can use window functions.
Note this doesn't hard-code divide by 3 in case you're in a partial quarter.
WITH combined AS (
SELECT *,
FROM your_tbl q
JOIN monthly_dates m
ON m.monthly_dt >= q.start_dt
AND m.monthly_dt < q.end_dt
)
SELECT *
, values / COUNT(1) OVER(PARTITION BY start_dt, end_dt) as monthly_values
FROM combined
sqlfiddle

SQL query for statistic info

So let's assume I have two tables, cars and engines. Every car has an id column and sold_date column which points to a date when the car was sold. Every engine has car_id and type columns, where the first one is a foreign key to the cars table and the second is an engine's type name, which can be anything from V1 to V999.
So what I want to get is a list of dates from let's say August 1st to August 3rd with every type of engine and a number of sold cars like this:
sold_date engine_type number_of_sold_cards
08.01.2015 V8 6
08.01.2015 V6 8
08.01.2015 V4 9
08.02.2015 V8 15
08.02.2015 V6 0
08.02.2015 V4 5
08.03.2015 V8 4
08.03.2015 V6 6
08.03.2015 V4 0
The example assumes that for these 3 days were sold only cars with engines' types of V8, V6 and V4. What it means is that if there had been sold 5 types of engines for the period (V8, V6, V4, V2, V0) instead, I'd need 5 rows for every date
Partition outer join to the rescue!
with cars as (select 1 id, to_date('01/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 2 id, to_date('01/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 3 id, to_date('02/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 4 id, to_date('03/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 5 id, to_date('01/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 6 id, to_date('01/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 7 id, to_date('03/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 8 id, to_date('03/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 9 id, to_date('02/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 10 id, to_date('02/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 11 id, to_date('02/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 12 id, to_date('04/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 13 id, to_date('04/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 14 id, to_date('04/08/2015', 'dd/mm/yyyy') sold_date from dual union all
select 14 id, to_date('04/08/2015', 'dd/mm/yyyy') sold_date from dual),
engines as (select 1 car_id, 'V8' engine_type from dual union all
select 2 car_id, 'V8' engine_type from dual union all
select 3 car_id, 'V8' engine_type from dual union all
select 4 car_id, 'V8' engine_type from dual union all
select 5 car_id, 'V4' engine_type from dual union all
select 6 car_id, 'V4' engine_type from dual union all
select 7 car_id, 'V4' engine_type from dual union all
select 8 car_id, 'V4' engine_type from dual union all
select 9 car_id, 'V6' engine_type from dual union all
select 10 car_id, 'V6' engine_type from dual union all
select 11 car_id, 'V6' engine_type from dual union all
select 12 car_id, 'V6' engine_type from dual union all
select 13 car_id, 'V2' engine_type from dual union all
select 14 car_id, 'V2' engine_type from dual union all
select 15 car_id, 'V0' engine_type from dual),
engine_types as (select distinct engine_type from engines),
res as (select c.id car_id,
e.engine_type,
c.sold_date
from engines e
inner join cars c on (e.car_id = c.id)),
final_res as (select et.engine_type,
res.sold_date
from engine_types et
left outer join res on (et.engine_type = res.engine_type)),
dates as (select to_date('01/08/2015', 'dd/mm/yyyy') - 1 + level dt -- paramaterise the start date
from dual
connect by level <= to_date('03/08/2015', 'dd/mm/yyyy') - to_date('01/08/2015', 'dd/mm/yyyy') + 1 -- paramaterise the start and end dates
)
select dts.dt,
fr.engine_type,
count(fr.sold_date) cnt
from dates dts
left outer join final_res fr partition by (fr.engine_type) on (dts.dt = fr.sold_date)
group by dts.dt,
fr.engine_type
order by dts.dt,
fr.engine_type;
DT ENGINE_TYPE CNT
---------- ----------- ----------
01/08/2015 V0 0
01/08/2015 V2 0
01/08/2015 V4 2
01/08/2015 V6 0
01/08/2015 V8 2
02/08/2015 V0 0
02/08/2015 V2 0
02/08/2015 V4 0
02/08/2015 V6 3
02/08/2015 V8 1
03/08/2015 V0 0
03/08/2015 V2 0
03/08/2015 V4 2
03/08/2015 V6 0
03/08/2015 V8 1
The first and second subqueries ("cars" and "engines") are just mimicking your tables; you would not need to include them in your query.
The "engine_types" subquery is just getting the distinct list of engine_types used in the engines table. If you have some other table that lists the available engine_types, then use that instead.
The "dates" subquery is just generating a list of dates between a given date range - in an ideal world, the start and end dates would be parameterised (assuming this is being run in PL/SQL or some such).
The "res" subquery does the join on the cars and engines table, to get the type of engine sold on each date.
The "final_res" subquery outer joins the res and engine_types subqueries, so that every engine type is listed, along with the sold_date, if it was sold.
Once you have that, then it's easy to do a partition outer join on the final_res subquery to the dates subquery.
You need to join cars and engine tables. Need to count the sold cars and group by sold_date and engine_type:
select c.sold_date
,e.engine_type
,count(*) as number_of_sold_cars
from cars c
inner join engines e on c.id = e.car_id
where c.sold_date between date '2015-08-01' and date '2015-08-03'
group by c.sold_date,e.engine_type
Update:
With this query you will have all engine types sold in the selected period. You will have 0 for number_of_sold_cars if this engine type is not sold on the particular date:
with engine_type as (
select distinct e.engine_type
from cars c
inner join engines e on c.id = e.car_id
where c.sold_date between date '2015-08-01' and date '2015-08-03'
)
select c.sold_date
,t.engine_type
,count(*) as number_of_sold_cars
from cars c
inner join engines e on c.id = e.car_id
left join enginte_type t on t.engine_type = e.engine_type
where c.sold_date between date '2015-08-01' and date '2015-08-03'
group by c.sold_date,t.engine_type

Transposing rows into columns

I have following data
with sample_data as (select to_date('05/01/2015', 'dd/mm/yyyy') dt, '1' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '2' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '3' Period, 'P' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '4' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '5' Period, 'P' code from dual
)
select * from sample_data
That gives me simple query results as below
DT PERIOD CODE
2015-01-05 00:00:00 1 A
2015-01-05 00:00:00 2 A
2015-01-05 00:00:00 3 P
2015-01-05 00:00:00 4 A
2015-01-09 00:00:00 5 P
I would like to transpose the results like this -
DATE ATTENDANCE
2015-01-05 12345
AAPAP
How can I do that?
Thanks a bunch!
This is an Oracle SQL question and not PL/SQL!
That could be something like:
select to_char(dt,'YYYY-MM-DD') date,listagg(period) within group (order by period)||chr(13)||chr(10)||listagg(code) within group (order by period) attendance
from (
with sample_data as (select to_date('05/01/2015', 'dd/mm/yyyy') dt, '1' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '2' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '3' Period, 'P' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '4' Period, 'A' code from dual union all
select to_date('05/01/2015', 'dd/mm/yyyy') dt, '5' Period, 'P' code from dual
) select * from sample_data
) group by to_char(dt,'YYYY-MM-DD')
Note that the function listagg only can be used from Oracle 11.2. If you are on an earlier version you can use xmlagg, but thats a bit more clotted.

How to get summary based on another table group summary?

Business Rules: Get total cust_points for customer only if total(trans_amount) for a trans_code grouping > 0.
For customer #1, the summary at date_code level (code 10) is > 0 so cust_points total = 70.
For customer #2 only code 20 group totals > 0 so total only 75 total cust_points
Here's my query:
with customers as
(select '1' as cust_id, 10 as date_code, 30 as cust_points from dual union all
select '1' as cust_id, 10 as date_code, 40 as cust_points from dual union all
select '1' as cust_id, 20 as date_code, 22 as cust_points from dual union all --These points should not total because trans_amount sum for code 20 is less than 0
select '1' as cust_id, 40 as date_code, 33 as cust_points from dual union all -- These points should not total because there is not trans_amounts > 0 for date_code
select '2' as cust_id, 10 as date_code, 20 as cust_points from dual union all
select '2' as cust_id, 20 as date_code, 65 as cust_points from dual union all
select '2' as cust_id, 20 as date_code, 10 as cust_points from dual
),
transactions_row as
(
select '1' as cust_id, '10' as trans_code, 10.00 as trans_amount from dual union all
select '1' as cust_id, '20' as trans_code, -15.00 as trans_amount from dual union all
select '1' as cust_id, '20' as trans_code, -20.00 as trans_amount from dual union all
select '1' as cust_id, '20' as trans_code, -10.00 as trans_amount from dual union all
select '1' as cust_id, '30' as trans_code, 30.00 as trans_amount from dual union all
select '1' as cust_id, '20' as trans_code, -20.00 as trans_amount from dual union all
select '2' as cust_id, '10' as trans_code, -50.00 as trans_amount from dual union all
select '2' as cust_id, '20' as trans_code, 20.00 as trans_amount from dual
)
select cust_id,
sum(cust_points)
from customers
where cust_id in
(
select cust_id
from (
select cust_id, trans_code, sum(trans_amount)
from transactions_row
group by cust_id, trans_code
having sum(trans_amount) > 0
)
)
group by cust_id
Desired Results
CUST_ID CUST_POINTS
1 70 /* (30 because total trans_amount for tran_code(10) > 0 +
40 because total trans_amount for tran_code(10) > 0) */
2 75 /* Do not include the 20 points because total trans_amt for 10 < 0 */
Here's one way using exists:
select cust_id,
sum(cust_points)
from customers c
where exists (
select 1
from transactions_row tr
where tr.trans_code = c.date_code
and tr.cust_id = c.cust_id
group by tr.trans_code, tr.cust_id
having sum(tr.trans_amount) > 0
)
group by cust_id
SQL Fiddle Demo

How to calculate price change over 3 years in SQL query

I need to calculate the price change of an item (both in cost and % change) over the last three years.
The table has four fields:
SKU_no, Date_updated, Price, Active_flag
When the Active_flag field is A, the item is active, when I it is inactive. Some items haven't changed prices in years so they won't have three years of entries with an inactive flag.
Sample table
SKU_NO Update_date Price Active_flag
30 1/1/1999 40.8 I
33 1/1/2014 70.59 A
33 1/1/2013 67.23 I
33 1/1/2012 60.03 I
33 1/1/2011 55.08 I
33 1/1/2010 55.08 I
34 1/1/2009 51 A
36 1/1/2014 70.59 A
36 1/1/2013 67.23 I
36 1/1/2012 60.03 I
38 1/1/2002 43.32 A
38 1/1/2001 43.32 I
38 4/8/2000 43.32 I
38 1/1/1999 43.32 I
39 1/1/2014 73.08 A
39 1/1/2013 69.6 I
39 1/1/2012 62.13 I
39 1/1/2011 57 I
39 1/1/2010 57 I
39 1/1/2009 52.8 I
This is the first query I wrote. I'm not too familiar with complex calculations
select
s.VENDOR,
s.FISCAL_YEAR,
s.FISCAL_MONTH_NO,
s.FISCAL_YEAR||'_'||FISCAL_MONTH_NO as PERIOD,
CASE WHEN S.COST_USED_FLAG IN ('CONTRACT') THEN 'CONTRACT' ELSE 'NON-CONTRACT' END AS CONTRACT_TYPE,
CASE WHEN ((s.FISCAL_YEAR = 2014 AND FISCAL_MONTH_NO <=9) OR (FISCAL_YEAR = 2013 AND FISCAL_MONTH_NO >=10)) THEN 'CP_1'
WHEN ((s.FISCAL_YEAR = 2013 AND FISCAL_MONTH_NO <= 9) OR (FISCAL_YEAR = 2012 AND FISCAL_MONTH_NO >=10)) THEN 'CP_2'
WHEN ((s.FISCAL_YEAR = 2012 AND FISCAL_MONTH_NO <= 9) OR (FISCAL_YEAR = 2011 AND FISCAL_MONTH_NO >=10)) THEN 'CP_3'
ELSE 'NULL' END CAGR_PERIODS,
CASE WHEN s.MARKET IN ('PO', 'SC', 'OC') THEN 'PC' ELSE 'EC' END AS MARKET_TYPE,
s.MARKET,
s.COST_PLUS_FLAG,
s.COST_USED_FLAG,
LPAD(S.PC_ITEM_NO,6,'0') AS NEW_ITEM_NO,
s.PC_ITEM_NO,
i.ITEM_NO,
i.VEND_CAT_NUM,
i.DESCRIPTION,
s.PC_PROD_CAT,
s.PC_PROD_SUBCAT,
i.SELL_UOM,
i.QTY_PER_SELL_UOM,
i.PRIMARY_UOM,
i.HEAD_CONV_FACT,
SUM(s.QTY_EACH) AS QUANTITY_SOLD,
SUM(s.EXT_GROSS_COGS) AS TOTAL_COGS,
SUM(s.EXT_GROSS_COGS)/ SUM(s.QTY_EACH) as NET_SALES,
SUM(s.EXT_SALES)/ SUM(s.QTY_EACH) as ASP,
SUM(s.EXT_SALES) AS TOTAL_SALES,
SUM(S.EXT_SALES) - SUM(S.EXT_GROSS_COGS) as GROSS_PROFIT
from SIXSIGMA.CIA_ALL_SALES_TREND_DATA s
INNER JOIN MGMSH.ITEM i
ON S.PC_ITEM_NO = I.ITEM_NO
WHERE S.VENDOR = 'BD' AND
(S.EXT_SALES IS NOT NULL AND S.FISCAL_YEAR IN ('2013','2012','2011'))
GROUP BY
s.VENDOR,
s.FISCAL_YEAR,
s.FISCAL_MONTH_NO,
s.FISCAL_YEAR||'_'||FISCAL_MONTH_NO,
CASE WHEN s.MARKET IN ('PO', 'SC', 'OC') THEN 'PC' ELSE 'EC' END,
CASE WHEN S.COST_USED_FLAG IN ('CONTRACT') THEN 'CONTRACT' ELSE 'NON-CONTRACT' END,
CASE WHEN ((s.FISCAL_YEAR = 2014 AND FISCAL_MONTH_NO <=9) OR (FISCAL_YEAR = 2013 AND FISCAL_MONTH_NO >=10)) THEN 'CP_1'
WHEN ((s.FISCAL_YEAR = 2013 AND FISCAL_MONTH_NO <= 9) OR (FISCAL_YEAR = 2012 AND FISCAL_MONTH_NO >=10)) THEN 'CP_2'
WHEN ((s.FISCAL_YEAR = 2012 AND FISCAL_MONTH_NO <= 9) OR (FISCAL_YEAR = 2011 AND FISCAL_MONTH_NO >=10)) THEN 'CP_3'
ELSE 'NULL' END,
s.MARKET,
s.COST_USED_FLAG,
s.COST_PLUS_FLAG,
s.PC_ITEM_NO,
s.PC_PROD_CAT,
i.SELL_UOM,
i.QTY_PER_SELL_UOM,
i.PRIMARY_UOM,
i.HEAD_CONV_FACT,
i.DESCRIPTION,
i.VEND_CAT_NUM,
s.PC_PROD_SUBCAT,
i.ITEM_NO
ORDER BY s.PC_ITEM_NO,s.FISCAL_YEAR, s.FISCAL_MONTH_NO
There are several ways to approach this, but I would recommend a windowing function such as LAG or LEAD. With these functions, you can reference neighboring rows. For example:
lead(column, offset, default) over (partition by some_column order by column)
And in the example below:
lead(price, 1, price) over (partition by sku_no order by update_date desc)
Here is a working example with sample data:
with sample_data as (
select '30' sku_no, to_date('1/1/1999','DD/MM/YYYY') update_date, 40.8 price, 'I' active_flag from dual union all
select '33', to_date('1/1/2014','DD/MM/YYYY'), 70.59, 'A' from dual union all
select '33', to_date('1/1/2013','DD/MM/YYYY'), 67.23, 'I' from dual union all
select '33', to_date('1/1/2012','DD/MM/YYYY'), 60.03, 'I' from dual union all
select '33', to_date('1/1/2011','DD/MM/YYYY'), 55.08, 'I' from dual union all
select '33', to_date('1/1/2010','DD/MM/YYYY'), 55.08, 'I' from dual union all
select '34', to_date('1/1/2009','DD/MM/YYYY'), 51 , 'A' from dual union all
select '36', to_date('1/1/2014','DD/MM/YYYY'), 70.59, 'A' from dual union all
select '36', to_date('1/1/2013','DD/MM/YYYY'), 67.23, 'I' from dual union all
select '36', to_date('1/1/2012','DD/MM/YYYY'), 60.03, 'I' from dual union all
select '38', to_date('1/1/2002','DD/MM/YYYY'), 43.32, 'A' from dual union all
select '38', to_date('1/1/2001','DD/MM/YYYY'), 43.32, 'I' from dual union all
select '38', to_date('4/8/2000','DD/MM/YYYY'), 43.32, 'I' from dual union all
select '38', to_date('1/1/1999','DD/MM/YYYY'), 43.32, 'I' from dual union all
select '39', to_date('1/1/2014','DD/MM/YYYY'), 73.08, 'A' from dual union all
select '39', to_date('1/1/2013','DD/MM/YYYY'), 69.6 , 'I' from dual union all
select '39', to_date('1/1/2012','DD/MM/YYYY'), 62.13, 'I' from dual union all
select '39', to_date('1/1/2011','DD/MM/YYYY'), 57 , 'I' from dual union all
select '39', to_date('1/1/2010','DD/MM/YYYY'), 57 , 'I' from dual union all
select '39', to_date('1/1/2009','DD/MM/YYYY'), 52.8 , 'I' from dual)
select
sku_no,
update_date,
price,
lead(price,1, price) over (partition by sku_no order by update_date desc) prior_price, -- Showing the offset
price - lead(price,1, price) over (partition by sku_no order by update_date desc) price_difference, -- Calculate the difference
round((price - lead(price,1, price) over (partition by sku_no order by update_date desc)) * 100 /price, 2) percent_change -- Calculate the percentage
from sample_data
where update_date >= add_months(trunc(sysdate,'YYYY'),-36); -- You said in the last three years
You can also use LAG with a different order by sort. If you want to calculate the difference from three years prior, I would suggest using the KEEP function.