Bug #118493 Subpartitions can not be pruned by conditions derived from equal propagation
Submitted: 20 Jun 6:54 Modified: 20 Jun 10:38
Reporter: NANLONG YU Email Updates:
Status: Verified Impact on me:
None 
Category:MySQL Server: Partitions Severity:S3 (Non-critical)
Version:8.0, 8.0.42 OS:Any
Assigned to: CPU Architecture:Any

[20 Jun 6:54] NANLONG YU
Description:
In the following case, there are two tables named t1 and t2:

MySQL [test]> show create table t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `a` int DEFAULT NULL,
  `b` int DEFAULT NULL,
  `c` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3
/*!50100 PARTITION BY RANGE (`b`)
SUBPARTITION BY HASH (`c`)
SUBPARTITIONS 3
(PARTITION p0 VALUES LESS THAN (10) ENGINE = InnoDB,
 PARTITION p1 VALUES LESS THAN (20) ENGINE = InnoDB) */
1 row in set (0.01 sec)

MySQL [test]> show create table t2\G
*************************** 1. row ***************************
       Table: t2
Create Table: CREATE TABLE `t2` (
  `a` int DEFAULT NULL,
  `b` int DEFAULT NULL,
  `c` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3
/*!50100 PARTITION BY RANGE (`b`)
SUBPARTITION BY HASH (`c`)
SUBPARTITIONS 3
(PARTITION p0 VALUES LESS THAN (10) ENGINE = InnoDB,
 PARTITION p1 VALUES LESS THAN (20) ENGINE = InnoDB) */
1 row in set (0.00 sec)

And there are 7 rows in each table:

MySQL [test]> select * from t1;
+------+------+------+
| a    | b    | c    |
+------+------+------+
|    3 |    1 |    3 |
|    6 |    1 |    6 |
|    1 |    1 |    1 |
|    4 |    1 |    4 |
|    7 |    1 |    7 |
|    2 |    1 |    2 |
|    5 |    1 |    5 |
+------+------+------+
7 rows in set (0.00 sec)

MySQL [test]> select * from t2;
+------+------+------+
| a    | b    | c    |
+------+------+------+
|    3 |    1 |    3 |
|    6 |    1 |    6 |
|    1 |    1 |    1 |
|    4 |    1 |    4 |
|    7 |    1 |    7 |
|    2 |    1 |    2 |
|    5 |    1 |    5 |
+------+------+------+
7 rows in set (0.00 sec)

And the plan of the following SQL statement is:

MySQL [test]> explain select * from t1 left join t2 on t1.b = t2.b and t1.c = t2.c where t1.b = 1 and t2.b = 1 and t1.c = 1;
+----+-------------+-------+----------------------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table | partitions                 | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                      |
+----+-------------+-------+----------------------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t1    | p0_p0sp1                   | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where                                |
|  1 | SIMPLE      | t2    | p0_p0sp0,p0_p0sp1,p0_p0sp2 | ALL  | NULL          | NULL | NULL    | NULL |    7 |    14.29 | Using where; Using join buffer (hash join) |
+----+-------------+-------+----------------------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.01 sec)

MySQL [test]> show warnings;
+-------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                                                                                                                       |
+-------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select `test`.`t1`.`a` AS `a`,`test`.`t1`.`b` AS `b`,`test`.`t1`.`c` AS `c`,`test`.`t2`.`a` AS `a`,`test`.`t2`.`b` AS `b`,`test`.`t2`.`c` AS `c` from `test`.`t1` join `test`.`t2` where ((`test`.`t1`.`c` = 1) and (`test`.`t2`.`c` = 1) and (`test`.`t1`.`b` = 1) and (`test`.`t2`.`b` = 1)) |
+-------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

As indicated by the warning, the predicate (`test`.`t2`.`c` = 1) was added to the original SQL statement. This predicate is inferred by equal propagation from (`test`.`t1`.`c` = 1) and t1.c = t2.c .

However, the EXPLAIN result indicates that the subpartitions of t2 were not pruned by (`test`.`t2`.`c` = 1), which is unexpected. After all, t1 and t2 share the same conditions (b=1 and c=1) and the same partitioning method. Yet, the EXPLAIN output shows that t2 needs to access three subpartitions (p0_p0sp0, p0_p0sp1, p0_p0sp2), whereas t1 only needs to access one subpartition (p0_p0sp1).

In the code, the prune_partitions function is invoked twice.

The first invocation occurs during the prepare phase (specifically within Sql_cmd_dml::prepare->Sql_cmd_select::prepare_inner->SELECT_LEX::prepare->SELECT_LEX::apply_local_transforms->SELECT_LEX::prune_table_partitions_recursively->SELECT_LEX::prune_table_partitions), before build_equal_items is executed.

The second invocation takes place in optimize_phase1 (via JOIN::optimize_phase1->JOIN::prune_table_partitions), after build_equal_items has completed.

During the first call to prune_partitions, the first-level partitions of table t2 are pruned based on the WHERE clause condition t2.b = 1. Consequently, part_info->is_pruning_completed is set to true.

However, in the second call to prune_partitions, even if build_equal_items has subsequently deduced t2.c = 1, the secondary (second-level) partitions cannot be further pruned because part_info->is_pruning_completed is already set to true.

```
  /*
    If the prepare stage already have completed pruning successfully,
    it is no use of running prune_partitions() again on the same condition.
    Since it will not be able to prune anything more than the previous call
    from the prepare step.
  */
  if (part_info && part_info->is_pruning_completed) return false;
```

```
  /*
    Decide if the current pruning attempt is the final one.

    During the prepare phase, before locking, subqueries and stored programs
    are not evaluated. So we need to run prune_partitions() a second time in
    the optimize phase to prune partitions for reading, when subqueries and
    stored programs may be evaluated.

    The upcoming pruning attempt will be the final one when:
    - condition is constant, or
    - condition may vary for every row (so there is nothing to prune) or
    - evaluation is in execution phase.
  */
  if (pprune_cond->const_item() || !pprune_cond->const_for_execution() ||
      thd->lex->is_query_tables_locked())
    part_info->is_pruning_completed = true;
```

How to repeat:
drop table if exists t1;

create table t1 (a int, b int, c int) partition by range (b) subpartition by hash (c) subpartitions 3 (partition p0 values less than(10), partition p1 values less than(20));

insert into t1 values(1,1,1),(2,1,2),(3,1,3),(4,1,4),(5,1,5),(6,1,6),(7,1,7);

drop table if exists t2;

create table t2 (a int, b int, c int) partition by range (b) subpartition by hash (c) subpartitions 3 (partition p0 values less than(10), partition p1 values less than(20));

insert into t2 values(1,1,1),(2,1,2),(3,1,3),(4,1,4),(5,1,5),(6,1,6),(7,1,7);

explain select * from t1 left join t2 on t1.b = t2.b and t1.c = t2.c where t1.b = 1 and t2.b = 1 and t1.c = 1;
[20 Jun 10:38] MySQL Verification Team
Hello NANLONG YU,

Thank you for the report and feedback.

regards,
Umesh