From b9c36d42ded7a548a9f815208902eb65aad9b473 Mon Sep 17 00:00:00 2001 From: Dave Gosselin Date: Thu, 7 May 2026 08:49:06 -0400 Subject: [PATCH] MDEV-39323: CROSS JOIN query returns wrong result with NOT BETWEEN For 1 NOT BETWEEN c0 AND c2 with c0 float and c2 int, the range optimizer was building a SEL_TREE for only c0 > 1 and dropping the c2 < 1 disjunct. can_optimize_range_const rejected c2 because of a type mismatch. The case from the ticket returned no rows (instead of the expected six rows) because the index range scan on c0 fetched only matches for c0 > 1 but then they were discarded by the t1.c1 = t1.c2 predicate. Plan before the fix (range scan on c0, key_len 5): +----+-------+-------+------+---------+------+ | id | table | type | key | key_len | rows | +----+-------+-------+------+---------+------+ | 1 | t1 | range | c0 | 5 | 1 | | 1 | t2 | index | c0 | 6 | 3 | +----+-------+-------+------+---------+------+ Plan after the fix (Item_func_between::get_mm_tree returns no usable range tree, so the optimizer falls back to using the covering index scan on i0): +----+-------+-------+------+---------+------+ | id | table | type | key | key_len | rows | +----+-------+-------+------+---------+------+ | 1 | t2 | index | c0 | 6 | 6 | | 1 | t1 | index | i0 | 14 | 8 | +----+-------+-------+------+---------+------+ In Item_func_between::get_mm_tree, when can_optimize_range_const fails for a range bound argument and the predicate is negated, we now abandon the whole tree instead of continuing the loop. The new code mirrors the existing guard in the same function for non FIELD_ITEM arguments. The bug was introduced by MDEV-36235 which added can_optimize_range_const without considering the negated path. While we could fine tune the rejection behavior in can_optimize_range_const, I think that should be a separate patch as it requires careful consideration for each pair of types that could be supplied to [NOT] BETWEEN and that's outside the scope of this ticket (which is intended to fix the wrong result issue). This behavior matches MySQL 9. --- mysql-test/main/range.result | 47 ++++++++++++++++++++++++++++ mysql-test/main/range.test | 35 +++++++++++++++++++++ mysql-test/main/range_mrr_icp.result | 47 ++++++++++++++++++++++++++++ sql/opt_range.cc | 22 +++++++++++++ 4 files changed, 151 insertions(+) diff --git a/mysql-test/main/range.result b/mysql-test/main/range.result index 6943a295acb46..a7796ea4b668a 100644 --- a/mysql-test/main/range.result +++ b/mysql-test/main/range.result @@ -3755,6 +3755,53 @@ id key1 3 2 drop table t1; # +# MDEV-39323: CROSS JOIN query returns wrong result with NOT BETWEEN +# +CREATE TABLE t1 ( +c0 float DEFAULT NULL, +c1 mediumint DEFAULT NULL, +c2 int DEFAULT NULL, +UNIQUE KEY c0 (c0), +UNIQUE KEY c2 (c2), +KEY i0 (c2, c1, c0), +KEY i1 (c2), +KEY i2 (c1) +); +INSERT INTO t1 VALUES +(567621000, -8388608, NULL), +(0.363988, 0, -1643936528), +(-422810000, 0, NULL), +(0, 0, 0), +(NULL, 0, NULL), +(NULL, 8388607, NULL), +(NULL, -8388608, NULL); +CREATE TABLE t2 (c0 decimal(10,0) unsigned DEFAULT NULL); +INSERT INTO t2 VALUES (NULL),(NULL),(NULL),(NULL),(NULL),(NULL); +SELECT t1.c0, t1.c1, t1.c2 FROM t2 CROSS JOIN t1 +WHERE NOT (1 BETWEEN t1.c0 AND t1.c2) AND t1.c1 = t1.c2; +c0 c1 c2 +0 0 0 +0 0 0 +0 0 0 +0 0 0 +0 0 0 +0 0 0 +SELECT t1.c0, t1.c1, t1.c2 FROM t1 +WHERE NOT (1 BETWEEN t1.c0 AND t1.c2) AND t1.c1 = t1.c2; +c0 c1 c2 +0 0 0 +SELECT c0, c2 FROM t1 WHERE NOT (1 BETWEEN c0 AND c2); +c0 c2 +0 0 +0.363988 -1643936528 +567621000 NULL +SELECT c0, c2 FROM t1 WHERE 1 NOT BETWEEN c0 AND c2; +c0 c2 +0 0 +0.363988 -1643936528 +567621000 NULL +DROP TABLE t1, t2; +# # End of 10.11 tests # set global innodb_stats_persistent= @innodb_stats_persistent_save; diff --git a/mysql-test/main/range.test b/mysql-test/main/range.test index a77cef34e4c19..3822353ff119d 100644 --- a/mysql-test/main/range.test +++ b/mysql-test/main/range.test @@ -2556,6 +2556,41 @@ evalp $query; drop table t1; +--echo # +--echo # MDEV-39323: CROSS JOIN query returns wrong result with NOT BETWEEN +--echo # +CREATE TABLE t1 ( + c0 float DEFAULT NULL, + c1 mediumint DEFAULT NULL, + c2 int DEFAULT NULL, + UNIQUE KEY c0 (c0), + UNIQUE KEY c2 (c2), + KEY i0 (c2, c1, c0), + KEY i1 (c2), + KEY i2 (c1) +); +INSERT INTO t1 VALUES + (567621000, -8388608, NULL), + (0.363988, 0, -1643936528), + (-422810000, 0, NULL), + (0, 0, 0), + (NULL, 0, NULL), + (NULL, 8388607, NULL), + (NULL, -8388608, NULL); +CREATE TABLE t2 (c0 decimal(10,0) unsigned DEFAULT NULL); +INSERT INTO t2 VALUES (NULL),(NULL),(NULL),(NULL),(NULL),(NULL); + +SELECT t1.c0, t1.c1, t1.c2 FROM t2 CROSS JOIN t1 + WHERE NOT (1 BETWEEN t1.c0 AND t1.c2) AND t1.c1 = t1.c2; +SELECT t1.c0, t1.c1, t1.c2 FROM t1 + WHERE NOT (1 BETWEEN t1.c0 AND t1.c2) AND t1.c1 = t1.c2; +--sorted_result +SELECT c0, c2 FROM t1 WHERE NOT (1 BETWEEN c0 AND c2); +--sorted_result +SELECT c0, c2 FROM t1 WHERE 1 NOT BETWEEN c0 AND c2; + +DROP TABLE t1, t2; + --echo # --echo # End of 10.11 tests --echo # diff --git a/mysql-test/main/range_mrr_icp.result b/mysql-test/main/range_mrr_icp.result index 7a5a1405c7375..a6c171c291db9 100644 --- a/mysql-test/main/range_mrr_icp.result +++ b/mysql-test/main/range_mrr_icp.result @@ -3744,6 +3744,53 @@ id key1 3 2 drop table t1; # +# MDEV-39323: CROSS JOIN query returns wrong result with NOT BETWEEN +# +CREATE TABLE t1 ( +c0 float DEFAULT NULL, +c1 mediumint DEFAULT NULL, +c2 int DEFAULT NULL, +UNIQUE KEY c0 (c0), +UNIQUE KEY c2 (c2), +KEY i0 (c2, c1, c0), +KEY i1 (c2), +KEY i2 (c1) +); +INSERT INTO t1 VALUES +(567621000, -8388608, NULL), +(0.363988, 0, -1643936528), +(-422810000, 0, NULL), +(0, 0, 0), +(NULL, 0, NULL), +(NULL, 8388607, NULL), +(NULL, -8388608, NULL); +CREATE TABLE t2 (c0 decimal(10,0) unsigned DEFAULT NULL); +INSERT INTO t2 VALUES (NULL),(NULL),(NULL),(NULL),(NULL),(NULL); +SELECT t1.c0, t1.c1, t1.c2 FROM t2 CROSS JOIN t1 +WHERE NOT (1 BETWEEN t1.c0 AND t1.c2) AND t1.c1 = t1.c2; +c0 c1 c2 +0 0 0 +0 0 0 +0 0 0 +0 0 0 +0 0 0 +0 0 0 +SELECT t1.c0, t1.c1, t1.c2 FROM t1 +WHERE NOT (1 BETWEEN t1.c0 AND t1.c2) AND t1.c1 = t1.c2; +c0 c1 c2 +0 0 0 +SELECT c0, c2 FROM t1 WHERE NOT (1 BETWEEN c0 AND c2); +c0 c2 +0 0 +0.363988 -1643936528 +567621000 NULL +SELECT c0, c2 FROM t1 WHERE 1 NOT BETWEEN c0 AND c2; +c0 c2 +0 0 +0.363988 -1643936528 +567621000 NULL +DROP TABLE t1, t2; +# # End of 10.11 tests # set global innodb_stats_persistent= @innodb_stats_persistent_save; diff --git a/sql/opt_range.cc b/sql/opt_range.cc index b042445a7aa1a..8fb04d913e3cd 100644 --- a/sql/opt_range.cc +++ b/sql/opt_range.cc @@ -8621,6 +8621,9 @@ SEL_TREE *Item::get_mm_tree(RANGE_OPT_PARAM *param, Item **cond_ptr) } +/* + Disallow range creation when BETWEEN arguments' types don't match. +*/ bool Item_func_between::can_optimize_range_const(Item_field *field_item) const { @@ -8660,7 +8663,26 @@ Item_func_between::get_mm_tree(RANGE_OPT_PARAM *param, Item **cond_ptr) { Item_field *field_item= (Item_field*) (arguments()[i]->real_item()); if (!can_optimize_range_const(field_item)) + { + /* + For NOT BETWEEN the resulting tree is the OR of one tree per + NOT BETWEEN argument. Skipping a disjunct here is wrong + because the remaining tree wouldn't scan all rows that satisfy + the NOT BETWEEN. So bail here to avoid building a bad range. + */ + if (negated) + { + tree= nullptr; + break; + } + + /* + For BETWEEN, dropping a conjunct is fine because the + remaining tree is a superset of matching rows and the WHERE + clause will filter any extra rows out. + */ continue; + } SEL_TREE *tmp= get_full_func_mm_tree(param, field_item, (Item*)(intptr) i); if (negated)