Bug #121154 CTE derived merge incorrectly preserves a NULL-complemented LEFT JOIN row rejected by the WHERE predicate
Description:
MySQL returns a row that must be filtered according to SQL three-valued logic when a CTE containing a `LEFT JOIN` is merged into the outer query.
The right side of the `LEFT JOIN` is empty, so the join produces a NULL-complemented row. The outer predicate is:
```sql
a <> 'sample'
```
For the NULL-complemented row, `a` is `NULL`. The predicate therefore evaluates to `UNKNOWN`, and the `WHERE` clause must reject the row.
With the default optimizer settings, MySQL incorrectly returns:
```text
NULL | NULL
```
When `derived_merge` is disabled, the same query correctly returns an empty result. The result therefore changes solely because of an optimizer transformation.
Expected result:
```text
Empty set
```
Actual result with `derived_merge=on`:
```text
+------+------+
| a | b |
+------+------+
| NULL | NULL |
+------+------+
```
The execution plans also demonstrate the behavioral difference.
With `derived_merge=on`:
```text
Nested loop left join
-> Rows fetched before execution
-> Zero rows (Impossible filter)
```
With `derived_merge=off`:
```text
Zero rows (no matching row in const table)
```
This is a logic bug because merging a derived table or CTE must not change the query result, and a null-rejecting `WHERE` predicate must not retain a NULL-complemented outer-join row.
How to repeat:
Run the following query using the default optimizer settings:
```sql
SET optimizer_switch = 'default';
WITH cut AS (
SELECT r.a, r.b
FROM (SELECT 1 AS x) AS l
LEFT JOIN (
SELECT VERSION() AS a, NULL AS b
FROM (SELECT 1 AS z) AS e
WHERE FALSE
) AS r ON TRUE
)
SELECT *
FROM cut
WHERE a <> 'sample';
```
Actual result:
```text
+------+------+
| a | b |
+------+------+
| NULL | NULL |
+------+------+
```
Disable derived-table merging and execute the same query:
```sql
SET optimizer_switch = 'derived_merge=off';
WITH cut AS (
SELECT r.a, r.b
FROM (SELECT 1 AS x) AS l
LEFT JOIN (
SELECT VERSION() AS a, NULL AS b
FROM (SELECT 1 AS z) AS e
WHERE FALSE
) AS r ON TRUE
)
SELECT *
FROM cut
WHERE a <> 'sample';
```
Expected and actual result with merging disabled:
```text
Empty set
```
The predicate can also be evaluated directly to confirm the expected SQL semantics:
```sql
SELECT NULL <> 'sample';
```
Result:
```text
NULL
```
The issue was originally detected in VECT pair 37. In that query, the unmodified query returned 2 rows, while three independently generated CTE cuts returned 9,992 rows. The affected candidates were two `join` candidates and one `atomic_predicate` candidate. All three differences disappeared when `derived_merge` was disabled.
Portions of this website are copyright © 2001, 2002 The PHP Group
Page generated in 0.051 sec. using MySQL 8.4.7-u6-cloud
Timestamp references displayed by the system are UTC.
Content reproduced on this site is the property of the
respective copyright holders.
It is not reviewed in advance
by Oracle and does not necessarily represent the opinion of
Oracle or any other party.
Description: MySQL returns a row that must be filtered according to SQL three-valued logic when a CTE containing a `LEFT JOIN` is merged into the outer query. The right side of the `LEFT JOIN` is empty, so the join produces a NULL-complemented row. The outer predicate is: ```sql a <> 'sample' ``` For the NULL-complemented row, `a` is `NULL`. The predicate therefore evaluates to `UNKNOWN`, and the `WHERE` clause must reject the row. With the default optimizer settings, MySQL incorrectly returns: ```text NULL | NULL ``` When `derived_merge` is disabled, the same query correctly returns an empty result. The result therefore changes solely because of an optimizer transformation. Expected result: ```text Empty set ``` Actual result with `derived_merge=on`: ```text +------+------+ | a | b | +------+------+ | NULL | NULL | +------+------+ ``` The execution plans also demonstrate the behavioral difference. With `derived_merge=on`: ```text Nested loop left join -> Rows fetched before execution -> Zero rows (Impossible filter) ``` With `derived_merge=off`: ```text Zero rows (no matching row in const table) ``` This is a logic bug because merging a derived table or CTE must not change the query result, and a null-rejecting `WHERE` predicate must not retain a NULL-complemented outer-join row. How to repeat: Run the following query using the default optimizer settings: ```sql SET optimizer_switch = 'default'; WITH cut AS ( SELECT r.a, r.b FROM (SELECT 1 AS x) AS l LEFT JOIN ( SELECT VERSION() AS a, NULL AS b FROM (SELECT 1 AS z) AS e WHERE FALSE ) AS r ON TRUE ) SELECT * FROM cut WHERE a <> 'sample'; ``` Actual result: ```text +------+------+ | a | b | +------+------+ | NULL | NULL | +------+------+ ``` Disable derived-table merging and execute the same query: ```sql SET optimizer_switch = 'derived_merge=off'; WITH cut AS ( SELECT r.a, r.b FROM (SELECT 1 AS x) AS l LEFT JOIN ( SELECT VERSION() AS a, NULL AS b FROM (SELECT 1 AS z) AS e WHERE FALSE ) AS r ON TRUE ) SELECT * FROM cut WHERE a <> 'sample'; ``` Expected and actual result with merging disabled: ```text Empty set ``` The predicate can also be evaluated directly to confirm the expected SQL semantics: ```sql SELECT NULL <> 'sample'; ``` Result: ```text NULL ``` The issue was originally detected in VECT pair 37. In that query, the unmodified query returned 2 rows, while three independently generated CTE cuts returned 9,992 rows. The affected candidates were two `join` candidates and one `atomic_predicate` candidate. All three differences disappeared when `derived_merge` was disabled.