有没有其他人看过这个MySQL舍入错误?

时间:2017-03-16 23:23:38

标签: mysql

我一直致力于提高MySQL查询的效率和速度。我有一个需要从一堆行中总结一列的地方。根据用户舍入首选项计算和舍入列,因此舍入方法使用参数。

我发现使用参数会生成一个引用因子,这会导致round函数像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。

2 个答案:

答案 0 :(得分:1)

我不确定我的含义,但在手册中他们说:

  

对于精确值数字,ROUND()使用“从零开始的一半”   或“向最近的方向”规则:具有0.5的小数部分的值   如果为正数或向下,则向上舍入到下一个整数   下一个整数,如果是负数。 (换句话说,它是圆形的   零。)小数部分小于.5的值向下舍入到   如果为正,则为下一个整数;如果为负,则为下一个整数。

我感觉不好将手册链接到5k Rep OP,但是这里有关于round()函数的数据类型的信息。没有什么我可以完全理解,但它可能会帮助你搞清楚。 => ROUND(X,d)

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

还可以看到关于

的示例
  

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

答案 1 :(得分:0)

问题的一部分陈述:

  

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

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

计划1:

$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;
}

计划2:

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

$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

可以看到with quotes(并且在没有指定数据类型的情况下使用PDO bindParam()),当精度限制在小数点后的两个位置时问题仍然存在,但似乎消失了(至少在这种情况下)当检查精度超过小数点后第二位时。这是一个问题吗?我不知道......

虽然为了这个答案的目的,我还没有使用mysqli_*() PHP函数运行相同的测试,但很可能MySQLI Prepared Queries的行为方式也相同。

FIXES:

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

    $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中) 相当常用的算术运算。