Calculating new percentages over total in a "select distinct" query with multiple percentages rows - sql

The title may be puzzling, but the situation should be really simple foranyone reading the following lines.
I have this table VAL:
customer month_1 month_2 month_3 value
ABC 0.5 0 0.50 200
ABC 0.25 0.25 0.50 200
XYZ 1 0 0 150
RST 0 0 1 200
RST 0 0.50 0.50 130
(This is the code to get it)
create table VAL (customer nvarchar(255), month_1 decimal(18,2), month_2 decimal(18,2), month_3 decimal(18,2), value decimal(18,2))
insert into VAL values ('ABC', 0.5, 0 , 0.50, 200)
insert into VAL values ('ABC',0.25 , 0.25, 0.50 , 200 )
insert into VAL values ('XYZ', 1 , 0 , 0 , 150 )
insert into VAL values ('RST', 0 , 0 , 1 , 200 )
insert into VAL values ('RST', 0 , 0.50, 0.50 , 130 )
This can be seen (transforming what is a percentage in actual values) as
customer value_month_1 value_month_2 value_month_3 value
ABC 100 0 100 200
ABC 50 50 100 200
XYZ 150 0 0 150
RST 0 0 200 200
RST 0 65 65 130
I need to transform everything in the following table, that is sort of a collapsed version of the first one:
customer month_1 month_2 month_3 value
ABC 0.375 0.125 0.50 400
XYZ 1.0 0 0.0 150
RST 0.0 0.197 0.793 330
So far, I'm able to do this customer by customer with the following code:
select distinct customer
,sum(month_1) as month_1
,sum(month_2) as month_2
,sum(month_3) as month_3
,sum(value) as value
from (
select distinct customer
,month_1 * sum(value)/(select sum(value) from VAL where customer='ABC' group by customer) as month_1
,month_2 * sum(value)/(select sum(value) from VAL where customer='ABC' group by customer) as month_2
,month_3 * sum(value)/(select sum(value) from VAL where customer='ABC' group by customer) as month_3
,sum(value) as value from VAL where customer='ABC' group by customer, month_1, month_2,month_3) as NEW
group by customer
that gives me the following result:
customer month_1 month_2 month_3 value
ABC 0.375 0.125 0.50 400
I'm pretty sure there are better ways to do this, probably with some command I don't know very well how to use.
Anyone able to help?

You just need a weighted average:
select customer,
sum(month_1 * value) / sum(value) as month_1,
sum(month_2 * value) / sum(value) as month_2,
sum(month_3 * value) / sum(value) as month_3,
sum(value)
from val
group by customer;

Related

SQL decreasing sum by a percentage

I have a table like
timestamp
type
value
08.01.2023
1
5
07.01.2023
0
20
06.01.2023
1
1
05.01.2023
0
50
04.01.2023
0
50
03.01.2023
1
1
02.01.2023
1
1
01.01.2023
1
1
Type 1 means a deposit, type 0 means a withdrawal.
The thing is when a type is 1 then the amount is the exact amount the user deposited so we can just sum that but type 0 means a withdrawal in percentage.
What I'm looking for is to create another column with current deposited amount. For the example above it would look like that.
timestamp
type
value
deposited
08.01.2023
1
5
5.4
07.01.2023
0
20
1.4
06.01.2023
1
1
1.75
05.01.2023
0
50
0.75
04.01.2023
0
50
1.5
03.01.2023
1
1
3
02.01.2023
1
1
2
01.01.2023
1
1
1
I can't figure out how to make a sum like this which would subtract percentage of previous total
You are trying to carry state over time, so ether need to use a UDTF to doing the carry work for you. Or use a recursive CTE
with data(transaction_date, type, value) as (
select to_date(column1, 'dd.mm.yyyy'), column2, column3
from values
('08.01.2023', 1, 5),
('07.01.2023', 0, 20),
('06.01.2023', 1, 1),
('05.01.2023', 0, 50),
('04.01.2023', 0, 50),
('03.01.2023', 1, 1),
('02.01.2023', 1, 1),
('01.01.2023', 1, 1)
), pre_process_data as (
select *
,iff(type = 0, 0, value)::number as add
,iff(type = 0, value, 0)::number as per
,row_number()over(order by transaction_date asc) as rn
from data
), rec_cte_block as (
with recursive rec_sub_cte as (
select
p.*,
p.add::number(20,4) as deposited
from pre_process_data as p
where p.rn = 1
union all
select
p.*,
round(div0((r.deposited + p.add)*(100-p.per), 100), 2) as deposited
from rec_sub_cte as r
left join pre_process_data as p
where p.rn = r.rn+1
)
select *
from rec_sub_cte
)
select * exclude(add, per, rn)
from rec_cte_block
order by 1;
I wrote the recursive CTE this way, as there currently is an incident if IFF or CASE is used inside the CTE.
TRANSACTION_DATE
TYPE
VALUE
DEPOSITED
2023-01-01
1
1
1
2023-01-02
1
1
2
2023-01-03
1
1
3
2023-01-04
0
50
1.5
2023-01-05
0
50
0.75
2023-01-06
1
1
1.75
2023-01-07
0
20
1.4
2023-01-08
1
5
6.4
Solution without recursion and UDTF
create table depo (timestamp date,type int, value float);
insert into depo values
(cast('01.01.2023' as date),1, 1.0)
,(cast('02.01.2023' as date),1, 1.0)
,(cast('03.01.2023' as date),1, 1.0)
,(cast('04.01.2023' as date),0, 50.0)
,(cast('05.01.2023' as date),0, 50.0)
,(cast('06.01.2023' as date),1, 1.0)
,(cast('07.01.2023' as date),0, 20.0)
,(cast('08.01.2023' as date),1, 5.0)
;
with t0 as(
select *
,sum(case when type=0 and value>=100 then 1 else 0 end)over(order by timestamp) gr
from depo
)
,t1 as (select timestamp as dt,type,gr
,case when type=1 then value else 0 end depo
,case when type=0 then ((100.0-value)/100.0) else 0.0 end pct
,sum(case when type=0 and value<100 then log((100.0-value)/100.0,2.0)
when type=0 and value>=100 then null
else 0.0
end)
over(partition by gr order by timestamp ROWS BETWEEN CURRENT ROW
AND UNBOUNDED FOLLOWING) totLog
from t0
)
,t2 as(
select *
,case when type=1 then
isnull(sum(depo*power(cast(2.0 as float),totLog))
over(partition by gr order by dt rows between unbounded preceding and 1 preceding)
,0)/power(cast(2.0 as float),totLog)
+depo
else
isnull(sum(depo*power(cast(2.0 as float),totLog))
over(partition by gr order by dt rows between unbounded preceding and 1 preceding)
,0)/power(cast(2.0 as float),totLog)*pct
end rest
from t1
)
select dt,type,depo,pct*100 pct
,rest-lag(rest,1,0)over(order by dt) movement
,rest
from t2
order by dt
dt
type
depo
pct
movement
rest
2023-01-01
1
1
0
1
1
2023-02-01
1
1
0
1
2
2023-03-01
1
1
0
1
3
2023-04-01
0
0
50
-1.5
1.5
2023-05-01
0
0
50
-0.75
0.75
2023-06-01
1
1
0
1
1.75
2023-07-01
0
0
80
-0.35
1.4
2023-08-01
1
5
0
5
6.4
I think, it is better to perform this kind of calculations on client side or middle level.
Sequential calculations are difficult to implement in Sql. In some special cases, you can use logarithmic expressions. But it is clearer and easier to implement through recursion, as #Simeon showed.
To expand on #ValNik's answer
The fist simple step is to change "deduct 20%, then deduct 50%, then deduct 30%" in to a multiplication...
X - 20% - 50% - 30%
=>
x * 0.8 * 0.5 * 0.7
=>
x * 0.28
The second trick is to understand how to calculate cumulative PRODUCT() when you only have cumulative sum; SUM() OVER (), using the properties of logarithms...
a * b == exp( log(a) + log(b) )
0.8 * 0.5 * 0.7
=>
exp( log(0.8) + log(0.5) + log(0.7) )
=>
exp( -0.2231 + -0.6931 + -0.3567 )
=>
exp( -1.2730 )
=>
0.28
The next trick is easier to explain with integers rather than percentages. That is to be able to break down the original problem in to one that can be solved using "cumulative sum" and "cumulative product"...
Current working:
row_id
type
value
equation
result
1
+
10
0 + 10
10
2
+
20
(0 + 10 + 20)
30
3
*
2
(0 + 10 + 20) * 2
60
4
+
30
(0 + 10 + 20) * 2 + 30
90
5
*
3
((0 + 10 + 20) * 2 + 30) * 3
270
Rearranged working:
row_id
type
value
CUMPROD
new equation
result
1
+
10
2*3=6
(10*6 ) / 6
10
2
+
20
2*3=6
(10*6 + 20*6 ) / 6
30
3
*
2
3=3
(10*6 + 20*6 ) / 3
60
4
+
30
3=3
(10*6 + 20*6 + 30*3) / 3
90
5
*
3
=1
(10*6 + 20*6 + 30*3) / 1
270
CUMPROD is the "cumulative product" of all future "multiplication values".
The equation is then the "cumulative sum" of value * CUMPROD divided by the current CUMPROD.
So...
row 1 : SUM(10*6 ) / 6 => SUM(10 )
row 2 : SUM(10*6, 20*6 ) / 6 => SUM(10, 20)
row 3 : SUM(10*6, 20*6 ) / 3 => SUM(10, 20) * 2
row 4 : SUM(10*6, 20*6, 30*3) / 3 => SUM(10, 20) * 2 + SUM(30)
row 5 : SUM(10*6, 20*6, 30*3) / 1 => SUM(10, 20) * 2*3 + SUM(30) * 3
The only things to be cautious of are:
LOG(0) = Infinity (which would happen when deducting 100%)
Deducting more than 100% makes no sense
So, I copied #ValNik's code that creates a new partition every time 100% or more is deducted (forcing everything in the next partition to start at zero again).
This gives the following SQL (a re-arranged version of #ValNik's code):
WITH
partition_when_deduct_everything AS
(
SELECT
*,
SUM(
CASE WHEN type = 0 AND value >= 100 THEN 1 ELSE 0 END
)
OVER (
ORDER BY timestamp
)
AS deduct_everything_id,
CASE WHEN type = 1 THEN value
ELSE 0
END
AS deposit,
CASE WHEN type = 1 THEN 1.0 -- Deposits == Deduct 0%
WHEN value >= 100 THEN 1.0 -- Treat "deduct everything" as a special case
ELSE (100.0-value)/100.0 -- Change "deduct 20%" to "multiply by 0.8"
END
AS multiplier
FROM
your_table
)
,
cumulative_product_of_multipliers as
(
SELECT
*,
EXP(
ISNULL(
SUM(
LOG(multiplier)
)
OVER (
PARTITION BY deduct_everything_id
ORDER BY timestamp
ROWS BETWEEN 1 FOLLOWING
AND UNBOUNDED FOLLOWING
)
, 0
)
)
AS future_multiplier
FROM
partition_when_deduct_everything
)
SELECT
*,
ISNULL(
SUM(
deposit * future_multiplier
)
OVER (
PARTITION BY deduct_everything_id
ORDER BY timestamp
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW
),
0
)
/
future_multiplier
AS rest
FROM
cumulative_product_of_multipliers
Demo : https://dbfiddle.uk/mrioIMiB
So how this should be solved, is a UDTF, because it requires to "sorting the data once" and "traversing the data only once", and if you have different PARTITIONS aka user_id etc etc you can work in parallel):
create or replace function carry_value_state(_TYPE float, _VALUE float)
returns table (DEPOSITED float)
language javascript
as
$$
{
initialize: function(argumentInfo, context) {
this.carried_value = 0.0;
},
processRow: function (row, rowWriter, context){
if(row._TYPE === 1) {
this.carried_value += row._VALUE;
} else {
let limited = Math.max(Math.min(row._VALUE, 100.0), 0.0);
this.carried_value -= (this.carried_value * limited) / 100;
}
rowWriter.writeRow({DEPOSITED: this.carried_value});
}
}
$$;
which then gets used like:
select d.*,
c.*
from data as d
,table(carry_value_state(d.type::float, d.value::float) over (order by transaction_date)) as c
order by 1;
so for the data we have been using in the example, that gives:
TRANSACTION_DATE
TYPE
VALUE
DEPOSITED
2023-01-01
1
1
1
2023-01-02
1
1
2
2023-01-03
1
1
3
2023-01-04
0
50
1.5
2023-01-05
0
50
0.75
2023-01-06
1
1
1.75
2023-01-07
0
20
1.4
2023-01-08
1
5
6.4
yes, the results are now in floating point, so you should double round to avoid FP representation problems, like:
round(round(c.deposited, 6) , 2) as deposited
An alternative approach using Match_Recognize(), POW(), SUM().
I would not recommend using Match_Recognize() unless you have too, it's fiddly and can waste time, however does look elegant.
with data(transaction_date, type, value) as (
select
to_date(column1, 'dd.mm.yyyy'),
column2,
column3
from
values
('08.01.2023', 1, 5),
('07.01.2023', 0, 20),
('06.01.2023', 1, 1),
('05.01.2023', 0, 50),
('04.01.2023', 0, 50),
('03.01.2023', 1, 1),
('02.01.2023', 1, 1),
('01.01.2023', 1, 1)
)
select
*
from
data match_recognize(
order by
transaction_date measures
sum(iff(CLASSIFIER() = 'ROW_WITH_DEPOSIT', value, 0)) DEPOSITS,
pow(iff(CLASSIFIER() = 'ROW_WITH_WITHDRAWL', value / 100, 1) ,count(row_with_withdrawl.*)) DISCOUNT_FROM_WITHDRAWL,
CLASSIFIER() TRANS_TYPE,
first(transaction_date) as start_date,
last(transaction_date) as end_date,
count(*) as rows_in_sequence,
count(row_with_deposit.*) as num_deposits,
count(row_with_withdrawl.*) as num_withdrawls
after
match skip PAST LAST ROW pattern((row_with_deposit + | row_with_withdrawl +)) define row_with_deposit as type = 1,
row_with_withdrawl as type = 0
);

Running assignment of values with break T-SQL

With the below table of data
Customer
Amount Billed
Amount Paid
Date
1
100
60
01/01/2000
1
100
40
01/02/2000
2
200
150
01/01/2000
2
200
30
01/02/2000
2
200
10
01/03/2000
2
200
15
01/04/2000
I would like to create the next two columns
Customer
Amount Billed
Amount Paid
Assigned
Remainder
Date
1
100
60
60
40
01/01/2000
1
100
40
40
0
01/02/2000
2
200
150
150
50
01/01/2000
2
200
30
30
20
01/02/2000
2
200
10
10
10
01/03/2000
2
200
15
10
-5
01/04/2000
The amount paid on each line should be removed from the amount billed and pushed onto the next line for the same customer. The process should continue until there are no more records or the remainder is < 0.
Is there a way of doing this without a cursor? Maybe a recursive CTE?
Thanks
As I mentioned in the comments, this is just a cumulative SUM:
WITH YourTable AS(
SELECT *
FROM (VALUES(1,100,60 ,CONVERT(date,'01/01/2000')),
(1,100,40 ,CONVERT(date,'01/02/2000')),
(2,200,150,CONVERT(date,' 01/01/2000')),
(2,200,30 ,CONVERT(date,'01/02/2000')),
(2,200,10 ,CONVERT(date,'01/03/2000')),
(2,200,15 ,CONVERT(date,'01/04/2000')))V(Customer,AmountBilled,AmountPaid,[Date]))
SELECT Customer,
AmountBilled,
AmountPaid,
AmountBilled - SUM(AmountPaid) OVER (PARTITION BY Customer ORDER BY [Date] ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS Remainder,
[Date]
FROM YourTable
ORDER BY Customer,
[Date];
Note this returns -5 for the last row, not 5, as 200 - 205 = -5. If you want 5 wrap the whole expression in an absolute function.
You can achieve this using recursive CTE as well.
DECLARE #customer table (Customer int, AmountBilled int, AmountPaid int, PaidDate date)
insert into #customer
values
(1 ,100, 60 ,'01/01/2000')
,(1 ,100, 40 ,'01/02/2000')
,(2 ,200, 150 ,'01/01/2000')
,(2 ,200, 30 ,'01/02/2000')
,(2 ,200, 10 ,'01/03/2000')
,(2 ,200, 15 ,'01/04/2000');
;WITH CTE_CustomerRNK as
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY customer order by paiddate) AS RNK
from #customer),
CTE_Customer as
(
SELECT customer, AmountBilled, AmountPaid, (amountbilled-amountpaid) as remainder, paiddate ,RNK FROM CTE_CustomerRNK where rnk = 1
union all
SELECT r.customer, r.AmountBilled, r.AmountPaid, (c.remainder - r.AmountPaid) as remainder, r.PaidDate, r.rnk
FROM CTE_CustomerRNK as r
inner join CTE_Customer as c
on c.Customer = r.Customer
and r.rnk = c.rnk + 1
)
SELECT customer, AmountBilled, AmountPaid, remainder, paiddate
FROM CTE_Customer order by Customer
customer
AmountBilled
AmountPaid
remainder
paiddate
1
100
60
40
2000-01-01
1
100
40
0
2000-01-02
2
200
150
50
2000-01-01
2
200
30
20
2000-01-02
2
200
10
10
2000-01-03
2
200
15
-5
2000-01-04

How to set trailing stop loss for backtesting on Google Bigquery if possible

so I am doing some backtesting for some trading strategies on the stock market on Google Bigquery and I would like to set a trailing stop loss of 1% away from the entered price. If the stock price went up by let's say 5%, the trailing stop loss will move up 5% as well. If the stock price went down, the trailing stop loss will not change. (https://www.investopedia.com/terms/t/trailingstop.asp)
I have this table which shows my signal to enter and the exit column will show a value of 1 if the price goes below the trailing stop loss price, which means the trade is exited.
This is the table I have so far:
date price entry_signal
30/06/2018 95 0
01/07/2018 100 1
02/07/2018 103 0
03/07/2018 105 0
04/07/2018 104.50 0
05/07/2018 101 0
I would like to have a column showing what is the trailing stop loss at every date. The trailing stop loss is first set as 99% of the price on 01/07/2018 when the enter_signal = 1, where trade is executed on this date.
When the price moves up by y%, the trailing stop loss will move up by y% as well. However if the price goes down, the trailing stop loss will not change from its last value.
When the price <= trailing stop loss, the trade is exited and there will be an exit_signal of 1...
I am currently stuck at not having the trailing stop loss to move down by y% if the price move down by y% as well....
Desired table outcome:
date price trailing stop loss entry_signal exit_signal
30/06/2018 95 NULL 0 0
01/07/2018 100 99 1 0
02/07/2018 103 101.97 0 0
03/07/2018 105 103.95 0 0
04/07/2018 104.50 103.95 0 0
05/07/2018 101 103.95 0 1
This was my original code:
SELECT
date, price, entry_signal,
GREATEST(trailing_stop_loss, 0.99 * price) AS trailing_stop_loss
FROM (
SELECT
date, price, entry_signal,
LAST_VALUE(trailing_stop_loss IGNORE NULLS) OVER (ORDER BY DATE) AS trailing_stop_loss
FROM (
SELECT
date, price, entry_signal,
IF(entry_signal * 0.99 * price > 0, 0.99 * price, NULL) AS trailing_stop_loss
FROM table
)
)
Table I have obtained:
date price trailing stop loss entry_signal
30/06/2018 95 NULL 0
01/07/2018 100 99 1
02/07/2018 103 101.97 0
03/07/2018 105 103.95 0
04/07/2018 104.50 103.455 0
05/07/2018 101 99.99 0
Below is for BigQuery Standard SQL
I am currently stuck at not having the trailing stop loss to move down by y% if the price move down by y% as well....
#standardSQL
WITH temp1 AS (
SELECT day, price, entry_signal,
UNIX_DATE(PARSE_DATE('%d/%m/%Y', day)) day_as_days,
COUNTIF(entry_signal = 1) OVER(ORDER BY UNIX_DATE(PARSE_DATE('%d/%m/%Y', day))) grp
FROM `project.dataset.table`
), temp2 AS (
SELECT day, price,
0.99 * price AS trailing_stop_loss,
IFNULL(price > LAG(price) OVER(PARTITION BY grp ORDER BY day_as_days), TRUE) AS up,
entry_signal, grp, day_as_days
FROM temp1
)
SELECT day, price, trailing_stop_loss, entry_signal,
IF(price > trailing_stop_loss, 0, 1) AS exit_signal
FROM (
SELECT day_as_days, day, price, entry_signal,
IF(up, trailing_stop_loss, arr[OFFSET(0)]) trailing_stop_loss
FROM (
SELECT day_as_days, day, price, up, trailing_stop_loss, entry_signal,
ARRAY_AGG(trailing_stop_loss) OVER(PARTITION BY grp ORDER BY IF(up, day_as_days, 0) DESC) arr
FROM temp2
)
)
-- ORDER BY day_as_days
with result
Row day price trailing_stop_loss entry_signal exit_signal
1 30/06/2018 95.0 94.05 0 0
2 01/07/2018 100.0 99.0 1 0
3 02/07/2018 103.0 101.97 0 0
4 03/07/2018 105.0 103.95 0 0
5 04/07/2018 104.5 103.95 0 0
6 05/07/2018 101.0 103.95 0 1
Desired table outcome: ...
As you can see, above query at least partially addresses your stuck at point - while I am not sure what the whole picture is and what else needs to be addressed - even if your overall problem is still not fully addressed - i feel like your particular question is answered.
So, I hope above will unblock you and you can complete your challenge by yourself :o)
Btw, I used below dummy data (from your question)
WITH `project.dataset.table` AS (
SELECT '30/06/2018' day, 95 price, 0 entry_signal UNION ALL
SELECT '01/07/2018', 100, 1 UNION ALL
SELECT '02/07/2018', 103, 0 UNION ALL
SELECT '03/07/2018', 105, 0 UNION ALL
SELECT '04/07/2018', 104.50, 0 UNION ALL
SELECT '05/07/2018', 101, 0
)

Need to do an aggregate function inside a subquery by converting rows into columns

Here's input table:Account
accountnumber year quarter catgeory owedamt payedamt writeoffamt
101 2001 1 Tax 130.0 0.0 30.0
101 2001 1 Interest 130.0 30.0 30.0
101 2001 2 Interest 120.0 20.0 20.0
102 2002 1 Interest 120.0 100.0 20.0
102 2002 2 Tax 110.0 100.0 10.0
output
accountnumber year quarter Tax (outstanding) Interest (outstanding)
101 2001 1 100.0 70.0
101 2001 2 0.0 80.0
102 2002 2 10.0 0.0
Here "outstanding" is caluclated on the basis of sum(OWED_AMT - (PAID_AMT + WRITEOFF_AMT)) AS OUTSTANDING
Note: we need to have the columns as Tax and Interest but the amount in those columns should be outstanding which is calculated on the basis of above formula.
Note 2: for the accountntumber:102 ,Interest (outstanding) is calculated
sum(OWED_AMT - (PAID_AMT + WRITEOFF_AMT)) which is (120-(100+20))=0.0
But we dont have any Tax category in that year and the quarter so the output resulted 0.0
If we have Both Tax (outstanding) and Interest (outstanding) as zero in the same year and same quarter we can skip that row
Query i am using is :
with summaryData (accountnumber ,
year ,
quarter, tax, interest) as
(
SELECT accountnumber ,
year ,
quarter ,
SUM(CASE WHEN catgeory = 'Tax' THEN sum(OWED_AMT - (PAID_AMT + WRITEOFF_AMT))
ELSE 0
END),
SUM(CASE WHEN catgeory = 'Interest' THEN sum(OWED_AMT - (PAID_AMT + WRITEOFF_AMT))
ELSE 0
END)
FROM acccount
GROUP BY accountnumber ,
year ,
quarter
)
select * from summaryData
where tax > 0 or interest > 0;
The error i am getting here is :
Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
You don't need the nested aggregations in the CTE. So:
with summaryData (accountnumber, year, quarter, tax, interest) as (
SELECT accountnumber, year, quarter,
SUM(CASE WHEN catgeory = 'Tax' THEN OWED_AMT - (PAID_AMT + WRITEOFF_AMT)
ELSE 0
END),
SUM(CASE WHEN catgeory = 'Interest' THEN OWED_AMT - (PAID_AMT + WRITEOFF_AMT)
ELSE 0
END)
FROM acccount
GROUP BY accountnumber, year, quarter
)

SQL query to calculate interval discount

I have trouble understanding how I can solve this problem with a T-SQL query.
I have a price column and a volume column. In another table a have discounts at different levels of volume. So my discount table could have values as
(StartLevel, DiscountFactor)
(0, 1);
(25, 0.95);
(50, 0.90);
(100, 0.75)
What I want is to calculate a total price. If Volume is 35, I want it to multiply
Price x ((35-25) x 0.95 + (25-0) x 1)
If the volume is 200, it should be
Price x ((200-100) x 0.75 + (100-50) x .9+(50-25) x .95+(25) x 1)
Can anybody help me with a query that solves this?
This can help:
DECLARE #products TABLE
(
id INT ,
price MONEY ,
volume INT
)
DECLARE #discounts TABLE
(
id INT ,
Level INT ,
Factor MONEY
)
INSERT INTO #products
VALUES ( 1, 10, 35 ),
( 2, 15, 200 )
INSERT INTO #discounts
VALUES ( 1, 0, 1 ),
( 2, 25, 0.95 ),
( 3, 50, 0.90 ),
( 4, 100, 0.75 )
SELECT p.id, p.price * SUM(ca.m)
FROM #products p
CROSS APPLY ( SELECT * ,
Factor * ( -Level + LEAD(Level) OVER ( PARTITION BY p.id ORDER BY Level, d ) ) AS m
FROM ( SELECT 1 AS d ,
Level ,
Factor
FROM #discounts
WHERE Level < p.volume
UNION ALL
SELECT 2 AS d ,
p.volume ,
0
) t
) ca
GROUP BY p.id, p.price
Without grouping it returns:
id price volume d Level Factor m
1 10.00 35 1 0 1.00 25.00
1 10.00 35 1 25 0.95 9.50
1 10.00 35 2 35 0.00 NULL
2 15.00 200 1 0 1.00 25.00
2 15.00 200 1 25 0.95 23.75
2 15.00 200 1 50 0.90 45.00
2 15.00 200 1 100 0.75 75.00
2 15.00 200 2 200 0.00 NULL
Then just group by product and sum of m results in:
id Total
1 345.00
2 2531.25
For a given Volume and Price you can get the discount based on interval using LEAD which is available in SQL Server 2012+ onwards.
Sample Data
DECLARE #PriceTable TABLE(Volume INT,Price DECIMAL(9,2) )
DECLARE #Discount TABLE(StartLevel int, DiscountFactor DECIMAL(9,2))
INSERT INTO #PriceTable
VALUES(75, 20.5),
(150, 20),
(250, 20.5),
(0, 15);
INSERT INTO #Discount
VALUES(0, 1),
(25, 0.95),
(50, 0.90),
(100, 0.75);
Query
SELECT Volume,Price,FinalPrice
FROM #PriceTable P
CROSS APPLY(
SELECT SUM(CASE WHEN (MaxLevel >=StartLevel) THEN (MaxLevel-StartLevel) ELSE 0 END *DiscountFactor)*P.Price as FinalPrice
FROM
(
SELECT CASE WHEN LEAD(StartLevel)OVER(ORDER BY StartLevel) < P.Volume THEN LEAD(StartLevel)OVER(ORDER BY StartLevel) ELSE P.Volume END MaxLevel,StartLevel, DiscountFactor
FROM #Discount
) IQ
)T
Output
Volume Price FinalPrice
75 20.50 1460.6250
150 20.00 2625.0000
250 20.50 4228.1250
0 15.00 0.0000