2009-09-17 13 views
3

J'ai une table définie comme ceci:Comment arrondir une colonne dans une seule requête SQL sans modifier la somme globale?

create table #tbFoo 
(bar float) 

Je suis à la recherche d'un moyen pour arrondir chaque valeur contenue dans la barre de colonne sans changer la somme totale (qui est connu pour être un entier, ou très proche d'un nombre entier en raison de la précision du nombre de float).

Arrondir chaque valeur à l'entier le plus proche ne fonctionnera pas (ex: 1,5; 1,5, arrondi à 1, 1 ou 2; 2)

Il est assez facile de le faire en utilisant plusieurs demandes (par exemple stocker la somme originale, arrondir, calculer la nouvelle somme et mettre à jour autant de lignes que nécessaire pour revenir à la somme originale), mais ce n'est pas une solution très élégante.

Existe-t-il un moyen de le faire en utilisant une seule requête SQL? J'utilise SQL Server 2008, donc les solutions tirant parti de ce fournisseur spécifique sont les bienvenues.


Editer: Je cherche une demande en minimisant les différences entre les anciennes valeurs et les nouvelles. En d'autres termes, une valeur ne doit jamais être arrondi si une plus grande valeur a été arrondi vers le bas, et vice-versa

+0

Une bonne question. Je le mets dans ma liste de choses à faire pour les articles de blog. – Quassnoi

Répondre

3

Mise à jour:

Voir cette solution a expliqué plus en détail dans l'article sur mon blog:


Vous devez garder hors cumulatif définie pour chaque valeur:

1.2 (1 + 0.0) ~ 1 1 1.2 +0.2 
1.2 (1 + 0.2) ~ 1 2 2.4 +0.4 
1.2 (1 + 0.4) ~ 1 3 3.6 +0.6 
1.2 (1 + 0.6) ~ 2 5 4.8 -0.2 
1.2 (1 - 0.2) ~ 1 6 6.0 0.0 

Cela se fait facilement en MySQL, mais SQL Server vous devrez écrire un curseur ou subselects cumulatifs (qui sont moins efficaces).

Mise à jour:

La requête ci-dessous sélectionne la différence entre les sommes des valeurs et de celles arrondies au plus petit entier le plus proche.

Cela nous donne le nombre (N) de valeurs que nous devrions arrondir.

Ensuite, nous ordonnons les valeurs par leur partie fractionnaire (ceux qui sont plus proches de leur plafond aller en premier) et autour du premier N, les autres vers le bas.

SELECT value, 
     FLOOR(value) + CASE WHEN ROW_NUMBER() OVER (ORDER BY value - FLOOR(value) DESC) <= cs THEN 1 ELSE 0 END AS nvalue 
FROM (
     SELECT cs, value 
     FROM (
       SELECT SUM(value) - SUM(FLOOR(value)) AS cs 
       FROM @mytable 
       ) c 
     CROSS JOIN 
       @mytable 
     ) q 

Voici le script pour les données de test:

SET NOCOUNT ON 
GO 
SELECT RAND(0.20090917) 
DECLARE @mytable TABLE (value FLOAT NOT NULL) 
DECLARE @cnt INT; 
SET @cnt = 0; 
WHILE @cnt < 100 
BEGIN 
     INSERT 
     INTO @mytable 
     VALUES (FLOOR(RAND() * 100)/10) 
     SET @cnt = @cnt + 1 
END 

INSERT 
INTO @mytable 
SELECT 600 - SUM(value) 
FROM @mytable 
+1

@Quassnoi: merci pour votre réponse. Avec votre solution, 1.4/1.4/1.2 serait arrondi à 1/1/2, ce qui n'est pas "juste" puisque 1.4 est supérieur à 1.2. – Brann

+0

@Quassnoi: c'est exactement ce que je cherchais. Merci ! – Brann

+0

@Quassnoi: dans mon problème realworld, j'ai une autre colonne 'ListID', et je veux que la somme par ListID reste constante. J'ai adapté votre requête en utilisant des clauses group by, et cela fonctionne presque, MAIS le ROW_NUMBER s'applique à l'ensemble de ma requête, au lieu de la requête groupée par requête. Est-ce que je peux faire quelque chose? – Brann

0

d'abord obtenir la différence entre la somme arrondie et la somme réelle, et le nombre d'enregistrements:

declare @Sum float, @RoundedSum float, @Cnt int 

select @Sum = sum(bar), @RoundedSum = sum(round(bar)), @Cnt = count(*) 
from #tbFoo 

Ensuite, vous répartir la différence également sur toutes les valeurs avant d'arrondir:

declare @Offset float 

set @Offset = (@Sum - @RoundedSum)/@Cnt 

select bar = round(bar + @Offset) 
from #tbFoo 
+1

@guffa: si je ne me trompe pas: 0.2/0.2/0.2/0.2/0.2, somme = 1, roundedsum = 0, offest = 0.2, finalresulst = 0/0/0/0/0, finalsum = 0. – Brann

+0

@Brann: Oui, vous avez raison, cela ne donne pas le bon résultat. Cependant, le principe de propagation de la déviation est également sain. Je vais en donner d'avantage ... – Guffa

1

Si vous avez une liste de n valeurs dont les éléments ne sont précis qu'à une valeur entière (+ -0,5), alors la somme de ces éléments aura une erreur cumulative ou + - (n * 0,5). Si vous avez 6 éléments dans votre liste qui devraient correspondre à un certain nombre, votre pire scénario est que vous êtes à 3 si vous ajoutez simplement les valeurs entières.

Si vous trouvez un moyen d'afficher 10.2 comme 11 pour faire fonctionner la somme, vous avez changé la précision de cet élément de + -0.5 à + -0.8, ce qui est contre-intuitif quand on regarde des entiers?

Une solution possible à envisager est d'arrondir votre nombre pendant l'affichage seulement (en utilisant une chaîne de format sur votre sortie), pas déjà à l'étape de récupération. Chaque nombre sera aussi proche que possible de la valeur réelle, mais la somme sera plus correcte aussi. Exemple: Si vous avez 3 valeurs de 1/3 chacune, affichées sous forme de pourcentages entiers, alors vous devriez montrer 33, 33 et 33. Pour faire autre chose, vous devez créer une marge d'erreur supérieure à + - 0,5 pour toute valeur individuelle. Votre total devrait toujours être affiché à 100%, car c'est la meilleure valeur possible (par opposition à travailler avec des sommes de valeurs déjà arrondies)

De plus, sachez qu'en utilisant un flotteur, vous avez déjà introduit une limitation sur votre précision, car vous n'avez aucun moyen de représenter exactement 0,1. Pour en savoir plus, lisez What Every Computer Scientist Should Know About Floating-Point Arithmetic

+0

@Galghamon: Oui, montrer 10.2 comme 11 est exactement ce que j'essaie de réaliser. Et c'est ce que l'utilisateur veut voir aussi. – Brann

Questions connexes