Calculate in join in sql - sql

I am trying to achieve staggered calculation in joins in sql 2008. I can have n number of rows for 1 job id. I have created a sample below
CREATE TABLE Job
(
JobID INT NOT NULL,
Amount INT NOT NULL
);
INSERT INTO Job (JobID, Amount)
VALUES (1, 25),
(1, 45),
(1, 40),
(2, 25),
(3, 26),
(3, 26);
now the discount for JobID = 1 is 80 , So what I am expecting in output of query result is below:
If the Amount > Discount , so show the finalvalue = Amount - Discount
but if Amount < Discount , then show Finalvalue = Amount - Amount ,
and if Discount is still left , deduct the same from the subsequent rows.
Job ID Amount FinalValue
1 25 0
1 45 0
1 40 30
Can all this be done in a join?

Here you are:
EDIT: ATTENTION: You should add a column for sorting. My approach is partitioning AND sorting by JobID which makes the output random...
EDIT: Sorry, did not add the tables...
CREATE TABLE Job
(
JobID INT NOT NULL,
Amount INT NOT NULL
);
INSERT INTO Job (JobID, Amount)
VALUES (1, 25), (1, 45), (1, 40), (2, 25), (3, 26), (3, 26);
CREATE TABLE Discount
(
JobID INT NOT NULL,
Discount INT NOT NULL
);
INSERT INTO Discount(JobID,Discount)VALUES(1,80),(2,0),(3,10);
WITH myCTE AS
(
SELECT ROW_NUMBER() OVER(PARTITION BY Job.JobID ORDER BY Job.JobID) AS inx
,Job.JobID
,Job.Amount
,Discount.Discount
FROM Job
INNER JOIN Discount ON Job.JobID=Discount.JobID
)
SELECT * FROM myCTE
CROSS APPLY
(
SELECT SUM(x.Amount)
FROM myCTE AS x
WHERE x.JobID=myCTE.JobID
AND x.inx<=myCTE.inx
) AS AmountCummulativ(AmountCummulativ)
CROSS APPLY(SELECT AmountCummulativ-myCTE.Discount) AS DiscountCalculated(DiscountCalculated)
CROSS APPLY(SELECT CASE WHEN DiscountCalculated<0 THEN 0 ELSE DiscountCalculated END) AS DiscountResolved(DiscountResolved)
Hope this helps

I think what you are looking for can be done using a case statement
select a.jobid,a.Amount,case when a.amount > b.discount then a.amount - b.discount else 0 end final_value
from Job a inner join Job_discount b on a.jobid = b.jobid
You can look at the results here http://sqlfiddle.com/#!3/f9a46/1
I had to assume the discount table structure

I added some sequence (row number) for the Job table (calling it JobOrder), to increment the sums. You can change the order as it is now for JobId, Amount!
With JobOrder as (
-- Job order by id and amount
select row_number() over (order by JobID, Amount asc) as rowno, JobId, Amount from Job
),
JobSumIncr as (
select
JobID,
Amount,
(select sum(Amount)
from JobOrder j2
where j2.JobID = j.JobID and j2.RowNo <= j.RowNo
) as AmountTotal
from JobOrder j
)
select
j.JobID,
j.Amount,
j.AmountTotal,
d.Discount,
(case when d.Discount>=j.AmountTotal then 0 else j.AmountTotal-d.Discount end) as FinalValue
from
JobSumIncr j left join Discount d on j.JobID = d.JobID;
Asuming that your Discount table is something like:
CREATE TABLE Discount (
JobID INT,
Discount INT
);
SqlFiddle here! and for a more secure Sql (checking null values and see the discount left) see this SQLFiddle too.
A version which keeps track of the discount left, see sqlfiddle-2 above.

You can try following code : 80 is the discount amount
Select JobID, Amount,
case when SUM (Amount) OVER (partition by JobID ORDER BY JobID ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) > 80 then SUM(Amount) OVER (partition by JobID ORDER BY JobID ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) - 80 else 0 end as FinalValue
from Job

Related

SQL QUERY : multiple records against one column value . need to compare another column value to fetch one record

A sample table us like
STATUS INVOICE
=======================
processed 100
reconciled 100
reconciled 200
paid 300
paid 100
paid 200
Output should be
STATUS INVOICE
=======================
processed 100
reconciled 200
paid 300
Logic : If there are multiple statuses against an invoice number , then we should follow the below order to fetch .
Processed > reconciled > paid
Please help me with the SQL query statement for this requirement .
This is a prioritization query. You can handle it using row_number():
select t.*
from (select t.*,
row_number() over (partition by invoice
order by case status when 'Processed' then 1 when 'reconciled' then 2 when 'paid' then 3 else 4 end
) as seqnum
from t
) t
where seqnum = 1;
You need conditional ordering with row_number() :
select top (1) with ties t.*
from table t
order by row_number() over (partition by invoice
order by (case status
when 'Processed'
then 1
when 'reconciled'
then 2
when 'paid'
then 3
else 4
end)
);
Others answers are ok but I'm posting it for the cases there's hundres (or more) of categories because in this case populating a table variable is better than writing lots and lots of CASE statements.
The trick here is to populate a table variable or temp table with your ordering rules and just aggregate by it.
create table dbo.[SAMPLE]
(
[STATUS] varchar(30) not null
,[INVOICE] int not null
)
GO
insert into dbo.[SAMPLE]
values
('processed', 100)
,('reconciled', 100)
,('reconciled', 200)
,('paid', 300)
,('paid', 100)
,('paid', 200)
GO
The below is a partial result showing how it get grouped by your ordering rules
--Processed > reconciled > paid
declare #Ordering as table
(
[STATUS] varchar(30) not null
,[Order] smallint not null
)
insert into #Ordering
values
('processed', 3)
,('reconciled',2)
,('paid',1)
select sp.[INVOICE], max(ord.[Order]) as Precedence
from dbo.[SAMPLE] sp
join #Ordering ord on ord.[STATUS] = sp.[STATUS]
group by sp.[INVOICE]
and below the final query with the expected results
--Processed > reconciled > paid
declare #Ordering as table
(
[STATUS] varchar(30) not null
,[Order] smallint not null
)
insert into #Ordering
values
('processed', 3)
,('reconciled',2)
,('paid',1)
select ord.[STATUS], grouped.INVOICE
from
(
select sp.[INVOICE], max(ord.[Order]) as Precedence
from dbo.[SAMPLE] sp
join #Ordering ord on ord.[STATUS] = sp.[STATUS]
group by sp.[INVOICE]
) as grouped
join #Ordering ord on ord.[Order] = grouped.Precedence
It can be also a interesting solution from performance perspective (acid test required of course).
if you have a status table and the order of status like this
id desc
1 processed
2 reconcilied
3 paid
the better way is joining with this tatble, group by invoice and select max(id)
select i.invoice, max(s.id)
from status s left outer join invoice i
on s.desc = i.status
group by i.invoice
if you havn't this table you can use with to create a virtual table and do this or you can use the case then
https://modern-sql.com/feature/with
https://learn.microsoft.com/it-it/sql/t-sql/language-elements/case-transact-sql?view=sql-server-2017
You can try this.
DECLARE #SampleDate TABLE (STATUS VARCHAR(20), INVOICE INT)
INSERT INTO #SampleDate VALUES
('processed', 100),
('reconciled', 100),
('reconciled', 200),
('paid', 300),
('paid', 100),
('paid', 200)
SELECT STATUS, INVOICE FROM (
SELECT T.*, ROW_NUMBER() OVER(PARTITION BY INVOICE ORDER BY St.ID) AS RN FROM #SampleDate T
INNER JOIN (VALUES (1,'processed'), (2,'reconciled'), (3,'paid')) St(Id, Name) ON T.STATUS = St.Name
) AS X WHERE RN= 1
Result:
STATUS INVOICE
-------------------- -----------
processed 100
reconciled 200
paid 300
WITH TempData As (SELECT MAX(INVOICE) AS INVOICE, [STATUS], CASE WHEN [STATUS] = 'processed' THEN 1 WHEN [STATUS] = 'reconciled' THEN 2 WHEN [STATUS] = 'paid' THEN 3 ELSE 4 END AS SEQ
FROM SAMPLETEST GROUP BY [STATUS])
SELECT [STATUS], INVOICE FROM TempData ORDER BY TempData.SEQ;

Retrieve specific value if set, or general if not

I have to write a query which will import some data from table. Table structure is like:
Item_ID
Plant
Price
I have second table with
Item_ID
Plant
Second table is a key, and I have to match rows from first table to get valid price. Seems to be easy, however:
In first table column plant might determinate specific plant, or have value 'ALL'. What I want to do is retrieve price for given plant, if it is set, or get price for value 'ALL' if there is no row for given plant. In other words:
If first.Plant = second.Plant
return price
Else If first.Plant = 'ALL'
return price
Else
return NULL
I can't use simple ON first.Plant = second.Plant OR first.Plant = 'ALL', because there might be two rows: one for given plant and second for rest with value 'ALL'. I need to return only first price in that case. E.g.
Item_ID | Plant | Price
2 | M1 | 10,0
2 | All | 12,0
1 | All | 9,0
In that case for Item_ID = 2 and Plant = M1 the only valid price = 10, but for Item_ID = 2 and Plant = M2 price = 12, and for any Item_ID = 1 price = 9
I hope You understood something after my explanation ;)
Using ROW_Number and a Common Table Expression you can ensure that ROW_NUMBER is partitioned by Item_ID and Plant and ordered such that if there are two rows then 'All' is second. You then simple select the rows with a row number = 1:
Setup
CREATE TABLE #Price
(
Item_ID int,
Plant Varchar(20),
Price Decimal(6,2)
)
CREATE TABLE #Plant
(
Item_ID int,
Plant VARCHAR(20)
)
INSERT INTO #Price
VALUES (2, 'M1', 10.0),
(2, 'All', 12.0),
(1, 'All', 9.0)
INSERT INTO #Plant
VALUES (2, 'M1'),
(2, 'M2'),
(1, 'M1'),
(1, 'M2')
Here's the query for SQL Server >= 2012
;WITH CTE
AS
(
SELECT PL.Item_ID, PL.Plant, PR.PRice, ROW_NUMBER() OVER (PARTITION BY Pl.Item_ID, Pl.Plant ORDER BY IIF(PR.Plant = 'All', 1, 0)) AS RN
FROM #Plant PL
INNER JOIN #Price PR
ON PL.Item_Id = PR.Item_Id AND (PL.Plant = PR.Plant OR PR.Plant = 'ALL')
)
SELECT Item_Id, Plant, Price
FROM CTE
WHERE RN = 1
And here's a version using CASE which will work for all SQL Server >= 2008 R2
;WITH CTE
AS
(
SELECT PL.Item_ID, PL.Plant, PR.PRice, ROW_NUMBER() OVER (PARTITION BY Pl.Item_ID, Pl.Plant ORDER BY CASE WHEN PR.Plant = 'All' Then 1 Else 0 End) AS RN
FROM #Plant PL
INNER JOIN #Price PR
ON PL.Item_Id = PR.Item_Id AND (PL.Plant = PR.Plant OR PR.Plant = 'ALL')
)
SELECT Item_Id, Plant, Price
FROM CTE
WHERE RN = 1

How would l write SQL to label quantities until they run out?

I would like to label quantities (in the quantity table) using the labels assigned (see label assignment table) until the quantity goes to 0. Then I know that I am done labeling that particular ID.
label assignment table is as follows:
ID | Label | Quantity
1 aaa 10
1 bbb 20
2 ccc 20
And my quantity table:
ID | Total Quantity
1 60
2 20
And I would like to get the following result:
ID | Label | Quantity
1 aaa 10 (read from reference table, remaining 50)
1 bbb 20 (read from reference table, remaining 30)
1 [NULL] 30 (no label in reference table, remaining 0)
2 ccc 20 (read from reference table, remaining 0)
You can do it with a simple JOIN and UNION operation so as to include 'not covered' quantities:
SELECT la.ID, la.Label, la.Quantity
FROM label_assignment AS la
INNER JOIN quantity AS q ON la.ID = q.ID
UNION
SELECT q.ID, NULL AS Label, q.TotalQuantity - la.TotalQuantity
FROM quantity AS q
INNER JOIN (
SELECT ID, SUM(Quantity) AS TotalQuantity
FROM label_assignment
GROUP BY ID
) AS la ON q.ID = la.ID AND q.TotalQuantity > la.TotalQuantity
Demo here
DECLARE #PerLabelQuantity TABLE(Id int, Label varchar(10), Quantity int);
INSERT INTO #PerLabelQuantity
VALUES (1, 'aaa', 10), (1, 'bbb', 20), (2, 'ccc', 20);
DECLARE #QuantityRequired TABLE(Id int, TotalQuantity int);
INSERT INTO #QuantityRequired
VALUES (1, 60), (2, 20);
SELECT t.Id,
CASE WHEN o.Overflowed = 1 THEN NULL ELSE t.Label END AS Label,
CASE WHEN o.Overflowed = 1 THEN t.QuantityStillNeeded
WHEN t.QuantityStillNeeded < 0 THEN t.Quantity + t.QuantityStillNeeded
ELSE t.Quantity END AS Quantity
FROM (
SELECT p.Id, p.Label, p.Quantity,
MAX(p.Label) OVER (PARTITION BY p.Id) AS LastLabel,
r.TotalQuantity - SUM(p.Quantity)
OVER (PARTITION BY p.Id
ORDER BY Label
ROWS UNBOUNDED PRECEDING) AS QuantityStillNeeded
FROM #PerLabelQuantity p
INNER JOIN #QuantityRequired r ON p.Id = r.Id) t
INNER JOIN (VALUES (0), (1)) o(Overflowed)
ON t.LastLabel = t.Label AND t.QuantityStillNeeded > 0 OR Overflowed = 0
WHERE t.QuantityStillNeeded > -t.Quantity; -- Remove this if you want labels with
-- 0 quantity used, but you'll need to tweak
-- the CASE expression for Quantity
The subquery calculates a set of used up labels and how many items remain afterward. If there is any quantity remaining after the last label, then we need to insert a row in the result set. To do this, I join on a two-element table but the join condition is only true when we are at the last label and there is quantity remaining. This is probably a confusing way to do this, and we could combine the UNION from George's answer with the subquery from mine to avoid this Overflow table.
Here's the changed (and probably preferable) query:
SELECT Id,
Label,
CASE WHEN QuantityStillNeeded < 0 THEN Quantity + QuantityStillNeeded
ELSE Quantity END AS Quantity
FROM (SELECT p.Id, p.Label, p.Quantity,
r.TotalQuantity - SUM(p.Quantity)
OVER (PARTITION BY p.Id
ORDER BY Label
ROWS UNBOUNDED PRECEDING) AS QuantityStillNeeded
FROM #PerLabelQuantity p
INNER JOIN #QuantityRequired r ON p.Id = r.Id) t
WHERE t.QuantityStillNeeded > -t.Quantity
UNION ALL
SELECT q.Id, NULL AS Label, q.TotalQuantity - la.TotalQuantity AS Quantity
FROM #QuantityRequired AS q
INNER JOIN (
SELECT Id, SUM(Quantity) AS TotalQuantity
FROM #PerLabelQuantity
GROUP BY Id) la ON q.ID = la.ID
WHERE q.TotalQuantity > la.TotalQuantity
Simplest answer I think, after getting ideas from the other answers: Just create a "FAKE" label for the missing amount:
DECLARE #PerLabelQuantity TABLE(Id int, Label varchar(10), Quantity int);
INSERT INTO #PerLabelQuantity
VALUES (1, 'aaa', 10), (1, 'bbb', 20), (2, 'ccc', 20);
SELECT *
FROM #PerLabelQuantity
DECLARE #QuantityRequired TABLE(Id int, TotalQuantity int);
INSERT INTO #QuantityRequired
VALUES (1, 60), (2, 20);
SELECT *
FROM #QuantityRequired
-- MAKE A FAKE LABEL LET'S CALL IT [NULL] WITH THE AMOUNT THAT IS NOT LABELED
-- i.e. WITH THE REMAINING AMOUNT
-- Probably should be done by copying the original data and the following
-- into a temp table but this is just for proof of concept
INSERT INTO #PerLabelQuantity( Id, Label, Quantity )
SELECT q.ID,
NULL,
ISNULL(q.TotalQuantity - p.TotalQuantityLabeled, q.TotalQuantity)
FROM #QuantityRequired q
LEFT JOIN (SELECT p.ID, SUM(Quantity) AS TotalQuantityLabeled
FROM #PerLabelQuantity p
GROUP BY p.Id) p ON
p.ID = q.ID
AND q.TotalQuantity - p.TotalQuantityLabeled > 0
SELECT *
FROM #PerLabelQuantity p

Calculating a fields value according to the values of the previous and next fields

For clarity assume that I have a table with a carID, a mileage and a date. The dates are always months (eg 01/02/2015, 01/03/2015, ...). Each carID has a row for each month, but not each row has values for the mileage field, some are NULL.
Example table:
carID mileage date
-----------------------------------------
1 400 01/01/2015
2 NULL 01/02/2015
3 NULL 01/03/2015
4 1050 01/04/2015
If such a field is NULL I need to calculate what value it should have by looking at the previous and next values (these aren't necessarily the next or previous month, they can be months apart).
I want to do this by taking the difference of the previous and next values, then calculate the time between them and make the value accordingly to the time. I have no idea however as how to do this.
I have already used a bit of code to look at the next value before, it looks like this:
, carKMcombiDiffList as (
select ml.*,
(ml.KM - mlprev.KM) as diff
from carKMcombilist ml outer apply
(select top 1 ml2.*
from carKMcombilist ml2
where ml2.FK_CarID = ml.FK_CarID and
ml2.beginmonth < ml.beginmonth
order by ml2.beginmonth desc
) mlprev
)
What this does is check if the current value is larger then the previous value. I assume I can use this as well to check the previous one in my current problem, I just don't know how I can add the next one in it AND all the logic that I need to make the calculations.
Assumption: CarID and date are always a unique combination
This is what i came up with:
select with_dates.*,
prev_mileage.mileage as prev_mileage,
next_mileage.mileage as next_mileage,
next_mileage.mileage - prev_mileage.mileage as mileage_delta,
datediff(month,prev_d,next_d) as month_delta,
(next_mileage.mileage - prev_mileage.mileage)/datediff(month,prev_d,next_d)*datediff(month,prev_d,with_dates.d) + prev_mileage.mileage as estimated_mileage
from (select *,
(select top 1 d
from mileage as prev
where carid = c.carid
and prev.d < c.d
and prev.mileage is not null
order by d desc ) as prev_d,
(select top 1 d
from mileage as next_rec
where carid = c.carid
and next_rec.d > c.d
and next_rec.mileage is not null
order by d asc) as next_d
from mileage as c
where mileage is null) as with_dates
join mileage as prev_mileage
on prev_mileage.carid = with_dates.carid
and prev_mileage.d = with_dates.prev_d
join mileage as next_mileage
on next_mileage.carid = with_dates.carid
and next_mileage.d = with_dates.next_d
Logic:
First, for every mileage is nullrecord i select the previous and next date where mileage is not null. After this i just join the rows based on carid and date and do some simple math to approximate.
Hope this helps, it was quite fun.
The following query obtains the previous and next available mileages for a record.
with data as --test data
(
select * from (VALUES
(0, null, getdate()),
(1, 400, '20150101'),
(1, null, '20150201'),
(1, null, '20150301'),
(1, 1050, '20150401'),
(2, 300, '20150101'),
(2, null, '20150201'),
(2, null, '20150301'),
(2, 1235, '20150401'),
(2, null, '20150501'),
(2, 1450, '20150601'),
(3, 200, '20150101'),
(3, null, '20150201')
) as v(carId, mileage, [date])
where v.carId != 0
)
-- replace 'data' with your table name
select d.*,
(select top 1 mileage from data dprev where dprev.mileage is not null and dprev.carId = d.carId and dprev.[date] <= d.date order by dprev.[date] desc) as 'Prev available mileage',
(select top 1 mileage from data dnext where dnext.mileage is not null and dnext.carId = d.carId and dnext.[date] >= d.date order by dnext.[date] asc) as 'Next available mileage'
from data d
Note that these columns can still be null if there is no data available before/after a specific date.
From here it's up to you on how you use these values. Probably you want to interpolate values for records where mileage is missing.
Edit
In order to interpolate the values for missing mileages I had to compute three auxiliary columns:
ri - index of record in a continuous group where mileage is missing
gi - index of a continuous group where mileage is missing per car
gc - count of records per continuous group where mileage is missing
The limit columns from the query above where renamed to
pa (Previous Available) and
na (Next Available).
The query is not compact and I am sure it can be improved but the good part of the cascading CTEs is that you can easily check intermediary results and understand each step.
SQL Fiddle: SO 29363187
with data as --test data
(
select * from (VALUES
(0, null, getdate()),
(1, 400, '20150101'),
(1, null, '20150201'),
(1, null, '20150301'),
(1, 1050, '20150401'),
(2, 300, '20150101'),
(2, null, '20150201'),
(2, null, '20150301'),
(2, 1235, '20150401'),
(2, null, '20150501'),
(2, 1450, '20150601'),
(3, 200, '20150101'),
(3, null, '20150201')
) as v(carId, mileage, [date])
where v.carId != 0
),
-- replace 'data' with your table name
limits AS
(
select d.*,
(select top 1 mileage from data dprev where dprev.mileage is not null and dprev.carId = d.carId and dprev.[date] <= d.date order by dprev.[date] desc) as pa,
(select top 1 mileage from data dnext where dnext.mileage is not null and dnext.carId = d.carId and dnext.[date] >= d.date order by dnext.[date] asc) as na
from data d
),
t1 as
(
SELECT l.*,
case when mileage is not null
then null
else row_number() over (partition by l.carId, l.pa, l.na order by l.carId, l.[date])
end as ri, -- index of record in a continuous group where mileage is missing
case when mileage is not null
then null
else dense_rank() over (partition by carId order by l.carId, l.pa, l.na)
end as gi -- index of a continuous group where mileage is missing per car
from limits l
),
t2 as
(
select *,
(select count(*) from t1 tm where tm.carId = t.carId and tm.gi = t.gi) gc --count of records per continuous group where mileage is missing
FROM t1 t
)
select *,
case when mileage is NULL
then pa + (na - pa) / (gc + 1.0) * ri -- also converts from integer to decimal
else NULL
end as 'Interpolated value'
from t2
order by carId, [date]

how to get query value from 1 row to use to another row?

This is example query:
payment_Type payment_value cost_type cost value
Cost I 100 Registration 40
Cost I 100 books 40
Cost I 100 Lab 40
The COST I has 3 elements Cost_Type that have their own Cost_value.
I want to manipulate like this:
payment_Type payment_value cost_type cost value Payment_by_cost_type
Cost I 100 Registration 40 40
Cost I 100 books 40 40
Cost I 100 Lab 40 20
The point is the I want to divided the payment_value into each cost_value. In the example the payment_by_cost becomes 40, 40, 20 = 100.
The lab cost_value is 40 but it can assign value is 20 because remains from the divided 2 cost type above.
Is it possible that I can use the value from Payment_by_cost_type in the next row record? I have been trying to insert the value Payment_by_cost_type to a temporary table but select cannot have insert statement.
Does anyone have any ideas on how to solve this? I've been consulting to DWH he said it must using Store procedure it cannot done by query.
I guess your table contains not only "Cost I" but other values so here is a query to output results for all groups (by Payment_type) in the table:
;with table1 as
(select
t.*,
row_number()
OVER
(PARTITION BY payment_Type order by cost_type) rn
from t
)
,table2 as
( select t4.*,isnull((select sum(cost_value) from table1
where table1.payment_type=t4.payment_type and rn<t4.rn),0) CumSum
from table1 t4
)
select payment_type,payment_value,cost_type,cost_value,
case when cost_value+CumSum<=payment_value then cost_value
else
payment_value-Cumsum
end
from table2
order by Payment_type,rn;
SQLFIDDLE demo
You need to define some kind of order for your records to define in which order the payments should be applied
Once you have done that (i'm using ID in this example)...
select *
, case
when payment_value-(select isnull(SUM(cost_value),0) from yourtable t2 where t2.id<t1.id)<cost_value
then payment_value-(select isnull(SUM(cost_value),0) from yourtable t2 where t2.id<t1.id)
else cost_value
end
from yourtable t1
Doing it step by step using common table expressions.
declare #t table (
payment_type varchar(20),
payment_value int,
cost_type varchar(20),
cost_value int,
cost_id int --for the ordering
)
insert #t values
('Cost I',100,'Registration',40,1),
('Cost I',100,'books',40,2),
('Cost I',100,'Lab',40,3),
('Cost 2',100,'Registration',40,4),
('Cost 2',100,'books',50,5),
('Cost 2',100,'Lab',40,6)
--get count for each payment_type to determine last row
;with payment_value_cte(payment_type,payment_value,count) as
(
select payment_type,payment_value,COUNT(*) from #t group by payment_type,payment_value
),
--use sequential index for each row in payment type
payment_value_index_cte(
payment_type ,
payment_value,
cost_type,
cost_value,
cost_id,
row) as
(
select *,ROW_NUMBER() OVER(PARTITION BY payment_type ORDER BY cost_id) from #t --assumes order is by an id
),
--get sum of each row for payment type except last row
payment_value_sum_except_last_cte(
payment_type,
payment_value,
current_sum) as
(
select pi.payment_type,pi.payment_value,SUM(cost_value)
from payment_value_index_cte pi
inner join payment_value_cte pt on pt.payment_type = pi.payment_type
where pi.row < pt.count
group by pi.payment_type,pi.payment_value
)
select
pi.payment_type,pi.payment_value,pi.cost_type,pi.cost_value,
--if last row calculate difference, else use the cost_value
case when pi.row = pt.count then pt.payment_value - pe.current_sum else pi.cost_value end [Payment_by_cost_type]
from payment_value_index_cte pi
inner join payment_value_cte pt on pt.payment_type = pi.payment_type
inner join payment_value_sum_except_last_cte pe on pe.payment_type = pi.payment_type
SELECT payment_Type, payment_value, cost_type, cost_value,
CASE WHEN ROW_NUMBER() OVER (ORDER BY(SELECT 1)) = COUNT(*) OVER (PARTITION BY payment_Type)
THEN SUM(cost_value) OVER (PARTITION BY payment_Type) - payment_value
ELSE cost_value END AS Payment_by_cost_type
FROM dbo.your_table
Demo on SQLFiddle