Self join with aggregate function or some better way - sql

I am using SQL-Server and have a table of my Purchase orders (stock). But stuck in a query while I was trying to get my All Available stock with its Latest Cost Price and Latest Selling Price.
I made a query it run successfully, but i need some better and optimized way to do this, because it will get slow when table have n number of records.
Query Sample:
SELECT
po.ProductID, sum(po.AvailableQty) as AvailableQty,
(select top 1 po2.CostPrice from Sales_PurchaseOrders po2 where po2.PurchasedAt=max(po.PurchasedAt)) as CostPrice,
(select top 1 po2.SellingPrice from Sales_PurchaseOrders po2 where po2.PurchasedAt=max(po.PurchasedAt)) as SellingPrice
FROM
Sales_PurchaseOrders po
INNER JOIN Sales_Products p on p.ProductID=po.ProductID
GROUP BY po.ProductID
Table Data:
PurchaseOrderID ProductID CostPrice SellingPrice AvailableQty PurchasedAt
--------------- ----------- --------------------------------------- --------------------------------------- --------------------------------------- -----------------------
1 1 90.000000 100.000000 2.000000 2016-07-28 00:00:00.000
2 1 33.580000 50.000000 0.000000 2016-06-28 00:00:00.000
3 2 200.000000 240.000000 15.000000 2016-07-30 00:00:00.000
4 1 50.000000 60.000000 0.000000 2016-08-02 00:00:00.000
5 1 50.000000 60.000000 1.000000 2016-08-03 00:00:00.000
6 1 100.000000 110.000000 6.000000 2016-08-04 00:00:00.000
7 1 25.000000 30.000000 3.000000 2016-08-03 00:00:00.000
8 1 20.000000 30.000000 0.000000 2016-07-30 00:00:00.000
1007 1 100.000000 200.000000 2.000000 2016-09-24 00:00:00.000
Query Result:
ProductID AvailableQty CostPrice SellingPrice
----------- --------------------------------------- --------------------------------------- ---------------------------------------
1 14.000000 100.000000 200.000000
2 15.000000 200.000000 240.000000
May be via some kind of aggregate function, or any other better optimized way to do this.
Thanks,

I think this does what you want:
SELECT po.ProductID, sum(po.AvailableQty) as AvailableQty,
MAX(last_CostPrice), MAX(last_SellingPrice)
FROM (SELECT po.*,
FIRST_VALUE(CostPrice) OVER (PARTITION BY ProductId ORDER BY PurchasedAt DESC) as last_CostPrice,
FIRST_VALUE(SellingPrice) OVER (PARTITION BY ProductId ORDER BY PurchasedAt DESC) as last_SellingPrice
FROM Sales_PurchaseOrders po
) po
GROUP BY po.ProductID;
Notes:
The table Sales_Products seems totally unnecessary for the query.
You probably want the most recent cost and selling price for the product, not for all products.
You can use FIRST_VALUE() in the subquery to get this information.

Dear Mehmood Try this.
;with wt_table
as
(
select ROW_NUMBER() over(partition by po.ProductID order by PurchasedAt desc) as Num,
AvailableQty=sum(po.AvailableQty) over(partition by po.ProductID),
po.ProductID,
po.CostPrice,
po.SellingPrice,
po.PurchasedAt
From #Sales_PurchaseOrders po)
select * from wt_table where Num=1

try this:
with Sales_PurchaseOrders(PurchaseOrderID,ProductID,CostPrice,SellingPrice,AvailableQty,PurchasedAt)AS(
select 1,1,90.000000,100.000000,2.000000,'2016-07-28 00:00:00.000' union all
select 2,1,33.580000,50.000000,0.000000,'2016-06-28 00:00:00.000' union all
select 3,2,200.000000,240.000000,15.000000,'2016-07-30 00:00:00.000' union all
select 4,1,50.000000,60.000000,0.000000,'2016-08-02 00:00:00.000' union all
select 5,1,50.000000,60.000000,1.000000,'2016-08-03 00:00:00.000' union all
select 6,1,100.000000,110.000000,6.000000,'2016-08-04 00:00:00.000' union all
select 7,1,25.000000,30.000000,3.000000,'2016-08-03 00:00:00.000' union all
select 8,1,20.000000,30.000000,0.000000,'2016-07-30 00:00:00.000' union all
select 1007,1,100.000000,200.000000,2.000000,'2016-09-24 00:00:00.000'
)
select * from (
SELECT
po.ProductID, sum(po.AvailableQty)over(partition by po.ProductID) as AvailableQty,CostPrice,SellingPrice,
row_number()over(partition by po.ProductID order by po.PurchasedAt desc) as seq
FROM Sales_PurchaseOrders po
) as t where t.seq=1
ProductID AvailableQty CostPrice SellingPrice seq
1 1 14,000000 100,000000 200,000000 1
2 2 15,000000 200,000000 240,000000 1

Related

Get max date for each from either of 2 columns

I have a table like below
AID BID CDate
-----------------------------------------------------
1 2 2018-11-01 00:00:00.000
8 1 2018-11-08 00:00:00.000
1 3 2018-11-09 00:00:00.000
7 1 2018-11-15 00:00:00.000
6 1 2018-12-24 00:00:00.000
2 5 2018-11-02 00:00:00.000
2 7 2018-12-15 00:00:00.000
And I am trying to get a result set as follows
ID MaxDate
-------------------
1 2018-12-24 00:00:00.000
2 2018-12-15 00:00:00.000
Each value in the id columns(AID,BID) should return the max of CDate .
ex: in the case of 1, its max CDate is 2018-12-24 00:00:00.000 (here 1 appears under BID)
in the case of 2 , max date is 2018-12-15 00:00:00.000 . (here 2 is under AID)
I tried the following.
1.
select
g.AID,g.BID,
max(g.CDate) as 'LastDate'
from dbo.TT g
inner join
(select AID,BID,max(CDate) as maxdate
from dbo.TT
group by AID,BID)a
on (a.AID=g.AID or a.BID=g.BID)
and a.maxdate=g.CDate
group by g.AID,g.BID
and 2.
SELECT
AID,
CDate
FROM (
SELECT
*,
max_date = MAX(CDate) OVER (PARTITION BY [AID])
FROM dbo.TT
) AS s
WHERE CDate= max_date
Please suggest a 3rd solution.
You can assemble the data in a table expression first, and the compute the max for each value is simple. For example:
select
id, max(cdate)
from (
select aid as id, cdate from t
union all
select bid, cdate from t
) x
group by id
You seem to only care about values that are in both columns. If this interpretation is correct, then:
select id, max(cdate)
from ((select aid as id, cdate, 1 as is_a, 0 as is_b
from t
) union all
(select bid as id, cdate, 1 as is_a, 0 as is_b
from t
)
) ab
group by id
having max(is_a) = 1 and max(is_b) = 1;

Sql group by latest repeated field

I don't even know what's a good title for this question.
But I'm having a table:
create table trans
(
[transid] INT IDENTITY (1, 1) NOT NULL,
[customerid] int not null,
[points] decimal(10,2) not null,
[date] datetime not null
)
and records:
--cus1
INSERT INTO trans ( customerid , points , date )
VALUES ( 1, 10, '2016-01-01' ) , ( 1, 20, '2017-02-01' ) , ( 1, 22, '2017-03-01' ) ,
( 1, 24, '2018-02-01' ) , ( 1, 50, '2018-02-25' ) , ( 2, 44, '2016-02-01' ) ,
( 2, 20, '2017-02-01' ) , ( 2, 32, '2017-03-01' ) , ( 2, 15, '2018-02-01' ) ,
( 2, 10, '2018-02-25' ) , ( 3, 10, '2018-02-25' ) , ( 4, 44, '2015-02-01' ) ,
( 4, 20, '2015-03-01' ) , ( 4, 32, '2016-04-01' ) , ( 4, 15, '2016-05-01' ) ,
( 4, 10, '2017-02-25' ) , ( 4, 10, '2018-02-27' ) ,( 4, 20, '2018-02-28' ) ,
( 5, 44, '2015-02-01' ) , ( 5, 20, '2015-03-01' ) , ( 5, 32, '2016-04-01' ) ,
( 5, 15, '2016-05-01' ) ,( 5, 10, '2017-02-25' );
-- selecting the data
select * from trans
Produces:
transid customerid points date
----------- ----------- --------------------------------------- -----------------------
1 1 10.00 2016-01-01 00:00:00.000
2 1 20.00 2017-02-01 00:00:00.000
3 1 22.00 2017-03-01 00:00:00.000
4 1 24.00 2018-02-01 00:00:00.000
5 1 50.00 2018-02-25 00:00:00.000
6 2 44.00 2016-02-01 00:00:00.000
7 2 20.00 2017-02-01 00:00:00.000
8 2 32.00 2017-03-01 00:00:00.000
9 2 15.00 2018-02-01 00:00:00.000
10 2 10.00 2018-02-25 00:00:00.000
11 3 10.00 2018-02-25 00:00:00.000
12 4 44.00 2015-02-01 00:00:00.000
13 4 20.00 2015-03-01 00:00:00.000
14 4 32.00 2016-04-01 00:00:00.000
15 4 15.00 2016-05-01 00:00:00.000
16 4 10.00 2017-02-25 00:00:00.000
17 4 10.00 2018-02-27 00:00:00.000
18 4 20.00 2018-02-28 00:00:00.000
19 5 44.00 2015-02-01 00:00:00.000
20 5 20.00 2015-03-01 00:00:00.000
21 5 32.00 2016-04-01 00:00:00.000
22 5 15.00 2016-05-01 00:00:00.000
23 5 10.00 2017-02-25 00:00:00.000
I'm trying to group all the customerid and sum their points. But here's the catch, If the trans is not active for 1 year(the next tran is 1 year and above), the points will be expired.
For this case:
Points for each customers should be:
Customer1 20+22+24+50
Customer2 20+32+15+10
Customer3 10
Customer4 10+20
Customer5 0
Here's what I have so far:
select
t1.transid as transid1,
t1.customerid as customerid1,
t1.date as date1,
t1.points as points1,
t1.rank1 as rank1,
t2.transid as transid2,
t2.customerid as customerid2,
t2.points as points2,
isnull(t2.date,getUTCDate()) as date2,
isnull(t2.rank2,t1.rank1+1) as rank2,
cast(case when(t1.date > dateadd(year,-1,isnull(t2.date,getUTCDate()))) Then 0 ELSE 1 END as bit) as ShouldExpire
from
(
select transid,CustomerID,Date,points,
RANK() OVER(PARTITION BY CustomerID ORDER BY date ASC) AS RANK1
from trans
)t1
left join
(
select transid,CustomerID,Date,points,
RANK() OVER(PARTITION BY CustomerID ORDER BY date ASC) AS RANK2
from trans
)t2 on t1.RANK1=t2.RANK2-1
and t1.customerid=t2.customerid
which gives
from the above table,how do I check for ShouldExpire field having max(rank1) for customer, if it's 1, then totalpoints will be 0, otherwise,sum all the consecutive 0's until there are no more records or a 1 is met?
Or is there a better approach to this problem?
The following query uses LEAD to get the date of the next record withing the same CustomerID slice:
;WITH CTE AS (
SELECT transid, CustomerID, [Date], points,
LEAD([Date]) OVER (PARTITION BY CustomerID
ORDER BY date ASC) AS nextDate,
CASE
WHEN [date] > DATEADD(YEAR,
-1,
-- same LEAD() here as above
ISNULL(LEAD([Date]) OVER (PARTITION BY CustomerID
ORDER BY date ASC),
getUTCDate()))
THEN 0
ELSE 1
END AS ShouldExpire
FROM trans
)
SELECT transid, CustomerID, [Date], points, nextDate, ShouldExpire
FROM CTE
ORDER BY CustomerID, [Date]
Output:
transid CustomerID Date points nextDate ShouldExpire
-------------------------------------------------------------
1 1 2016-01-01 10.00 2017-02-01 1 <-- last exp. for 1
2 1 2017-02-01 20.00 2017-03-01 0
3 1 2017-03-01 22.00 2018-02-01 0
4 1 2018-02-01 24.00 2018-02-25 0
5 1 2018-02-25 50.00 NULL 0
6 2 2016-02-01 44.00 2017-02-01 1 <-- last exp. for 2
7 2 2017-02-01 20.00 2017-03-01 0
8 2 2017-03-01 32.00 2018-02-01 0
9 2 2018-02-01 15.00 2018-02-25 0
10 2 2018-02-25 10.00 NULL 0
11 3 2018-02-25 10.00 NULL 0 <-- no exp. for 3
12 4 2015-02-01 44.00 2015-03-01 0
13 4 2015-03-01 20.00 2016-04-01 1
14 4 2016-04-01 32.00 2016-05-01 0
15 4 2016-05-01 15.00 2017-02-25 0
16 4 2017-02-25 10.00 2018-02-27 1 <-- last exp. for 4
17 4 2018-02-27 10.00 2018-02-28 0
18 4 2018-02-28 20.00 NULL 0
19 5 2015-02-01 44.00 2015-03-01 0
20 5 2015-03-01 20.00 2016-04-01 1
21 5 2016-04-01 32.00 2016-05-01 0
22 5 2016-05-01 15.00 2017-02-25 0
23 5 2017-02-25 10.00 NULL 1 <-- last exp. for 5
Now, you seem to want to calculate the sum of points after the last expiration.
Using the above CTE as a basis you can achieve the required result with:
;WITH CTE AS (
... above query here ...
)
SELECT CustomerID,
SUM(CASE WHEN rnk = 0 THEN points ELSE 0 END) AS sumOfPoints
FROM (
SELECT transid, CustomerID, [Date], points, nextDate, ShouldExpire,
SUM(ShouldExpire) OVER (PARTITION BY CustomerID ORDER BY [Date] DESC) AS rnk
FROM CTE
) AS t
GROUP BY CustomerID
Output:
CustomerID sumOfPoints
-----------------------
1 116.00
2 77.00
3 10.00
4 30.00
5 0.00
Demo here
The tricky part here is to dump all points when they expire, and start accumulating them again. I assumed that if there was only one transaction that we don't expire the points until there's a new transaction, even if that first transaction was over a year ago now?
I also get a different answer for customer #5, as they do appear to have a "transaction chain" that hasn't expired?
Here's my query:
WITH ordered AS (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY customerid ORDER BY [date]) AS order_id
FROM
trans),
max_transid AS (
SELECT
customerid,
MAX(transid) AS max_transid
FROM
trans
GROUP BY
customerid),
not_expired AS (
SELECT
t1.customerid,
t1.points,
t1.[date] AS t1_date,
CASE
WHEN m.customerid IS NOT NULL THEN GETDATE()
ELSE t2.[date]
END AS t2_date
FROM
ordered t1
LEFT JOIN ordered t2 ON t2.customerid = t1.customerid AND t1.transid != t2.transid AND t2.order_id = t1.order_id + 1 AND t1.[date] > DATEADD(YEAR, -1, t2.[date])
LEFT JOIN max_transid m ON m.customerid = t1.customerid AND m.max_transid = t1.transid
),
max_not_expired AS (
SELECT
customerid,
MAX(t1_date) AS max_expired
FROM
not_expired
WHERE
t2_date IS NULL
GROUP BY
customerid)
SELECT
n.customerid,
SUM(n.points) AS points
FROM
not_expired n
LEFT JOIN max_not_expired m ON m.customerid = n.customerid
WHERE
ISNULL(m.max_expired, '19000101') < n.t1_date
GROUP BY
n.customerid;
It could be refactored to be simpler, but I wanted to show the steps to get to the final answer:
customerid points
1 116.00
2 77.00
3 10.00
4 30.00
5 57.00
can you try this:
SELECT customerid,
Sum(t1.points)
FROM trans t1
WHERE NOT EXISTS (SELECT 1
FROM trans t2
WHERE Datediff(year, t1.date, t2.date) >= 1)
GROUP BY t1.customerid
Hope it helps!
try this:
select customerid,Sum(points)
from trans where Datediff(year, date, GETDATE()) < 1
group by customerid
output:
customerid Points
1 - 74.00
2 - 25.00
3 - 10.00
4 - 30.00

Get last two entries of each account in table

I've got script that gives me all transactions for day for all accounts and sub accounts. His return you can see on the image. What I want, is return result as two last transactions for each accountId and subaccountId. Ideal return would be:
AccountId| SubAccountId| AmountInDay | Date
---------------------------------------------
210 | 1 | 0.00 |2017-06-20 00:00:00.000
210 | 1 | 0.00 |2017-06-05 00:00:00.000
1234 | 1 | 0.00 |2017-06-20 00:00:00.000
1234 | 1 | 0.00 |2017-06-05 00:00:00.000
This is the code of my script:
with CTE1 as
(
select top 2 AccountId, SubAccountId, [Date], sum(Amount_Amount) as Amount
from dbo.PayoutInstallment
group by accountId, SubAccountId, [Date]
)
, CTE2 as
(
select AccountId,SubAccountId, Amount_Amount, [Date],
dense_rank() over (partition by AccountId order by [Date] desc) as rn
from dbo.PayoutInstallment
)
select a1.AccountId,a1.SubAccountId, Sum(a1.Amount_Amount) as AmountInDay, a1.[Date]
from CTE2 a1
left join CTE2 a2
on a1.AccountId = a2.AccountId and a1.[Date] > a2.[Date]
and a2.rn = a1.rn+1
group by a1.[Date], a1.AccountId, a1.SubAccountId
order by a1.[Date] desc
EDIT
Sample Data
AccountId| SubAccountId| AmountInDay | Date
---------------------------------------------
210 | 1 | 0.00 |2017-03-15 00:00:00.000
210 | 1 | 0.00 |2017-04-20 00:00:00.000
210 | 1 | 100.00 |2017-05-17 00:00:00.000
210 | 1 | 1.00 |2017-06-05 00:00:00.000
210 | 1 | 1.00 |2017-06-05 00:00:00.000
1234 | 1 | 0.00 |2017-06-05 00:00:00.000
1234 | 1 | 0.00 |2017-06-05 00:00:00.000
1234 | 1 | 1.00 |2017-06-10 00:00:00.000
1234 | 1 | 1.00 |2017-04-10 00:00:00.000
I think you can use row_number and get 2 records as below:
Select * from (
Select AccountId, SubAccountId, [Date], sum(Amount_Amount) over (partition by accountid, SubAccountId, [Date])
,RowN = Row_number() over (partition by accountid, SubAccountId, [Date] order by [date] desc)
from dbo.PayoutInstallment
) a where a.RowN <= 2
Assume one day one transaction,
;WITH cte AS(SELECT *
, ROW_NUMBER() OVER (PARTITION BY AccountId, SubAccountId ORDER BY [Date] DESC) AS Rownum
FROM PayoutInstallment
)
SELECT *
, SUM(AmountInDay) OVER (PARTITION BY AccountId, SubAccountId) AS SumLast2days
FROM cte
WHERE Rownum<=2
If you want the SUM for the last two day you need to assign a number to each day. Then bring all the data related to those days by JOIN both dataset and then perform a GROUP BY
WITH cte as (
SELECT AccountId, SubAccountId, [Date],
ROW_NUMBER() OVER (PARTITION BY AccountId, SubAccountId
ORDER BY [Date] DESC) AS rn
FROM dbo.PayoutInstallment
)
SELECT P.AccountId,
P.SubAccountId,
P.[Date],
SUM(ammount)
FROM dbo.PayoutInstallment P
JOIN cte C
ON P.[Date] = C.[Date]
AND P.AccountId = C.AccountId
AND P.SubAccountId = C.SubAccountId
WHERE rn <= 2 -- Just the last day of each account, subacount
GROUP BY P.AccountId,
P.SubAccountId,
P.[Date]
I see you are using GROUP BY, so if you want the results to be sorted after the grouping, you should use HAVING if you want otherwise you should use WHERE. Here is an example of a WHERE clause you can use in your query to get only results between the last two days.
WHERE (a1.[Date] BETWEEN GETDATE()AND GETDATE()-2)

New column which indicate min date from group of value

I would like to add new column which will be indicating min value of subgroup.
Id ShopId OrderDate
12232018 12229018 2011-01-01 00:00:00.000
12232018 12229018 2012-01-01 00:00:00.000
12232018 12394018 2012-02-02 00:00:00.000
12232018 11386005 2012-03-01 00:00:00.000
12232018 14347023 2012-04-02 00:00:00.000
12232018 14026026 2014-03-16 00:00:00.000
Here is the result I want to get:
NewCol Id ShopId OrderDate
1 12232018 12229018 2011-01-01 00:00:00.000
1 12232018 12229018 2012-01-01 00:00:00.000
0 12232018 12394018 2012-02-02 00:00:00.000
0 12232018 11386005 2012-03-01 00:00:00.000
0 12232018 14347023 2012-04-02 00:00:00.000
0 12232018 14026026 2014-03-16 00:00:00.000
Because ShopId have min OrderDate for Id I would like to assign '1' to this ShopId.
You can use min with windowing function to get this as below:
select NewCol = Case when orderdate = min(orderdate) over() then 1 else 0 end,*
from yourtable
--Probably you might require to add Partition by Id or shopId depends on requirement
Try this:
SELECT Id, ShopId, OrderDate,
CASE
WHEN MIN(OrderDate) OVER (PARTITION BY Id, ShopId) =
MIN(OrderDate) OVER (PARTITION BY Id) THEN 1
ELSE 0
END AS NewCol
FROM mytable
The query uses windowed version of MAX in order to compare the minimum-per-Id OrderDate to the minimum-per- (Id, ShopId) date. If these two values are the same, then we mark the corresponding (Id, ShopId) partition with 1.
Demo here
Less elegant than the others, but is ANSI
select MyTable.*, case when a1.mindate = orderdate then 1 else 0 end as NewCol
from MyTable
inner join
(
select id, min(orderdate) as mindate
from Mytable
group by id
) a1
on a1.id = MyTable.id
Use min with orderdate on ShopId and use that in the Case When statement like this:-
Select case when (a.OrderDate=b.min_order_dt) then 1 else 0 end as NewCol, a.*
from
your_table_name a
inner join
(
SELECT ShopId, min(OrderDate) as min_order_dt
from
your_table_name
group by shop_id
) b
on a.ShopId=b.ShopId;
Try this
select case when t2.ShopId is null then 0 else 1 end as newcol,t1.id,
t1.ShopiId,t1.OrderDate
from table as t1 left join
(
select ShopId,min(OrderDate) as OrderDate from table
group by ShopId
) as t2 on t1.ShopId=t2.ShopId and t1.OrderDate=t2.OrderDate

Calculate discount between weeks

I have a table containing product price data, like that:
ProductId RecordDate Price
46 2015-01-17 14:35:05.533 112.00
47 2015-01-17 14:35:05.533 88.00
45 2015-01-17 14:35:05.533 134.00
I have been able to group data by week and product, with this query:
SET DATEFIRST 1;
SELECT DATEADD(WEEK, DATEDIFF(WEEK, 0, [RecordDate]), 0) AS [Week], ProductId, MIN([Price]) AS [MinimumPrice]
FROM [dbo].[ProductPriceHistory]
GROUP BY DATEADD(WEEK, DATEDIFF(WEEK, 0, [RecordDate]), 0), ProductId
ORDER BY ProductId, [Week]
obtaining this result:
Week Product Price
2015-01-12 00:00:00.000 1 99.00
2015-01-19 00:00:00.000 1 98.00
2015-01-26 00:00:00.000 1 95.00
2015-02-02 00:00:00.000 1 95.00
2015-02-09 00:00:00.000 1 95.00
2015-02-16 00:00:00.000 1 95.00
2015-02-23 00:00:00.000 1 80.00
2015-03-02 00:00:00.000 1 97.00
2015-03-09 00:00:00.000 1 85.00
2015-01-12 00:00:00.000 2 232.00
2015-01-19 00:00:00.000 2 233.00
2015-01-26 00:00:00.000 2 194.00
2015-02-02 00:00:00.000 2 194.00
2015-02-09 00:00:00.000 2 199.00
2015-02-16 00:00:00.000 2 199.00
2015-02-23 00:00:00.000 2 199.00
2015-03-02 00:00:00.000 2 214.00
Now for each product I'd like to get the difference between the last two week values, so that I can calculate the discount. I don't know how to write this as a SQL Query!
EDIT:
Expected output would be something like that:
Product Price
1 -12.00
2 15.00
Thank you!
since you are using Sql Server 2014 you can use LAG or LEAD window function to do this.
Generate Row number to find the last two weeks for each product.
;WITH cte
AS (SELECT *,
Row_number()OVER(partition BY product ORDER BY weeks DESC)rn
FROM Yourtable)
SELECT product,
price
FROM (SELECT product,
Price=price - Lead(price)OVER(partition BY product ORDER BY rn)
FROM cte a
WHERE a.rn <= 2) A
WHERE price IS NOT NULL
SQLFIDDLE DEMO
Traditional solution, can be used before Sql server 2012
;WITH cte
AS (SELECT *,
Row_number()OVER(partition BY product
ORDER BY weeks DESC)rn
FROM Yourtable)
SELECT a.Product,
b.Price - a.Price
FROM cte a
LEFT JOIN cte b
ON a.Product = b.Product
AND a.rn = b.rn + 1
WHERE a.rn <= 2
AND b.Product IS NOT NULL