1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00

MDEV-39868 Wrong result with a window fn over merged derived table column

Problem:
========
A query with a window function over a column of a merged derived table
returns an empty set when another table is joined on a condition over the
same column and is accessed with "Range checked for each record":

  SELECT AVG(subq.c2) OVER (), t2.c1
  FROM t1 LEFT JOIN (SELECT * FROM t3) AS subq ON t1.c1 = subq.c1
  STRAIGHT_JOIN t2 ON subq.c2 > t2.c1;

With derived_merge=on all the references to subq.c2 are
Item_direct_view_ref objects sharing one underlying Item_field, because
their ref pointers all point into the derived table's field_translation.
Item::split_sum_func2() calls real_item() and puts that shared Item_field
into the list of the window function's temporary table fields, so
create_tmp_field_from_item_field() sets its result_field to a column of
the temporary table.

Item_field::val_int() reads field, but Item_field::save_in_field() reads
result_field, so the two now return different values. The join condition
is evaluated through the same Item_field, and the runtime range analysis
in Field::get_mm_leaf_int() uses save_in_field_no_warnings(). It reads the
still empty temporary table column instead of the value of t3.c2, treats
the value as NULL, and builds a SEL_TREE::IMPOSSIBLE. Table t2 then
produces no rows.

Solution:
=========
Do not unwrap Item_direct_view_ref in Item::split_sum_func2(). The wrapper
is created per reference and is not shared, so the temporary table field
is attached to the wrapper alone and the conditions that refer to the same
view column keep reading the base table field.

Item_ref::create_tmp_field_ex() already creates the same temporary table
field for a view ref over a column, and change_to_use_tmp_fields() already
handles REF_ITEM, so no other change is needed. Ref access was never
affected: get_store_key() takes real_item()->field explicitly.
This commit is contained in:
bsrikanth-mariadb
2026-09-10 16:15:42 +05:30
parent 6fd5edbbe2
commit c0c4b5573f
3 changed files with 212 additions and 1 deletions

View File

@@ -222,5 +222,109 @@ id select_type table type possible_keys key key_len ref rows Extra
NULL UNION RESULT <union2,3> ALL NULL NULL NULL NULL NULL
drop table t;
#
# MDEV-39868: Wrong result (empty set) for a query with a window
# function over a column of a merged derived table
#
CREATE TABLE t1(c1 VARCHAR(100) NOT NULL, KEY t1_k1 (c1));
CREATE TABLE t2(c1 BOOLEAN, c2 INT NOT NULL, KEY t2_k1 (c1, c2));
CREATE TABLE t3(c1 VARCHAR(100) NOT NULL, c2 INT NOT NULL);
INSERT INTO t1 VALUES ('foo'), ('bar');
INSERT INTO t2 VALUES (FALSE, 10);
INSERT INTO t3 VALUES ('foo', 10), ('bar', 20);
# t2 is accessed with "Range checked for each record", and the value
# of subq.c2 used by the range optimizer must come from t3, not from
# the temporary table created for the window function.
EXPLAIN
SELECT AVG(subq.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1);
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 index t1_k1 t1_k1 102 NULL 2 Using index; Using temporary
1 SIMPLE t3 ALL NULL NULL NULL NULL 2 Using where; Using join buffer (flat, BNL join)
1 SIMPLE t2 ALL t2_k1 NULL NULL NULL 1 Range checked for each record (index map: 0x1)
SELECT AVG(subq.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1);
col1 col2
15.0000 0
15.0000 0
# the same with the derived table materialized
SET optimizer_switch='derived_merge=off';
SELECT AVG(subq.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1);
col1 col2
15.0000 0
15.0000 0
SET optimizer_switch='derived_merge=default';
# the same without a derived table
SELECT AVG(subq.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN t3 AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1);
col1 col2
15.0000 0
15.0000 0
# the same without the range check on t2
SELECT AVG(subq.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 IGNORE INDEX (t2_k1) ON (subq.c2 > t2.c1);
col1 col2
15.0000 0
15.0000 0
# the same via a view
CREATE VIEW v3 AS SELECT * FROM t3;
SELECT AVG(v3.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN v3
ON (t1.c1 = v3.c1)
STRAIGHT_JOIN t2 ON (v3.c2 > t2.c1);
col1 col2
15.0000 0
15.0000 0
DROP VIEW v3;
# an aggregate function with GROUP BY uses a temporary table too
EXPLAIN
SELECT AVG(subq.c2) as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1)
GROUP BY t2.c1;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 index t1_k1 t1_k1 102 NULL 2 Using index; Using temporary; Using filesort
1 SIMPLE t3 ALL NULL NULL NULL NULL 2 Using where; Using join buffer (flat, BNL join)
1 SIMPLE t2 index t2_k1 t2_k1 6 NULL 1 Using where; Using index; Using join buffer (incremental, BNL join)
SELECT AVG(subq.c2) as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1)
GROUP BY t2.c1;
col1 col2
15.0000 0
DROP TABLE t1, t2, t3;
#
# End of 10.11 tests
#

View File

@@ -175,6 +175,97 @@ explain update t set c=1 where exists (
from t order by x=avg(row_number() over t) over (order by 1));
drop table t;
--echo #
--echo # MDEV-39868: Wrong result (empty set) for a query with a window
--echo # function over a column of a merged derived table
--echo #
CREATE TABLE t1(c1 VARCHAR(100) NOT NULL, KEY t1_k1 (c1));
CREATE TABLE t2(c1 BOOLEAN, c2 INT NOT NULL, KEY t2_k1 (c1, c2));
CREATE TABLE t3(c1 VARCHAR(100) NOT NULL, c2 INT NOT NULL);
INSERT INTO t1 VALUES ('foo'), ('bar');
INSERT INTO t2 VALUES (FALSE, 10);
INSERT INTO t3 VALUES ('foo', 10), ('bar', 20);
--echo # t2 is accessed with "Range checked for each record", and the value
--echo # of subq.c2 used by the range optimizer must come from t3, not from
--echo # the temporary table created for the window function.
EXPLAIN
SELECT AVG(subq.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1);
SELECT AVG(subq.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1);
--echo # the same with the derived table materialized
SET optimizer_switch='derived_merge=off';
SELECT AVG(subq.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1);
SET optimizer_switch='derived_merge=default';
--echo # the same without a derived table
SELECT AVG(subq.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN t3 AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1);
--echo # the same without the range check on t2
SELECT AVG(subq.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 IGNORE INDEX (t2_k1) ON (subq.c2 > t2.c1);
--echo # the same via a view
CREATE VIEW v3 AS SELECT * FROM t3;
SELECT AVG(v3.c2) OVER () as col1, t2.c1 as col2
FROM t1
JOIN v3
ON (t1.c1 = v3.c1)
STRAIGHT_JOIN t2 ON (v3.c2 > t2.c1);
DROP VIEW v3;
--echo # an aggregate function with GROUP BY uses a temporary table too
EXPLAIN
SELECT AVG(subq.c2) as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1)
GROUP BY t2.c1;
SELECT AVG(subq.c2) as col1, t2.c1 as col2
FROM t1
JOIN (
SELECT * FROM t3
) AS subq
ON (t1.c1 = subq.c1)
STRAIGHT_JOIN t2 ON (subq.c2 > t2.c1)
GROUP BY t2.c1;
DROP TABLE t1, t2, t3;
--echo #
--echo # End of 10.11 tests
--echo #

View File

@@ -2393,8 +2393,24 @@ void Item::split_sum_func2(THD *thd, Ref_ptr_array ref_pointer_array,
If this is an item_ref, get the original item
This is a safety measure if this is called for things that is
already a reference.
Item_direct_view_ref is an exception: the item it refers to is shared
between all the references to the same column of a merged derived table
or view, including the references that are left in the WHERE/ON
conditions of the query. Putting that shared item into the list of the
temporary table fields would set its result_field to a field of the
temporary table, and Item_field::save_in_field() reads the value from
result_field. As a result, a condition that refers to the same view
column would read the value from the (not yet filled) temporary table
when it is evaluated at execution time, e.g. by the range optimizer
doing "Range checked for each record" (MDEV-39868).
Keep the Item_direct_view_ref wrapper in this case: the wrapper itself
is not shared, so the temporary table field is attached to it only.
*/
Item *real_itm= real_item();
Item *real_itm= (type() == REF_ITEM &&
((Item_ref *) this)->ref_type() == Item_ref::VIEW_REF) ?
this : real_item();
//Item *real_itm= real_item();
ref_pointer_array[el]= real_itm;
if (type() == WINDOW_FUNC_ITEM)
{