Bug #68871 EXPLAIN does not show real plan used for query with HAVING
Submitted: 5 Apr 2013 7:32 Modified: 27 Aug 2014 4:36
Reporter: Valeriy Kravchuk Email Updates:
Status: Can't repeat Impact on me:
None 
Category:MySQL Server: Optimizer Severity:S3 (Non-critical)
Version:5.1.58, 5.5.30, 5.6.10 OS:Any
Assigned to: CPU Architecture:Any
Tags: explain, having, limit, order by

[5 Apr 2013 7:32] Valeriy Kravchuk
Description:
For queries with HAVING clause EXPLAIN may not show real plan used (suggesting other one). This makes troubleshooting more complex.

Here is a simplified example (based on real life case where query execution time was very different, while EXPLAIN had not let to see why), on MySQL 5.5.30. (In reality it was Percona Server 5.1.58, but as one can check we have upstream EXPLAIN problem here even in 5.5.30 and 5.6.10, hence my report):

C:\Program Files\MySQL\MySQL Server 5.5\bin>mysql -uroot -proot -P3312 test
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 17
Server version: 5.5.30 MySQL Community Server (GPL)

Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

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

mysql> show create table t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `id1` bigint(20) NOT NULL,
  `id2` bigint(20) NOT NULL,
  `id3` bigint(20) NOT NULL,
  `c1` bigint(20) DEFAULT NULL,
  `c2` bigint(20) NOT NULL,
  `seqid` bigint(20) NOT NULL AUTO_INCREMENT,
  `time_stamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id1`,`id2`),
  UNIQUE KEY `ix_seq` (`seqid`),
  KEY `ix_ts` (`time_stamp`)
) ENGINE=InnoDB AUTO_INCREMENT=1782 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

mysql> select count(*) from t1 where time_stamp BETWEEN '2013-03-18 17:00:00' AN
D '2013-03-31 22:00:00';
+----------+
| count(*) |
+----------+
|      579 |
+----------+
1 row in set (0.00 sec)

mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
|     1024 |
+----------+
1 row in set (0.00 sec)

So, for this specific table and data (see dump attached), we have:

mysql> EXPLAIN
    -> SELECT * FROM t1 o WHERE o.id3 = 100
    -> AND o.time_stamp BETWEEN '2013-03-18 17:00:00' AND '2013-03-31 22:00:00'
    -> ORDER BY o.seqid limit 10;
+----+-------------+-------+-------+---------------+--------+---------+------+--
----+-------------+
| id | select_type | table | type  | possible_keys | key    | key_len | ref  | r
ows | Extra       |
+----+-------------+-------+-------+---------------+--------+---------+------+--
----+-------------+
|  1 | SIMPLE      | o     | index | ix_ts         | ix_seq | 8       | NULL |
 17 | Using where |
+----+-------------+-------+-------+---------------+--------+---------+------+--
----+-------------+
1 row in set (0.00 sec)

mysql> EXPLAIN
    -> SELECT * FROM t1 o WHERE o.id3 = 100
    -> AND o.time_stamp BETWEEN '2013-03-18 17:00:00' AND '2013-03-31 22:00:00'
    -> ORDER BY o.seqid+0 limit 10;
+----+-------------+-------+-------+---------------+-------+---------+------+---
---+-----------------------------+
| id | select_type | table | type  | possible_keys | key   | key_len | ref  | ro
ws | Extra                       |
+----+-------------+-------+-------+---------------+-------+---------+------+---
---+-----------------------------+
|  1 | SIMPLE      | o     | range | ix_ts         | ix_ts | 4       | NULL |  5
78 | Using where; Using filesort |
+----+-------------+-------+-------+---------------+-------+---------+------+---
---+-----------------------------+
1 row in set (0.00 sec)

that is, different plans for queries, and for a reason - o.seqid+0 in the query is used in purpose, to make server NOT use index on sequid column (that it likes to use very much because of PRDER By with small LIMIT of rows selected). 

Now, let's add HAVING c2 > 0 (this condition is actually true for all rows in the table), and we'll see plans are reported as the same now:

mysql> EXPLAIN
    -> SELECT * FROM t1 o WHERE o.id3 = 100
    -> AND o.time_stamp BETWEEN '2013-03-18 17:00:00' AND '2013-03-31 22:00:00'
    -> HAVING c2 > 0
    -> ORDER BY o.seqid limit 10;
+----+-------------+-------+-------+---------------+-------+---------+------+---
---+-----------------------------+
| id | select_type | table | type  | possible_keys | key   | key_len | ref  | ro
ws | Extra                       |
+----+-------------+-------+-------+---------------+-------+---------+------+---
---+-----------------------------+
|  1 | SIMPLE      | o     | range | ix_ts         | ix_ts | 4       | NULL |  5
78 | Using where; Using filesort |
+----+-------------+-------+-------+---------------+-------+---------+------+---
---+-----------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN
    -> SELECT * FROM t1 o WHERE o.id3 = 100
    -> AND o.time_stamp BETWEEN '2013-03-18 17:00:00' AND '2013-03-31 22:00:00'
    -> HAVING c2 > 0
    -> ORDER BY o.seqid+0 limit 10;
+----+-------------+-------+-------+---------------+-------+---------+------+---
---+-----------------------------+
| id | select_type | table | type  | possible_keys | key   | key_len | ref  | ro
ws | Extra                       |
+----+-------------+-------+-------+---------------+-------+---------+------+---
---+-----------------------------+
|  1 | SIMPLE      | o     | range | ix_ts         | ix_ts | 4       | NULL |  5
78 | Using where; Using filesort |
+----+-------------+-------+-------+---------------+-------+---------+------+---
---+-----------------------------+
1 row in set (0.00 sec)

But then let's check Handler_read% after execution of each query:

mysql> flush status;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM t1 o WHERE o.id3 = 100
    -> AND o.time_stamp BETWEEN '2013-03-18 17:00:00' AND '2013-03-31 22:00:00'
    -> HAVING c2 > 0
    -> ORDER BY o.seqid limit 10;
+------+------+-----+------+----+-------+---------------------+
| id1  | id2  | id3 | c1   | c2 | seqid | time_stamp          |
+------+------+-----+------+----+-------+---------------------+
| 4386 | 8408 | 100 | NULL | 10 |  1270 | 2013-03-18 19:00:00 |
...
| 8119 |  739 | 100 | NULL | 10 |  1279 | 2013-03-18 19:00:00 |
+------+------+-----+------+----+-------+---------------------+
10 rows in set (0.38 sec)

mysql> show status like 'Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 521   |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.00 sec)

mysql> flush status;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM t1 o WHERE o.id3 = 100
    -> AND o.time_stamp BETWEEN '2013-03-18 17:00:00' AND '2013-03-31 22:00:00'
    -> HAVING c2 > 0
    -> ORDER BY o.seqid+0 limit 10;
+------+------+-----+------+----+-------+---------------------+
| id1  | id2  | id3 | c1   | c2 | seqid | time_stamp          |
+------+------+-----+------+----+-------+---------------------+
| 4386 | 8408 | 100 | NULL | 10 |  1270 | 2013-03-18 19:00:00 |
...
| 8119 |  739 | 100 | NULL | 10 |  1279 | 2013-03-18 19:00:00 |
+------+------+-----+------+----+-------+---------------------+
10 rows in set (0.01 sec)

mysql> show status like 'Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 579   |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.00 sec)

Obviously different plans were used, in the first case we still had "index" scan on ix_seq. On larger and real life data sets this has bad performance consequences, and EXPLAIN with its misleading information makes troubleshooting more complex.

How to repeat:
Load the dump attached, then check EXPLAIN results and Handler_read% values for the following queries:

SELECT * FROM t1 o WHERE o.id3 = 100
AND o.time_stamp BETWEEN '2013-03-18 17:00:00' AND '2013-03-31 22:00:00'
HAVING c2 > 0
ORDER BY o.seqid limit 10;

SELECT * FROM t1 o WHERE o.id3 = 100
AND o.time_stamp BETWEEN '2013-03-18 17:00:00' AND '2013-03-31 22:00:00'
HAVING c2 > 0
ORDER BY o.seqid+0 limit 10;

Suggested fix:
Do something with EXPLAIN to show real plan used in this case (or, even better, make server use the plan EXPLAIN shows for the case with HAVING and +0 in ORDER BY :).
[5 Apr 2013 7:34] Valeriy Kravchuk
Dump of table data for the test case

Attachment: t1.sql (text/x-sql), 51.76 KiB.

[5 Apr 2013 14:59] MySQL Verification Team
This is strictly a bug in the EXPLAIN output and not a bug in the optimizer itself.

First of all, HAVING should not confuse anyone, since when query is not aggregating, HAVING expressions are simply merged with WHERE expressions. In this case this means just and only checking of the values of one column in the filtered result, regarding if all values of the column satisfy the expression or not.

This is a bug in the EXPLAIN only since, it is visible from the outputs, that ORDER BY on some ever column with ever-increasing values, still uses that index. Hence, this requires less reads then the query with ORDER BY on the expression. 

This fact is not exhibited in EXPLAIN itself. Also, the query with ORDER BY on column does not do filesort, only more reads by index, which can be seen in status variables.

Hence this small bug in the EXPLAIN statement.
[27 Aug 2014 4:36] Erlend Dahl
[25 Aug 2014 1:49] Haixiang Li

Test it in  5.7.5-m15 and 5.6.16, this bug could not reproduce.
MySQL use table scan(type is 'ALL') ,not use index scan.