Bug #113513 Join reordering regression and inconsistent behavior for SQL involving LEFT JOIN
Submitted: 26 Dec 2023 12:12 Modified: 27 Dec 2023 5:19
Reporter: Hope Lee (OCA) Email Updates:
Status: Verified Impact on me:
None 
Category:MySQL Server: Optimizer Severity:S3 (Non-critical)
Version:8.0.33 OS:Any
Assigned to: CPU Architecture:Any

[26 Dec 2023 12:12] Hope Lee
Description:
For SQL like:

SELECT * FROM (
  SELECT t_o.a, alias1.b as a1_b, alias2.b as a2_b, alias3.b as a3_b 
  FROM t_o 
  LEFT JOIN t_i as alias1 ON t_o.a = alias1.b
  LEFT JOIN t_i as alias2 ON t_o.a = alias2.b
  LEFT JOIN t_i as alias3 ON t_o.a = alias3.b
  WHERE t_o.a > 2
) de;

I think the join order of tables alias1, alias2, and alias3 is arbitrary, as long as table t_o is joined first. In MySQL 8.0.13, the join order can be changed freely as expected. But in MySQL 8.0.33, it cannot, which I think is a regression for the optimizer.

How to repeat:
CREATE TABLE t_o (a INT);
CREATE TABLE t_i (b INT);

INSERT INTO t_o VALUES (3), (9), (10);
INSERT INTO t_i VALUES (29), (8), (5);

Behavior in MySQL 8.0.13:
mysql-8.0.13 > EXPLAIN SELECT * FROM (
	SELECT t_o.a, alias1.b as a1_b, alias2.b as a2_b, alias3.b as a3_b 
	FROM t_o 
	LEFT JOIN t_i as alias1 ON t_o.a = alias1.b
	LEFT JOIN t_i as alias2 ON t_o.a = alias2.b
	LEFT JOIN t_i as alias3 ON t_o.a = alias3.b
	WHERE t_o.a > 2
) de;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | t_o    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where                                        |
|  1 | SIMPLE      | alias1 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | alias2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | alias3 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+

We can change the join order freely with optimizer hints:
mysql-8.0.13 > EXPLAIN SELECT /*+ JOIN_PREFIX(t_o, alias2) */ * FROM (
	SELECT t_o.a, alias1.b as a1_b, alias2.b as a2_b, alias3.b as a3_b 
	FROM t_o 
	LEFT JOIN t_i as alias1 ON t_o.a = alias1.b
	LEFT JOIN t_i as alias2 ON t_o.a = alias2.b
	LEFT JOIN t_i as alias3 ON t_o.a = alias3.b
	WHERE t_o.a > 2
) de;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | t_o    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where                                        |
|  1 | SIMPLE      | alias2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | alias1 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | alias3 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+

mysql-8.0.13 > EXPLAIN SELECT /*+ JOIN_PREFIX(t_o, alias3) */ * FROM (
	SELECT t_o.a, alias1.b as a1_b, alias2.b as a2_b, alias3.b as a3_b 
	FROM t_o 
	LEFT JOIN t_i as alias1 ON t_o.a = alias1.b
	LEFT JOIN t_i as alias2 ON t_o.a = alias2.b
	LEFT JOIN t_i as alias3 ON t_o.a = alias3.b
	WHERE t_o.a > 2
) de;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | t_o    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where                                        |
|  1 | SIMPLE      | alias3 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | alias1 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | alias2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+

Behavior in MySQL 8.0.33:
mysql-8.0.33 > EXPLAIN SELECT * FROM (
	SELECT t_o.a, alias1.b as a1_b, alias2.b as a2_b, alias3.b as a3_b 
	FROM t_o 
	LEFT JOIN t_i as alias1 ON t_o.a = alias1.b
	LEFT JOIN t_i as alias2 ON t_o.a = alias2.b
	LEFT JOIN t_i as alias3 ON t_o.a = alias3.b
	WHERE t_o.a > 2
) de;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                      |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t_o    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where                                |
|  1 | SIMPLE      | alias1 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (hash join) |
|  1 | SIMPLE      | alias2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (hash join) |
|  1 | SIMPLE      | alias3 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (hash join) |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+

But we cannot change the join order any more with optimizer hints:
mysql-8.0.33 > EXPLAIN SELECT /*+ JOIN_PREFIX(t_o, alias2) */ * FROM (
	SELECT t_o.a, alias1.b as a1_b, alias2.b as a2_b, alias3.b as a3_b 
	FROM t_o 
	LEFT JOIN t_i as alias1 ON t_o.a = alias1.b
	LEFT JOIN t_i as alias2 ON t_o.a = alias2.b
	LEFT JOIN t_i as alias3 ON t_o.a = alias3.b
	WHERE t_o.a > 2
) de;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                      |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t_o    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where                                |
|  1 | SIMPLE      | alias1 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (hash join) |
|  1 | SIMPLE      | alias2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (hash join) |
|  1 | SIMPLE      | alias3 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (hash join) |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+

mysql-8.0.33 > EXPLAIN SELECT /*+ JOIN_PREFIX(t_o, alias3) */ * FROM (
	SELECT t_o.a, alias1.b as a1_b, alias2.b as a2_b, alias3.b as a3_b 
	FROM t_o 
	LEFT JOIN t_i as alias1 ON t_o.a = alias1.b
	LEFT JOIN t_i as alias2 ON t_o.a = alias2.b
	LEFT JOIN t_i as alias3 ON t_o.a = alias3.b
	WHERE t_o.a > 2
) de;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                      |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t_o    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where                                |
|  1 | SIMPLE      | alias1 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (hash join) |
|  1 | SIMPLE      | alias2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (hash join) |
|  1 | SIMPLE      | alias3 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (hash join) |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+

What's more, there is a strange thing in MySQL 8.0.13, which is we also cannot change the join order if we remove the outermost derived table:

mysql-8.0.13 > EXPLAIN SELECT /*+ JOIN_PREFIX(t_o,alias2) */ * FROM t_o 
LEFT JOIN t_i as alias1 ON t_o.a = alias1.b
LEFT JOIN t_i as alias2 ON t_o.a = alias2.b
LEFT JOIN t_i as alias3 ON t_o.a = alias3.b
WHERE t_o.a > 2;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | t_o    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where                                        |
|  1 | SIMPLE      | alias1 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | alias2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | alias3 | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+

Suggested fix:
This issue is due to incorrect and inconsistent settings on Table_ref::dep_tables in Query_block::simplify_joins. 

Actually, tables alias1, alias2, and alias3 only depend on the table t_o and have no dependencies on each other. The optimizer could have had a larger join order search space.
[27 Dec 2023 5:19] MySQL Verification Team
Hello Hope Lee,

Thank you for the report and feedback.

regards,
Umesh