Bug #52361 Ifnull(Subquery) produces incorrect values
Submitted: 25 Mar 2010 12:42 Modified: 29 Mar 2010 15:22
Reporter: Adam Trotter Email Updates:
Status: Duplicate Impact on me:
None 
Category:MySQL Server: Optimizer Severity:S2 (Serious)
Version:5.1.45, 5.1.46-bzr OS:Linux
Assigned to: Assigned Account CPU Architecture:Any
Tags: ifnull, regression, subquery

[25 Mar 2010 12:42] Adam Trotter
Description:
I am not certain this is a problem with the query optimizer as this must be reproduced in a very specific way and it seems as if it's both an issue with the query optimizer and handling of particular data types.

Given the sample tables and data, records 2, 3, 5, and 6 in t1 should produce a NULL result from the subquery (and do).  However, if the subquery is wrapped with an ifnul(subquery,0), it returns the portion of the number before the decimal from the last row that produced a result, and drops the digits after the decimal.

This is not an issue in 5.1.44, so it's a new issue to 5.1.45.

All of the following must happen for the bug to appear:

Unioning the query with the bug to another query (without the union, the query works fine)

The query is using an index (in this case the primary key) on the main table.  

A left join must be present such that for the records that will contain information that is going to be summed, they produce multiple rows.

A group by must be present to collapse the duplicated rows produced by the left join.

All of the above leads me to believe that this is a problem with the query optimizer, however, doing a simple operation on the ifnull fixes the issue as well.  If instead of saying ifnull(subquery,0) we say ifnull(subquery,'0') or the result of the subquery is ran through the cast function first, the problem does not happen.  Also, if the decimal(18,2) field is changed to a double, the problem does not happen either.

How to repeat:
drop temporary table if exists t1;
create temporary table t1
(
 id int unsigned,
 name varchar(50),
 primary key(id)
);

insert into t1 set id=1, name='Test 1 (Has Amount)';
insert into t1 set id=2, name='Test 2 (No Amount)';
insert into t1 set id=3, name='Test 3 (No Amount)';
insert into t1 set id=4, name='Test 4 (Has Amount)';
insert into t1 set id=5, name='Test 5 (No Amount)';
insert into t1 set id=6, name='Test 6 (No Amount)';

drop temporary table if exists t2;
create temporary table t2
(
 id int unsigned,
 t1_id int unsigned,
 primary key(id),
 index t1id_index(t1_id)
);

insert into t2 set id=1, t1_id=1;
insert into t2 set id=2, t1_id=1;
insert into t2 set id=3, t1_id=1;
insert into t2 set id=4, t1_id=1;

insert into t2 set id=5, t1_id=4;
insert into t2 set id=6, t1_id=4;
insert into t2 set id=7, t1_id=4;
insert into t2 set id=8, t1_id=4;

drop temporary table if exists t3;
create temporary table t3
(
 t1_id int unsigned,
 amount decimal(18,2)
);

insert into t3 set t1_id=1, amount=10.15;
insert into t3 set t1_id=1, amount=10.25;
insert into t3 set t1_id=1, amount=10.30;
insert into t3 set t1_id=4, amount=10.01;
insert into t3 set t1_id=4, amount=10.03;
insert into t3 set t1_id=4, amount=10.09;

(
 select t1.id,
        t1.name,
        ifnull(
         (select sum(amount)
          from t3
          where t3.t1_id=t1.id
         ),0) as total
 from t1
 force index(primary)
 left join t2 on t2.t1_id=t1.id
 group by t1.id
)
union
(select 1 as id,
        'bug' as name,
        0 as ytd_total
 from dual
);
[25 Mar 2010 12:44] Adam Trotter
Meant to tag this S2, not S3 :)
[25 Mar 2010 12:47] Adam Trotter
Results of the query:

+----+---------------------+-------+
| id | name                | total |
+----+---------------------+-------+
|  1 | Test 1 (Has Amount) | 30.70 |
|  2 | Test 2 (No Amount)  | 30.00 |
|  3 | Test 3 (No Amount)  | 30.00 |
|  4 | Test 4 (Has Amount) | 30.13 |
|  5 | Test 5 (No Amount)  | 30.00 |
|  6 | Test 6 (No Amount)  | 30.00 |
|  1 | bug                 |  0.00 |
+----+---------------------+-------+

Expected results:

+----+---------------------+-------+
| id | name                | total |
+----+---------------------+-------+
|  1 | Test 1 (Has Amount) | 30.70 |
|  2 | Test 2 (No Amount)  | 0     |
|  3 | Test 3 (No Amount)  | 0     |
|  4 | Test 4 (Has Amount) | 30.13 |
|  5 | Test 5 (No Amount)  | 0     |
|  6 | Test 6 (No Amount)  | 0     |
|  1 | bug                 | 0     |
+----+---------------------+-------+
[25 Mar 2010 12:59] Valeriy Kravchuk
With 5.1.43 I've got correct results:

mysql> (
    ->  select t1.id,
    ->         t1.name,
    ->         ifnull(
    ->          (select sum(amount)
    ->           from t3
    ->           where t3.t1_id=t1.id
    ->          ),0) as total
    ->  from t1
    ->  force index(primary)
    ->  left join t2 on t2.t1_id=t1.id
    ->  group by t1.id
    -> )
    -> union
    -> (select 1 as id,
    ->         'bug' as name,
    ->         0 as ytd_total
    ->  from dual
    -> );
+----+---------------------+-------+
| id | name                | total |
+----+---------------------+-------+
|  1 | Test 1 (Has Amount) | 30.70 |
|  2 | Test 2 (No Amount)  |  0.00 |
|  3 | Test 3 (No Amount)  |  0.00 |
|  4 | Test 4 (Has Amount) | 30.13 |
|  5 | Test 5 (No Amount)  |  0.00 |
|  6 | Test 6 (No Amount)  |  0.00 |
|  1 | bug                 |  0.00 |
+----+---------------------+-------+
7 rows in set (0.11 sec)

mysql> select version();
+----------------------+
| version()            |
+----------------------+
| 5.1.43-community-log |
+----------------------+
1 row in set (0.02 sec)

So, this looks like recent regression.
[25 Mar 2010 13:05] Valeriy Kravchuk
Verified just as described with recent 5.1.46 from bzr on Linux:

openxs@suse:/home2/openxs/dbs/5.1> bin/mysql -uroot test
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.1.46-debug Source distribution

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> drop temporary table if exists t1;
Query OK, 0 rows affected, 1 warning (0.01 sec)

mysql> create temporary table t1
    -> (
    ->  id int unsigned,
    ->  name varchar(50),
    ->  primary key(id)
    -> );
Query OK, 0 rows affected (0.04 sec)

mysql>
mysql> insert into t1 set id=1, name='Test 1 (Has Amount)';
Query OK, 1 row affected (0.00 sec)

mysql> insert into t1 set id=2, name='Test 2 (No Amount)';
Query OK, 1 row affected (0.00 sec)

mysql> insert into t1 set id=3, name='Test 3 (No Amount)';
Query OK, 1 row affected (0.00 sec)

mysql> insert into t1 set id=4, name='Test 4 (Has Amount)';
Query OK, 1 row affected (0.00 sec)

mysql> insert into t1 set id=5, name='Test 5 (No Amount)';
Query OK, 1 row affected (0.00 sec)

mysql> insert into t1 set id=6, name='Test 6 (No Amount)';
Query OK, 1 row affected (0.00 sec)

mysql>
mysql> drop temporary table if exists t2;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> create temporary table t2
    -> (
    ->  id int unsigned,
    ->  t1_id int unsigned,
    ->  primary key(id),
    ->  index t1id_index(t1_id)
    -> );
Query OK, 0 rows affected (0.00 sec)

mysql>
mysql> insert into t2 set id=1, t1_id=1;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t2 set id=2, t1_id=1;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t2 set id=3, t1_id=1;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t2 set id=4, t1_id=1;
Query OK, 1 row affected (0.00 sec)

mysql>
mysql> insert into t2 set id=5, t1_id=4;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t2 set id=6, t1_id=4;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t2 set id=7, t1_id=4;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t2 set id=8, t1_id=4;
Query OK, 1 row affected (0.00 sec)

mysql>
mysql> drop temporary table if exists t3;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> create temporary table t3
    -> (
    ->  t1_id int unsigned,
    ->  amount decimal(18,2)
    -> );
Query OK, 0 rows affected (0.00 sec)

mysql>
mysql> insert into t3 set t1_id=1, amount=10.15;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t3 set t1_id=1, amount=10.25;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t3 set t1_id=1, amount=10.30;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t3 set t1_id=4, amount=10.01;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t3 set t1_id=4, amount=10.03;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t3 set t1_id=4, amount=10.09;
Query OK, 1 row affected (0.00 sec)

mysql>
mysql> (
    ->  select t1.id,
    ->         t1.name,
    ->         ifnull(
    ->          (select sum(amount)
    ->           from t3
    ->           where t3.t1_id=t1.id
    ->          ),0) as total
    ->  from t1
    ->  force index(primary)
    ->  left join t2 on t2.t1_id=t1.id
    ->  group by t1.id
    -> )
    -> union
    -> (select 1 as id,
    ->         'bug' as name,
    ->         0 as ytd_total
    ->  from dual
    -> );
+----+---------------------+-------+
| id | name                | total |
+----+---------------------+-------+
|  1 | Test 1 (Has Amount) | 30.70 |
|  2 | Test 2 (No Amount)  | 30.00 |
|  3 | Test 3 (No Amount)  | 30.00 |
|  4 | Test 4 (Has Amount) | 30.13 |
|  5 | Test 5 (No Amount)  | 30.00 |
|  6 | Test 6 (No Amount)  | 30.00 |
|  1 | bug                 |  0.00 |
+----+---------------------+-------+
7 rows in set (0.02 sec)
[26 Mar 2010 8:50] Gleb Shchepa
This bug is a regression of the fix for the bug #50843: Filesort used instead of clustered index led to performance degradation.

Note: that fix changes the execution plan in this way:

 id     select_type     table   type    possible_keys   key     key_len ref     rows    Extra
-1      PRIMARY t1      ALL     NULL    NULL    NULL    NULL    6       Using temporary; Using filesort
+1      PRIMARY t1      index   NULL    PRIMARY 4       NULL    3
 1      PRIMARY t2      ref     t1id_index      t1id_index      5       test.t1.id      2       Using index
 2      DEPENDENT SUBQUERY      t3      ALL     NULL    NULL    NULL    NULL    6       Using where
 3      UNION   NULL    NULL    NULL    NULL    NULL    NULL    NULL    No tables used
 NULL    UNION RESULT    <union1,3>      ALL     NULL    NULL    NULL    NULL    NULL