【问题标题】:Has anyone else seen this MySQL rounding bug?有没有其他人见过这个 MySQL 舍入错误?
【发布时间】:2017-08-08 08:44:09
【问题描述】:

我一直致力于更高效、更快速的 MySQL 查询。我有一个需要从一堆行中总结一列的地方。该列是根据用户舍入偏好计算和舍入的,因此舍入的方法使用了一个参数。

我发现使用参数会生成一个带引号的因子,这会导致圆形函数像 FLOOR 而不是 CEILING。

您可以在此示例中轻松看到它:

mysql> SELECT ROUND(1.945,2), ROUND((ROUND((7002) / '1') * '1') / 3600,2) AS param_rounded, ROUND((ROUND((7002) / 1) * 1) / 3600,2) AS hard_rounded;
+----------------+---------------+--------------+
| ROUND(1.945,2) | param_rounded | hard_rounded |
+----------------+---------------+--------------+
|           1.95 |          1.94 |         1.95 |
+----------------+---------------+--------------+

7002 值是我数据中的一个真实示例(它实际上也是一个计算值),7200/3600 == 1.945。您可以看到param_rounded,使用'1'(引用)因子会导致不正确的舍入。这就是发生在我身上的事情,因为我总是使用参数化查询。 hard_rounded 是我现在正在做的事情,首先确认这些因素是适当的值(它们无论如何都来自数据库整数字段,所以我不担心它们的注入)并将它们直接插入到 SQL 字符串中。

编辑 在参数中使用适当的数据类型确实会导致正确的舍入。我在用于查询的库中找到了错误的参数类型。

但是,我认为这并不重要,因为 MySQL 最后一轮提供的实际数字是正确的——1.945。除法和乘法因子发生在最后一轮之前,所以我给 MySQL 的结果是 ROUND(1.945),它返回不正确。如果你在没有最后一轮的情况下输出因子,你会得到 1.945 的列结果。

【问题讨论】:

  • 为什么在使用数字类型的计算中使用文字字符串?特别是因为它们将被转换为浮点数?
  • 我在 5.5.52-MariaDB 上的结果和你一样,在 CentOS 7 上,在 phpmyadmin 中测试过。
  • 如果您要滥用这样的数据类型,请先阅读此内容; dev.mysql.com/doc/refman/5.7/en/type-conversion.html。然后去阅读浮点数是近似值,因此会产生舍入错误/瘀伤。
  • 这不是一个“MySQL 舍入错误”,它是一个无意义的字符串文字与隐式转换到浮点错误。显然ROUND() 正在为一些 输入工作,所以错误在于输入,它们由you, 提供,它们并不相同.

标签: mysql


【解决方案1】:

我不确定我是否理解其中的含义,但在手册中他们说:

对于精确值数字,ROUND() 使用“距离零的一半” 或“向最近舍入”规则:小数部分为 0.5 的值 如果为正数或更大,则向上舍入到下一个整数或向下舍入到 如果为负,则为下一个整数。 (换句话说,它从 零。)小数部分小于 0.5 的值向下舍入为 如果为正则为下一个整数,如果为负则为下一个整数。

我觉得将手册链接到 5k Rep OP 很糟糕,但这里有关于 round() 函数的数据类型的信息。没有什么我能完全理解的,但它可能会帮助你弄清楚。 => 圆(X,D)

https://dev.mysql.com/doc/refman/5.7/en/mathematical-functions.html#function_round

另见示例

对于近似值数字,结果取决于 C 库。

【讨论】:

  • 抱歉,评论太大了
  • 似乎问题是:当引号内使用数字时,它会截断(或向下舍入 - 需要检查);并且直接使用数字时,按照说明书上的为准。
  • @dhruvsaxena - 使用引号时,算术默认为浮点数,因为隐式转换为浮点数而不是整数。使用整数标量时,算术是定点的。表现不同的不是 ROUND,而是隐式数据类型转换和结果算术。
  • @MatBailie 我同意数据类型转换是隐式的,导致意外输出。但是,我只是想公平地考虑这个问题,因为它确实巧妙地表明了参数化查询的使用。我现在已经使用参数化的 PDO 查询进行了测试(并在此处发布了答案),但没有指定数据类型,这似乎确实会导致一些值得思考的事情。
  • 问题不在于引号中使用的数字——而是使用准备好的语句绑定添加引号的事实,因此如果您使用正确的准备好的语句,您会得到引号,并且因此你得到不正确的舍入结果。
【解决方案2】:

部分问题指出:

我发现使用参数会生成带引号的因子

所以,我猜查询不一定是这样写的,但引号是由查询处理器自动添加的。即使查询处理器没有添加引号,所获得的输出也类似于在显式添加引号时会看到的行为。因此,在某种程度上,暗示获得的结果比 OP 的一些粗心发挥更隐含。但是,仍有待观察。如果没有可验证的例子,我不想激起辩论或进一步评论。所以,就这样吧……


方案一:

$servername = "localhost";
$username = "test";
$password = "test";
$dbname = "testdb";

$array  =   array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1);

try {
    $conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $stmt = $conn->prepare("SELECT ROUND(1.945,2) AS test_round, 
                            ROUND((ROUND((7002) / ?) * ?) / 3600,2) AS param_rounded, 
                            ROUND(
                                    (7002 / ?)* ?
                                    / 3603,5
                            )AS param_rounded_5_places, 
                            ROUND(
                                (7002 / ?)* ?
                                / 3603,4
                            )AS param_rounded_4_places,      
                            ROUND(
                                (7002 / ?)* ?
                                / 3603,3
                            )AS param_rounded_3_places,           
                            ROUND((ROUND((7002) / 1) * 1) / 3600,2) AS hard_rounded,
                            ROUND((ROUND((7002) / CAST(? AS DECIMAL(10,2))) * CAST(? AS DECIMAL(10,2))) / 3600,2) AS param_rounded_modified"
                        );

    $stmt->bindParam(1, $array[0]);
    $stmt->bindParam(2, $array[1]);
    $stmt->bindParam(3, $array[2]);
    $stmt->bindParam(4, $array[3]);
    $stmt->bindParam(5, $array[4]);
    $stmt->bindParam(6, $array[5]);
    $stmt->bindParam(7, $array[6]);
    $stmt->bindParam(8, $array[7]);
    $stmt->bindParam(9, $array[8]);
    $stmt->bindParam(10, $array[9]);

    $stmt->execute();
    var_dump($stmt->fetchAll(PDO::FETCH_ASSOC));
}
catch(PDOException $e){
    echo "Diagnostic: ".$e;
}

方案二:

这个程序与第一个程序完全相同,除了:

$stmt->bindParam(1, $array[0], PDO::PARAM_INT);
$stmt->bindParam(2, $array[1], PDO::PARAM_INT);
$stmt->bindParam(3, $array[2], PDO::PARAM_INT);
$stmt->bindParam(4, $array[3], PDO::PARAM_INT);
$stmt->bindParam(5, $array[4], PDO::PARAM_INT);
$stmt->bindParam(6, $array[5], PDO::PARAM_INT);
$stmt->bindParam(7, $array[6], PDO::PARAM_INT);
$stmt->bindParam(8, $array[7], PDO::PARAM_INT);
$stmt->bindParam(9, $array[8], PDO::PARAM_INT);
$stmt->bindParam(10, $array[9], PDO::PARAM_INT);

现在,大多数用户习惯于用前一种方式编写查询:

$stmt->bindParam(1, $array[0]);

这可能适用于大多数算术运算(至少在我个人到目前为止的经验中),但ROUND 似乎突出了一个可能的问题。不受欢迎的截断/四舍五入...

Computed         Expected         Obtained
1.945            1.95             1.94

Demo - 虽然它只是一个直接的 MySQL 查询,但其行为与 PDO 完全一样。


一个有趣的观察:

也许值得用一个重复的分数或类似的东西来检查整个事情的表现形式:param_rounded_5_placesparam_rounded_4_places,它们基本上被计算为 7002 / 3603 ===> 1.943380516...

                          Computed        Expected        Obtained
param_rounded             1.945           1.95            1.94     --> Rounded down 
param_rounded_5_places    1.943380516     1.94338         1.94338 
param_rounded_4_places    1.943380516     1.9434          1.9434   --> NOT Rounded down

可以看出withquotes(和with PDObindParam()没有指定datatype),当精度限制在小数点后两位时问题依然存在,但似乎消失了(至少在这个case) 当精度检查超过小数点后第二位时。这是一个问题吗?我不知道.....

尽管出于此答案的目的,我没有使用mysqli_*() PHP 函数运行相同的测试,但很可能 MySQLi 准备查询的行为方式也相同。


修复:

  • 一个明显的解决方法是使用bindParm() 指定数据类型。请参阅mysqli_stmt_bind_param() 获取 MySQLi 等效项:

    $stmt->bindParam(1, $array[0], PDO::PARAM_INT);
    
  • 如果由于某种原因更改程序太麻烦,那么间接 解决方法是在查询中使用CAST。例如:

    ROUND((ROUND((7002) / CAST(? AS DECIMAL(10,2))) * CAST(? AS DECIMAL(10,2))) / 3600,2)
    

这个结果可以在param_rounded_modified 中看到,尽管这会以增加查询的复杂性为代价。


总之,我觉得结论会有点扭曲。错误更多是流行约定(绑定而不指定数据类型)的问题,因为这种情况会暴露,并不总是正确的。我们显然不能直接称其为错误(尽管存在如上所述的异常),因为 MYSQL 确实提供了在参数化查询中指定数据类型的方法,即使通常不明确要求(在 PDO 中)相当常用的算术运算。

【讨论】:

  • 我没有使用 PHP,只是直接使用 MySQL 控制台,直接与 MySQL 命令绑定。
  • 我认为这个 $stmt->bindParam(1, $array[0], PDO::PARAM_INT);应该读为 $stmt->bindParam('i', $array[0]);此外,它应该是 'd' (double) 而不是 'i' (int)
  • @LouisLoudogTrottier 好吧,对于bindParam(),数字 1、2.. 表示使用 ? 的参数的位置。不幸的是,PDO 没有针对doubledecimal 等的显式数据类型覆盖。请参阅this Q&A。现在,PDO::PARAM_STR 是隐式的(bindParam() 的第三个参数是可选的),但是它会导致 OP 发布的问题。因此,为了纠正这个异常,我们对PDO::PARAM_INT 使用显式覆盖,因为引号中的数字无论如何都是int
  • 对不起,虽然这是 mysqli 而不是 PDO。 mysqli 让您指定 int (i)、double(d)、string(s) 或 binary(b),应相应地使用。我错过了阅读您发布的有关 mysqli_stmt_bind_param() 的链接。顺便说一句,我的不好的,非常详细的答案。
  • @LouisLoudogTrottier 非常感谢您的友好评论!实际上,我已经准备好了一个 PDO 代码位,所以我认为使用这个查询来调整它以使用可用的东西找到问题的根源会更快。我还没有使用 MySQLi 再次测试整个事情,但我想可能有线索表明,值的轻微偏差问题也可能使用 MySQLi 重现。由于提出的问题,我想,我现在肯定会更加小心 ROUND 未来的功能:)
猜你喜欢
  • 2019-09-28
  • 1970-01-01
  • 2022-06-29
  • 1970-01-01
  • 1970-01-01
  • 2012-09-01
  • 1970-01-01
  • 2016-10-27
  • 1970-01-01
相关资源
最近更新 更多