【问题标题】:How can I round a column in a single SQL request without changing the overall sum?如何在单个 SQL 请求中舍入一列而不更改总和?
【发布时间】:2009-09-17 10:17:13
【问题描述】:

我有一个这样定义的表:

create table #tbFoo
(bar float)

我正在寻找一种方法来舍入列栏中包含的每个值而不更改总和(已知为整数,或者由于浮点数精度而非常接近整数)。

将每个值四舍五入到最接近的整数不起作用(例如:1,5;1,5 将四舍五入为 1;1 或 2;2)

使用多个请求很容易做到这一点(例如,存储原始总和、舍入、计算新总和以及根据需要更新尽可能多的行以返回原始总和),但这不是一个非常优雅的解决方案.

有没有办法使用单个 SQL 请求来做到这一点?


我使用的是 SQL Server 2008,因此欢迎使用该特定供应商的解决方案。


编辑:我正在寻找一个最小化旧值和新值之间差异的请求。换句话说,如果一个更大的值被向下舍入,则永远不应向上舍入,反之亦然

【问题讨论】:

  • 一个很好的问题。我将其放入我的博客文章待办事项列表中。

标签: sql sql-server algorithm sql-server-2008


【解决方案1】:

更新:

请参阅我的博客文章中更详细地解释此解决方案:


您需要为每个值保留累积偏移量:

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

这在MySQL 中很容易完成,但在SQL Server 中,您将不得不编写游标或使用累积子选择(效率较低)。

更新:

下面的查询选择值的总和与向下舍入到最接近的较小整数的总和之间的差。

这为我们提供了我们应该四舍五入的值的数量 (N)。

然后我们按小数部分对值进行排序(最接近上限的值排在最前面),并将第一个 N 向上舍入,其他向下舍入。

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

这是测试数据的脚本:

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

【讨论】:

  • @Quassnoi :感谢您的回答。使用您的解决方案,1.4/1.4/1.2 将四舍五入为 1/1/2,这不“公平”,因为 1.4 大于 1.2。
  • @Quassnoi :这正是我正在寻找的东西。谢谢!
  • @Quassnoi :在我的现实问题中,我有另一列“ListID”,我希望每个 ListID 的总和保持不变。我已经使用 group by 子句调整了您的请求,它几乎可以工作,但是 ROW_NUMBER 适用于我的整个请求,而不是按请求分组。有什么我可以做的吗?
  • @Brann:您能否发布一些示例数据和所需的结果集?
  • @Brann:同时,尝试将OVER (ORDER BY value)替换为OVER (PARTITTION BY listID ORDER BY value)。这将按组返回行号。
【解决方案2】:

如果您有一个包含 n 个值的列表,其元素仅在整数值 (+-0.5) 范围内精确,那么这些元素的任何总和都会产生累积误差或 +-(n*0.5)。如果您的列表中有 6 个元素加起来应该是某个数字,那么最糟糕的情况是,如果您只添加整数值,您会落后 3 个。

如果您找到某种方法将 10.2 显示为 11 以使总和起作用,您已将该元素的精度从 +-0.5 更改为 +-0.8,这在查看整数时违反直觉?

一种可能的解决方案是仅在显示期间对数字进行四舍五入(在输出中使用一些格式字符串),而不是在检索阶段。每个数字将尽可能接近实际值,但总和也会更正确。

示例:如果您有 3 个值,每个值为 1/3,显示为整数百分比,那么您应该显示 33、33 和 33。其他任何操作都是创建大于 +-0.5 的误差范围对于任何个人价值。您的总数仍应显示为 100%,因为这是可能的最佳值(而不是使用已舍入值的总和)

另外,请注意,通过使用浮点数,您已经引入了对精度的限制,因为您无法准确地表示 0.1。有关更多信息,请阅读 What Every Computer Scientist Should Know About Floating-Point Arithmetic

【讨论】:

  • @Galghamon:是的,将 10.2 显示为 11 正是我想要实现的目标。这也是用户希望看到的。
【解决方案3】:

先得到四舍五入和实际总和的差,以及记录数:

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

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

然后在四舍五入之前将差值平均分配到所有值上:

declare @Offset float

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

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

【讨论】:

  • @guffa:如果我没记错的话:0.2/0.2/0.2/0.2/0.2,sum=1,roundedsum=0,offest=0.2,finalresulst = 0/0/0/0/ 0,总和=0。
  • @Brann:是的,你是对的,这并没有给出正确的结果。但是,平均分配偏差的原则是合理的。不过我会再给它一些...
猜你喜欢
  • 2017-09-13
  • 1970-01-01
  • 2018-11-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-29
  • 2015-03-28
  • 1970-01-01
相关资源
最近更新 更多