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

谁说我不能喝 提交于 2021-02-07 09:20:59

问题


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.


回答1:


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.




回答2:


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



回答3:


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



回答4:


After more than one trial and test i think i found better solution

Idea

  1. Get Count of all(Count(*)) based on your conditions
  2. Get Row_Number()
  3. 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


来源:https://stackoverflow.com/questions/9161404/sql-rounding-percentages-to-make-the-sum-100-1-3-as-0-34-0-33-0-33

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!