SQL Rounding Percentages to make the sum 100% - 1/3 as 0.34, 0.33, 0.33 - sql

I am currently trying to split one value with percentage column. But as most of percentages values are 1/3, I am not able to get aboslute 100% with two decimal points in the value. For example:
Product Supplier percentage totalvalue customer_split
decimal(15,14) (decimal(18,2) decimal(18,2)
-------- -------- ------------ --------------- ---------------
Product1 Supplier1 0.33 10.00 3.33
Product1 Supplier2 0.33 10.00 3.33
Product1 Supplier3 0.33 10.00 3.33
So, here we are missing 0.01 in the value column and suppliers would like to put this missing 0.01 value against any one of the supplier randomly. I have been trying to get this done in a two sets of SQLs with temporary tables, but is there any simple way of doing this. If possible how can I get 0.34 in the percentage column itself for one of the above rows? 0.01 is negligible value, but when the value column is 1000000000 it is significant.

It sounds like you're doing some type of "allocation" here. This is a common problem any time you are trying to allocate something from a higher granulartiy to a lower granularity, and you need to be able to re-aggregate to the total value correctly.
This becomes a much bigger problem when dealing with larger fractions.
For example, if I try to divide a total value of, say $55.30 by eight, I get a decimal value of $6.9125 for each of the eight buckets. Should I round one to $6.92 and the rest to $6.91? If I do, I will lose a cent. I would have to round one to $6.93 and the others to $6.91. This gets worse as you add more buckets to divide by.
In addition, when you start to round, you introduce problems like "Should 33.339 be rounded to 33.34 or 33.33?"
If your business logic is such that you just want to take whatever remainder beyond 2 significant digits may exist and add it to one of the dollar values "randomly" so you don't lose any cents, #Diego is on the right track with this.
Doing it in pure SQL is a bit more difficult. For starters, your percentage isn't 1/3, it's .33, which will yield a total value of 9.9, not 10. I would either store this as a ratio or as a high-precision decimal field (.33333333333333).
P S PCT Total
-- -- ------------ ------
P1 S1 .33333333333 10.00
P2 S2 .33333333333 10.00
P3 S3 .33333333333 10.00
SELECT
BaseTable.P, BaseTable.S,
CASE WHEN BaseTable.S = TotalTable.MinS
THEN BaseTable.BaseAllocatedValue + TotalTable.Remainder
ELSE BaseTable.BaseAllocatedValue
END As AllocatedValue
FROM
(SELECT
P, S, FLOOR((PCT * Total * 100)) / 100 as BaseAllocatedValue,
FROM dataTable) BaseTable
INNER JOIN
(SELECT
P, MIN(S) AS MinS,
SUM((PCT * Total) - FLOOR((PCT * Total * 100)) / 100) as Remainder,
FROM dataTable
GROUP BY P) as TotalTable
ON (BaseTable.P = TotalTable.P)
It appears your calculation is an equal distribution based on the total number of products per supplier. If it is, it may be advantageous to remove the percentage and instead just store the count of items per supplier in the table.
If it is also possible to store a flag indicating the row that should get the remainder value applied to it, you could assign based on that flag instead of randomly.

run this, it will give an idea on how you can solve your problem.
I created a table called orders just with an ID to be easy to understand:
create table orders(
customerID int)
insert into orders values(1)
go 3
insert into orders values(2)
go 3
insert into orders values(3)
go 3
these values represent the 33% you have
1 33.33
2 33.33
3 33.33
now:
create table #tempOrders(
customerID int,
percentage numeric(10,2))
declare #maxOrder int
declare #maxOrderID int
select #maxOrderID = max(customerID) from orders
declare #total numeric(10,2)
select #total =count(*) from orders
insert into #tempOrders
select customerID, cast(100*count(*)/#total as numeric(10,2)) as Percentage
from orders
group by customerID
update #tempOrders set percentage = percentage + (select 100-sum(Percentage) from #tempOrders)
where customerID =#maxOrderID
this code will basically calculate the percentage and the order with the max ID, then it gets the diference from 100 to the percentage sum and add it to the order with the maxID (your random order)
select * from #tempOrders
1 33.33
2 33.33
3 33.34

This should be an easy task using Windowed Aggregate Functions. You probably use them already for the calculation of customer_split:
totalvalue / COUNT(*) OVER (PARTITION BY Product) as customer_split
Now sum up the customer_splits and if there's a difference to total value add (or substract) it to one random row.
SELECT
Product
,Supplier
,totalvalue
,customer_split
+ CASE
WHEN COUNT(*)
OVER (PARTITION BY Product
ROWS UNBOUNDED PRECEDING) = 1 -- get a random row, using row_number/order you might define a specific row
THEN totalvalue - SUM(customer_split)
OVER (PARTITION BY Product)
ELSE 0
END
FROM
(
SELECT
Product
,Supplier
,totalvalue
,totalvalue / COUNT(*) OVER (PARTITION BY Product) AS customer_split
FROM dropme
) AS dt

After more than one trial and test i think i found better solution
Idea
Get Count of all(Count(*)) based on your conditions
Get Row_Number()
Check if (Row_Number() value < Count(*))
Then select round(curr_percentage,2)
Else
Get sum of all other percentage(with round) and subtract it from 100
This steps will select current percentage every time EXCEPT Last one will be
100 - the sum of all other percentages
this is part of my code
Select your_cols
,(Select count(*) from [tbl_Partner_Entity] pa_et where [E_ID] =#E_ID)
AS cnt_all
,(ROW_NUMBER() over ( order by pe.p_id)) as row_num
,Case when (
(ROW_NUMBER() over ( order by pe.p_id)) <
(Select count(*) from [tbl_Partner_Entity] pa_et where [E_ID] =#E_ID))
then round(([partnership_partners_perc]*100),2)
else
100-
((select sum(round(([partnership_partners_perc]*100),2)) FROM [dbo].
[tbl_Partner_Entity] PEE where [E_ID] =#E_ID and pee.P_ID != pe.P_ID))
end AS [partnership_partners_perc_Last]
FROM [dbo].[tbl_Partner_Entity] PE
where [E_ID] =#E_ID

Related

Rounding all numbers but keep the sum

Say I have 4 records as follows:
CASH
========
1993.772
5015.572
996.884
1993.772
These numbers add up to 10000.00. Now I want to round all these numbers to two decimal places, but keep the sum as 10000.00.
In the above example, if I remove the last digit, the sum of the numbers will be 9999.99 and not 10000.00, but something like this would still add up to 10000.00:
CASH
========
1993.78 <- changed from 1993.77 to 1993.78
5015.57
996.88
1993.77
Any easy way to do it?
This is challenging. Here is one method:
select t.*,
round(cash, 2),
(case when row_number() over (order by cash desc) = 1
then sum(cash) over () - sum(round(cash, 2)) over (order by cash rows between unbounded preceding and 1 preceding)
else round(cash, 2)
end)
from t;
Here is a db<>fiddle.
Basically, this rounds all the values to two decimal places except the biggest one. For that one, it subtracts the sum of the rounded values from the total.
Note: This adds the extra to the largest value. It could round the smallest value, but I think rounding the largest is safer (a smaller incremental change to the value). If you have other columns to specify the ordering, then the "first" or "last" column can be chosen instead.
Aggregate all of the values up into a sum and into an array, then unroll the array back into their separate rows.
SELECT
unnest(array_agg(round(ct.cash, 2))) AS cash,
round(sum(ct.cash), 2) AS total
FROM cash_table AS ct;
Result
cash | total
--------+---------
1993.77 | 10000.00
5015.57 | 10000.00
996.88 | 10000.00
1993.77 | 10000.00

Sliding window functions in SQL Server, advanced calculation

I have a problem that it's very easy to be solved in C# code for example, but I have no idea how to write in a SQL query.
Here is the situation: let's say I have a table with 3 columns (ID, Date, Amount), and here is some data:
ID Date Amount
-----------------------
1 01.01.2016 -500
2 01.02.2016 1000
3 01.03.2016 -200
4 01.04.2016 300
5 01.05.2016 500
6 01.06.2016 1000
7 01.07.2016 -100
8 01.08.2016 200
The result I want to get from the table is this (ID, Amount .... Order By Date):
ID Amount
-----------------------
2 300
4 300
5 500
6 900
8 200
The idea is to distribute the amounts into installments, but the thing is when negative amount comes into play you need to remove amount from the last installment. I don't know how clear I am, so here is an example:
Let's say I have 3 Invoices with amounts 500, 200, -300.
If i start distribute these Invoices, first i distribute the amount 500, then 200. But when i come to the third one -300, then i need to remove from the last Invoice. In other workds 200 - 300 = -100, so the amount from second Invoice will disappear, but there are still -100 that needs to be substracted from first Invoice. So 500 - 100 = 400. The result i need is dataset with one row (first invoice with amount 400)
Another example when the first invoice is with negative amount (-500, 300, 500).
In this case, the first (-500) invoice will make the second disappear and another 200 will be substracted from the third. So the result will be: Third Invoice with amount 300.
This is something like Stack implementation in programming language, but i need to make it with sliding-window functions in SQL Server.
If anyone have any idea, please share.
Thanks.
I solved it using TSQL. But I think what this task also can solve using recursive CTE.
I used ID for finding a prev or next row.
-- create and fill test table
CREATE TABLE Invoices(
ID int,
[Date] date,
Amount float
)
INSERT Invoices(ID,Date,Amount) VALUES
(1,'20160101', -500),
(2,'20160201', 1000),
(3,'20160301', -200),
(4,'20160401', 300),
(5,'20160501', 500),
(6,'20160601', 1000),
(7,'20160701', -100),
(8,'20160801', 200)
My solution
-- copy all the data into temp table
SELECT *
INTO #Invoices
FROM Invoices
DECLARE
#nID int,
#nAmount float,
#pID int
-- run infinity loop
WHILE 1=1
BEGIN
-- set all the variables to NULL
SET #nID=NULL
SET #nAmount=NULL
SET #pID=NULL
-- get data from the last negative row
SELECT
#nID=ID,
#nAmount=Amount
FROM
(
SELECT TOP 1 *
FROM #Invoices
WHERE Amount<0
ORDER BY ID DESC
) q
-- get prev positive row
SELECT #pID=ID
FROM
(
SELECT TOP 1 *
FROM #Invoices
WHERE ID<#nID
AND Amount>0
ORDER BY ID DESC
) q
IF(#pID IS NULL)
BEGIN
-- get next positive row
SELECT #pID=ID
FROM
(
SELECT TOP 1 *
FROM #Invoices
WHERE ID>#nID
AND Amount>0
ORDER BY ID
) q
END
-- exit from loop
IF(#pID IS NULL) BREAK
-- substract amount from positive row
UPDATE #Invoices
SET
Amount+=#nAmount
WHERE ID=#pID
-- delete used negative row
DELETE #Invoices
WHERE ID=#nID
END
-- show result
SELECT *
FROM #Invoices
DROP TABLE #Invoices

Complicate SQL Amount split by percentage in same row transpose (Pivot)?

I am struggling to split total amount field into percentage in the same row and then update the last column with Amount type for which the percentage is applied.
Example data
Total Amount | UF% | UFI% |RA% |RL% |NP% | AmountType
100 |0.00 |20 |9.15 |0.75 |70.01
1520.23 |64.4 |19.1 |15.5 |0.25 |0.75
158520.03|13.25 |35 |2.25 |19.28 |30.22
I have to get percentage of total amount column and then transpose insert them as additional rows in the same table and upate the last column what type of amount it is.
For example for 1st row I can get 5 new rows
Total Amount Amount type
0 UF%
20 UFI%
9.15 RA%
0.75 RL%
70.01 NP%
I am one step at a time to I have created 5 new columns to calculate the percentage as TotalAmount UF%, TotalAmount UFI%, TotalAmountRA% and so on…
Selec t [Total Amount]* UF% as [TotalAmount UF%] … and so on.
I am stuck here shall I use Pivot/unpivot? Or case ?
Or is it any other easier way to use row over partition by ?
Please suggest.
this should work for you. Just copy this into an empty query window and execute. Adapt to your needs...
EDIT: Calculate percentages...
declare #amounts table (TotalAmount decimal(8,2),[UF%] decimal(4,2), [UFI%] decimal(4,2)
,[RA%] decimal(4,2),[RL%] decimal(4,2)
,[NP%] decimal(4,2));
insert into #amounts values
(100,0.00,20,9.15,0.75,70.01)
,(1520.23,64.4,19.1,15.5,0.25,0.75)
,(158520.03,13.25,35,2.25,19.28,30.22);
select up.TotalAmount
,up.Percentag
,(up.TotalAmount/100)*up.Percentag AS AmountPercentage
,up.Amount AS AmountType
from
(
select *
from #amounts
) AS tbl
unpivot
(
Percentag FOR Amount IN([UF%],[UFI%],[RA%],[RL%],[NP%])
) AS up

SQL Percentage Complete with COUNT and Remaining Percentage

Quite difficult to explain in the title so here is my scenario:
Let's say I have a value of £10,000, this is going to be paid in random instalments (up to 100%). The cumulative total is stored in a field called PercentageComplete.
Payment 1 is £5,000 (PercentageComplete = 50% of total amount owed)
Payment 2 is £2,500 (PercentageComplete = 75% of total amount owed)
So far the way I'm trying to work this out is by adding (0.5 + 0.75) which gives me a total of 1.25 ... and using the COUNT function I'm dividing this by 2 (which gives the incorrect answer of 0.625).
I'm probably over complicating this issue so thanks for any help.
The code I tried:
select FinanceID, CustomerID sum(PercentageComplete), COUNT(PercentageComplete)
from Finances group by FinanceID, CustomerID
I think you just want max():
select FinanceID, CustomerID max(PercentageComplete)
from Finances
group by FinanceID, CustomerID ;

My aggregate is not affected by ROLLUP

I have a query similar to the following:
SELECT CASE WHEN (GROUPING(Name) = 1) THEN 'All' ELSE Name END AS Name,
CASE WHEN (GROUPING(Type) = 1) THEN 'All' ELSE Type END AS Type,
sum(quantity) AS [Quantity],
CAST(sum(quantity) * (SELECT QuantityMultiplier FROM QuantityMultipliers WHERE a = t.b) AS DECIMAL(18,2)) AS Multiplied Quantity
FROM #Table t
GROUP BY Name, Type WITH ROLLUP
I'm trying to return a list of Names, Types, a summed Quantity and a summed quantity multiplied by an arbitrary number. All fine so far. I also need to return a sub-total row per Name and per Type, such as the following
Name Type Quantity Multiplied Quantity
------- --------- ----------- -------------------
a 1 2 4
a 2 3 3
a ALL 5 7
b 1 6 12
b 2 1 1
b ALL 7 13
ALL ALL 24 40
The first 3 columns are fine. I'm getting null values in the rollup rows for the multiplied quantity though. The only reason I can think this is happening is because SQL doesn't recognize the last column as an aggregate now that I've multiplied it by something.
Can I somehow work around this without things getting too convoluted?
I will be falling back onto temporary tables if this can't be done.
In your sub-query to acquire the multiplier, you have WHERE a=b. Are either a or b from the tables in your main query?
If these values are static (nothing to do with the main query), it looks like it should be fine...
If the a or b values are the name or type field, they can be NULL for the rollup records. If so, you can change to something similiar to...
CAST(sum(quantity * (<multiplie_query>)) AS DECIMAL(18,2)).
If a or b are other field from your main query, you'd be getting multiple records back, not just a single multiplier. You could change to something like...
CAST(sum(quantity) * (SELECT MAX(multiplier) FROM ...)) AS DECIMAL(18,2))