-1

I've been working on more efficient and faster MySQL queries. I have one where I need to sum up a column from a bunch of rows. The column is calculated and rounded based on user rounding preferences, so the method for rounding uses a parameter.

I found that using a parameter generates a quoted factor, and that causes the round function to act like a FLOOR instead of a CEILING.

You can easily see it in this example:

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 |
+----------------+---------------+--------------+

The 7002 value is a real-work example from my data (it's actually a calculated value as well), and 7200/3600 == 1.945. You can see that param_rounded, using '1' (quoted) factors results in incorrect rounding. That's what was happening to me since I always use parameterized queries. hard_rounded is what I'm now doing, first confirming that the factors are appropriate values (they come from a database integer field anyway, so I'm not worried about injection from them) and inserting them directly into the SQL string.

EDIT Using the appropriate data type in a parameter does result in correct rounding. I located the incorrect parameter type in the library I was using for queries.

However, I don't think it should matter, given that the actual number provided the final round in MySQL is correct -- 1.945. The division and multiplication factors occur before that final round, so what I'm giving MySQL works out to ROUND(1.945), which it returns incorrectly. If you output the factors without the final round, you get a column result of 1.945.

Matt H
  • 6,422
  • 2
  • 28
  • 32
  • 2
    Why are you using literal strings in calculations using number types? especially as they will be converted to floating point? – Ryan Vincent Mar 16 '17 at 23:35
  • I have the same results as you on 5.5.52-MariaDB, on CentOS 7, tested in phpmyadmin. – Louis Loudog Trottier Mar 16 '17 at 23:36
  • 4
    If you're going to abuse datatypes like that, read this first; https://dev.mysql.com/doc/refman/5.7/en/type-conversion.html. THEN go read up on floating point numbers being Approximations and so incur rounding errors / bruising. – MatBailie Mar 16 '17 at 23:51
  • 2
    This is not a 'MySQL rounding bug', it is a pointless-use-of-a-string-literal-with-implicit-conversion-to-floating-point bug. Obviously `ROUND()` is working for *some* inputs, so the bug lies in the *inputs*, which are supplied by *you,* and which are not identical. – user207421 Mar 16 '17 at 23:56
  • This may also be relevant to you; https://dev.mysql.com/doc/refman/5.7/en/arithmetic-functions.html – MatBailie Mar 17 '17 at 00:00
  • also : stackoverflow.com/questions/23120584/why-does-mysql-round-floats-way-more-than-expected – Louis Loudog Trottier Mar 20 '17 at 20:48

2 Answers2

1

i'm not sure i undertand the meaning but in the manual they say:

For exact-value numbers, ROUND() uses the “round half away from zero” or “round toward nearest” rule: A value with a fractional part of .5 or greater is rounded up to the next integer if positive or down to the next integer if negative. (In other words, it is rounded away from zero.) A value with a fractional part less than .5 is rounded down to the next integer if positive or up to the next integer if negative.

I feel bad linking the manual to a 5k Rep OP, but there is information about data type on the round() function here. Nothing I could fully understand but it might help you figure it out. => ROUND(X,D)

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

also see the example about

For approximate-value numbers, the result depends on the C library.

Louis Loudog Trottier
  • 1,367
  • 13
  • 26
  • sry, was too big for a comment – Louis Loudog Trottier Mar 16 '17 at 23:41
  • Seems as though the problem is: when a number is used within quotes, it truncates (or rounds down - need to check); and when the number is used directly, it obeys what the manual says. – Dhruv Saxena Mar 16 '17 at 23:45
  • @dhruvsaxena - When using quotes, the arithmetic defaults to floating point, due to implicit conversion to a float rather than an integer. When using integer scalars, the arithmetic is in fixed point. It's not ROUND that's behaving differently, it's the implicit datatype conversion and resulting arithmetic. – MatBailie Mar 17 '17 at 00:03
  • @MatBailie I concur that the datatype conversion was implicit, resulting in an unexpected output. However, I just wanted to give a fair consideration to the question, since it did subtly indicate the use of parametrised queries. I've now tested (and posted an answer here) with the parameterised PDO queries, but without specifying the datatype, which does appear to result in something worth pondering. – Dhruv Saxena Mar 17 '17 at 02:04
  • The problem isn't so much the number being used in quotes -- it's the fact that BINDING using a prepared statements adds the quotes, therefore if you use proper prepared statements, you get the quotes, and thus you get the incorrect rounding result. – Matt H Mar 20 '17 at 13:09
  • @MattH, Your original post speaks about MYSQL bug and nothing about mysqli, PDO, PHP, bind, statement etc and the bug is reproducable internaly within SQL itself. (as i understand). Mysqli binding for int should be similar to $mysql->bindParam('i',$intVariable); where 'i' stand for int (d for Dobule, s for string, and b for blob). But again, this is not your problem as far as i can undestand your question. – Louis Loudog Trottier Mar 20 '17 at 20:33
  • Thanks @LouisLoudogTrottier -- I am using a PHP statement system written by someone else, and that does appear to be the better fix. He is using the 1 parameter as a string. I still don't think MySQL should care about the string vs numeric parameter when rounding like that though, given the multiplications and divisions on the factor occur outside the final round. – Matt H Mar 21 '17 at 13:05
0

A part of the question states:

I found that using a parameter generates a quoted factor

So, it'd been my guess that the queries weren't necessarily written that way, but the quotes were added automatically by the query processor. Even if the quotes weren't added by the query processor, the output obtained resembled the behavior that would otherwise be seen with quotes explicitly added. Thus, in a way, implying that the result obtained was more implicit than as a consequence of some careless play on the OP's part. However, it remained to be seen. And I didn't want to spur up the debate or comment further without a verifiable example. So, here it goes....


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

Program 2:

This program would be exactly the same as the first program, except:

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

Now, most users are accustomed to writing the queries the former way:

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

This works fine for perhaps most arithmetic operations (at least in my personal experience so far), but ROUND seems to bring to the fore a possible issue. An undesirable truncation / rounding down...

Computed         Expected         Obtained
1.945            1.95             1.94

Demo - Though it is only a MySQL query directly, but the behavior is exactly comparable to PDO.


An interesting observation:

It was perhaps worthwhile to check what the manifestation of this whole thing would look like with a recurring fraction or alike: param_rounded_5_places and param_rounded_4_places which were basically computed as 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

It can be seen that the with quotes (and with PDO bindParam() without specifying datatype), the problem persists when the precision is limited to two places after the decimal point, but seems to disappear (at least in this case) when the precision is checked beyond the second decimal place. Is this an issue? I don't know.....

Although, for the purpose of this answer, I haven't run the same test with mysqli_*() PHP functions, but it might quite likely be so that MySQLi Prepared Queries too behave the same way.


FIXES:

  • One obvious fix is to specify the datatype with bindParm(). See mysqli_stmt_bind_param() for the MySQLi equivalent:

    $stmt->bindParam(1, $array[0], PDO::PARAM_INT);
    
  • If, for some reason, it's too cumbersome to change the program, then an indirect fix would be to use CAST in the query. For example:

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

The result of this can be seen in param_rounded_modified, although that would come at the expense of adding a small layer of complexity to the query.


In summary, I feel, the conclusion would be a bit twisted. The error is more of a matter of popular convention (binding without specifying data-types) which, as the situation like this would expose, isn't always right. We obviously can't squarely call it a bug (albeit there is an anomaly as noted above), because MYSQL does provide the means to specify the datatypes in parameterised queries, even though it is generally not explicitly required (in PDO) for rather commonly used arithmetic operations.

Dhruv Saxena
  • 1,336
  • 2
  • 12
  • 29
  • I wasn't using PHP, just straight MySQL console, binding directly with MySQL commands. – Matt H Mar 20 '17 at 13:08
  • I think this $stmt->bindParam(1, $array[0], PDO::PARAM_INT); should read $stmt->bindParam('i', $array[0]); Further more it shoud be 'd' (double) instead of 'i' (int) – Louis Loudog Trottier Mar 20 '17 at 20:38
  • @LouisLoudogTrottier Well, for [`bindParam()`](http://php.net/manual/en/pdostatement.bindparam.php), the numbers 1, 2.. represent the position of the parameter where `?` is used. Unfortunately, PDO doesn't have an explicit data-type override for `double`, `decimal`, etc. Please see [this Q&A](http://stackoverflow.com/q/1335081/2298301). Now, `PDO::PARAM_STR` is implicit (third argument to `bindParam()` is optional), however it then results in the problem that the OP posted. So, to correct this anomaly, we use explicit override for `PDO::PARAM_INT` since the numbers in quotes were `int` anyway. – Dhruv Saxena Mar 20 '17 at 21:02
  • Sorry, though this was mysqli and not PDO. mysqli let you specify int (i), double(d), string(s) or binary(b) and should be used accordinly. i've miss readed the link you posted about mysqli_stmt_bind_param(). My bad, great detailed answer btw. – Louis Loudog Trottier Mar 21 '17 at 00:58
  • @LouisLoudogTrottier Thank you very much for your kind comment! I actually had a PDO code bit ready with me, so thought it would have been quicker for me to adapt it with this query to get to the root of the issue using what was available. I haven't tested the whole thing again using MySQLi, but I guess there are perhaps clues that the problem of slight deviation in the values might be reproducible using MySQLi too. With thanks to the question asked, I guess, I'll now be certainly more careful with `ROUND` functions in future :) – Dhruv Saxena Mar 21 '17 at 01:05