somma divisa problema valori (trattare con errore di arrotondamento)
-
24-10-2019 - |
Domanda
Ho un prodotto che costa 4 € e ho bisogno di dividere questo denaro per 3 dipartimenti. Sulla seconda colonna, ho bisogno di ottenere il numero di righe per questo prodotto e dividere per il numero di dipartimenti.
La mia domanda:
select
department, totalvalue,
(totalvalue / (select count(*) from departments d2 where d2.department = p.product))
dividedvalue
from products p, departments d
where d.department = p.department
Department Total Value Divided Value
---------- ----------- -------------
A 4 1.3333333
B 4 1.3333333
C 4 1.3333333
Ma quando ho sommare i valori, ottengo 3,999999. Naturalmente con centinaia di righe ottengo grandi differenze ... C'è qualche possibilità di definire 2 numeri decimali e rotondo ultimo valore? (I miei risultati sarebbero 1.33 1.33 1.34) Insomma, un modo per regolare l'ultima riga?
Soluzione
Con sei decimali di precisione, si avrebbe bisogno di circa 5.000 transazioni a notare una differenza di un centesimo, se si arrotonda il numero finale a due decimali. Aumentando il numero di decimali da un livello accettabile eliminerebbe gran parte dei problemi, vale a dire utilizzando 9 decimali si avrebbe bisogno di circa 5.000.000 di transazioni a notare una differenza di un centesimo.
Altri suggerimenti
Al fine di gestire questa situazione, per ogni riga si dovrebbe effettuare le seguenti operazioni:
- Eseguire la divisione
- Il giro del risultato per il numero appropriato di centesimi
- Somma la differenza tra l'importo arrotondate e il risultato dell'operazione di divisione
- Quando la somma delle differenze supera la cifra decimale più basso (in questo caso, 0,01), aggiungere tale importo per i risultati della successiva operazione di divisione (dopo l'arrotondamento).
Questa distribuirà quantità frazionarie in modo uniforme le righe. Purtroppo, non esiste un modo semplice per farlo in SQL con le query semplici; probabilmente è meglio eseguire questo codice procedurale.
Per quanto riguarda quanto sia importante, quando si tratta di applicazioni e le istituzioni finanziarie, cose come questa sono molto importanti, anche se è solo per un centesimo, e anche se può accadere solo ogni tot numero di record; in genere, gli utenti vogliono vedere i valori legare al centesimo (o qualunque sia la vostra unità di valuta è) esattamente.
La cosa più importante, non si vuole per consentire un exploit come "Superman III" o "Office Space" a verificarsi.
Forse si può fare una fila indietro che sarà totale - somma (A, B, C). Ma dipende da cosa si vuole fare, se avete bisogno di valore esatto, è possibile mantenere le frazioni, il resto, troncare e non si preoccupano della perdita virtuale
Inoltre può essere fatto semplicemente aggiungendo la differenza arrotondamento di un valore particolare al numero successivo di essere arrotondate (prima dell'arrotondamento). In questo modo la pila rimane sempre la stessa dimensione.
Ecco un'implementazione TSQL (Microsoft SQL Server) dell'algoritmo fornito da Martin :
-- Set parameters.
DECLARE @departments INTEGER = 3;
DECLARE @totalvalue DECIMAL(19, 7) = 4.0;
WITH
CTE1 AS
(
-- Create the data upon which to perform the calculation.
SELECT
1 AS Department
, @totalvalue AS [Total Value]
, CAST(@totalvalue / @departments AS DECIMAL(19, 7)) AS [Divided Value]
, CAST(ROUND(@totalvalue / @departments, 2) AS DECIMAL(19, 7)) AS [Rounded Value]
UNION ALL
SELECT
CTE1.Department + 1
, CTE1.[Total Value]
, CTE1.[Divided Value]
, CTE1.[Rounded Value]
FROM
CTE1
WHERE
Department < @departments
),
CTE2 AS
(
-- Perform the calculation for each row.
SELECT
Department
, [Total Value]
, [Divided Value]
, [Rounded Value]
, CAST([Divided Value] - [Rounded Value] AS DECIMAL(19, 7)) AS [Rounding Difference]
, [Rounded Value] AS [Calculated Value]
FROM
CTE1
WHERE
Department = 1
UNION ALL
SELECT
CTE1.Department
, CTE1.[Total Value]
, CTE1.[Divided Value]
, CTE1.[Rounded Value]
, CAST(CTE1.[Divided Value] + CTE2.[Rounding Difference] - ROUND(CTE1.[Divided Value] + CTE2.[Rounding Difference], 2) AS DECIMAL(19, 7))
, CAST(ROUND(CTE1.[Divided Value] + CTE2.[Rounding Difference], 2) AS DECIMAL(19, 7))
FROM
CTE2
INNER JOIN CTE1
ON CTE1.Department = CTE2.Department + 1
)
-- Display the results with totals.
SELECT
Department
, [Total Value]
, [Divided Value]
, [Rounded Value]
, [Rounding Difference]
, [Calculated Value]
FROM
CTE2
UNION ALL
SELECT
NULL
, NULL
, SUM([Divided Value])
, SUM([Rounded Value])
, NULL
, SUM([Calculated Value])
FROM
CTE2
;
Output:
È possibile collegare qualsiasi numero che si desidera in alto. Non sono sicuro se v'è una dimostrazione matematica per questo algoritmo.