From bf37c4170e7c7ea748993bca6fc5df681215a50b Mon Sep 17 00:00:00 2001 From: Dave Gosselin Date: Wed, 28 Oct 2026 09:28:08 -0400 Subject: [PATCH] MDEV-40779: Wrong result for DISTINCT with HAVING, ROLLUP A query with DISTINCT over GROUP BY now reads AVG, STD and VARIANCE values from the DISTINCT temp table, and its ROLLUP rows pass HAVING while keeping NULL from the columns that they roll-up. The following scenario now returns correct results of -1.0000: CREATE TABLE t1 (a INT); INSERT INTO t1 VALUES (1),(2),(3); SELECT DISTINCT -AVG(a) FROM t1 GROUP BY a HAVING AVG(a) < 2; Only the group with average 1 passes HAVING and goes into the DISTINCT temp table. Duplicate removal checks HAVING again on that row, after all three groups have been read. The row now stores its own average, and the second check reads 1 from it. Before, the check read 3, the average of the last group, and deleted the row. This scenario now returns 2, 2: SELECT DISTINCT a, a+0 FROM t1 GROUP BY a WITH ROLLUP HAVING a=2; The ROLLUP total row has a=NULL and fails HAVING. For this query, HAVING is on the JOIN_TAB of the grouping table while JOIN::having is NULL. rollup_write_data considers JOIN_TAB::having if available, otherwise it falls back to JOIN::having which was set during JOIN::prepare. Without HAVING, SELECT DISTINCT a+0 over the same groups returns 1, 2, 3 and NULL. The ROLLUP row stores NULL for a even though a is not in the select list, and a+0 is computed from that stored a. Assisted-By: Claude Opus 5.5 --- mysql-test/main/having.result | 55 +++++++++++++++++++++++++++++++++++ mysql-test/main/having.test | 49 +++++++++++++++++++++++++++++++ mysql-test/main/olap.result | 37 +++++++++++++++++++++++ mysql-test/main/olap.test | 33 +++++++++++++++++++++ sql/item.h | 6 ++-- sql/item_sum.h | 44 ++++++++++++++++++++++++++-- sql/sql_select.cc | 26 ++++++++++++----- sql/sql_select.h | 3 +- 8 files changed, 238 insertions(+), 15 deletions(-) diff --git a/mysql-test/main/having.result b/mysql-test/main/having.result index fa86bf0d35606..065adb5381b0f 100644 --- a/mysql-test/main/having.result +++ b/mysql-test/main/having.result @@ -1018,6 +1018,61 @@ DROP TABLE t1; DROP VIEW v1; SET optimizer_trace= @save_optimizer_trace; # +# MDEV-40779 DISTINCT with HAVING on AVG() returns no rows when +# the last group fails HAVING +# +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1),(2),(3); +SELECT DISTINCT -AVG(a) FROM t1 GROUP BY a HAVING AVG(a) < 2; +-AVG(a) +-1.0000 +SELECT DISTINCT -AVG(a), AVG(a) FROM t1 GROUP BY a HAVING AVG(a) < 3; +-AVG(a) AVG(a) +-1.0000 1.0000 +-2.0000 2.0000 +SELECT DISTINCT -AVG(a) FROM t1 GROUP BY a ORDER BY AVG(a) DESC; +-AVG(a) +-3.0000 +-2.0000 +-1.0000 +DROP TABLE t1; +CREATE TABLE t1 (a INT, b INT); +INSERT INTO t1 VALUES (1,1),(1,3),(2,2),(2,8); +SELECT DISTINCT -STD(b) FROM t1 GROUP BY a HAVING VARIANCE(b) < 2; +-STD(b) +-1.0000 +SELECT DISTINCT -STD(b) FROM t1 GROUP BY a ORDER BY STD(b) DESC; +-STD(b) +-3.0000 +-1.0000 +DROP TABLE t1; +# The value of a DOUBLE aggregate is not rounded to its decimals +CREATE TABLE t1 (a INT, b INT, c DOUBLE(10,2)); +INSERT INTO t1 VALUES (1,1,0),(1,2,1),(1,3,1),(2,5,5); +SELECT DISTINCT CONCAT('v',VAR_POP(b)) FROM t1 GROUP BY a +HAVING VAR_POP(b) <> '0.6667'; +CONCAT('v',VAR_POP(b)) +v0.0000 +v0.6667 +SELECT DISTINCT CONCAT('v',AVG(c)) FROM t1 GROUP BY a +HAVING AVG(c) <> '0.666667'; +CONCAT('v',AVG(c)) +v0.666667 +v5.000000 +DROP TABLE t1; +# A BIT stored function over an aggregate, used as a HAVING alias +CREATE TABLE t1 (c INT); +INSERT INTO t1 VALUES (1),(1),(2),(2),(3),(3); +CREATE FUNCTION fbit(x INT) RETURNS BIT(8) RETURN x; +SELECT SQL_BIG_RESULT c, fbit(COUNT(*) + 63) AS d FROM t1 GROUP BY c +HAVING d > 64; +c d +1 A +2 A +3 A +DROP FUNCTION fbit; +DROP TABLE t1; +# # End of 10.11 tests # # diff --git a/mysql-test/main/having.test b/mysql-test/main/having.test index 4ca8c3a6748a7..ec283d2f065bd 100644 --- a/mysql-test/main/having.test +++ b/mysql-test/main/having.test @@ -1075,6 +1075,55 @@ EXECUTE stmt; DROP TABLE t1; DROP VIEW v1; SET optimizer_trace= @save_optimizer_trace; + +--echo # +--echo # MDEV-40779 DISTINCT with HAVING on AVG() returns no rows when +--echo # the last group fails HAVING +--echo # + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1),(2),(3); + +SELECT DISTINCT -AVG(a) FROM t1 GROUP BY a HAVING AVG(a) < 2; +--sorted_result +SELECT DISTINCT -AVG(a), AVG(a) FROM t1 GROUP BY a HAVING AVG(a) < 3; +SELECT DISTINCT -AVG(a) FROM t1 GROUP BY a ORDER BY AVG(a) DESC; + +DROP TABLE t1; + +CREATE TABLE t1 (a INT, b INT); +INSERT INTO t1 VALUES (1,1),(1,3),(2,2),(2,8); + +SELECT DISTINCT -STD(b) FROM t1 GROUP BY a HAVING VARIANCE(b) < 2; +SELECT DISTINCT -STD(b) FROM t1 GROUP BY a ORDER BY STD(b) DESC; + +DROP TABLE t1; + +--echo # The value of a DOUBLE aggregate is not rounded to its decimals +CREATE TABLE t1 (a INT, b INT, c DOUBLE(10,2)); +INSERT INTO t1 VALUES (1,1,0),(1,2,1),(1,3,1),(2,5,5); + +--sorted_result +SELECT DISTINCT CONCAT('v',VAR_POP(b)) FROM t1 GROUP BY a + HAVING VAR_POP(b) <> '0.6667'; +--sorted_result +SELECT DISTINCT CONCAT('v',AVG(c)) FROM t1 GROUP BY a + HAVING AVG(c) <> '0.666667'; + +DROP TABLE t1; + +--echo # A BIT stored function over an aggregate, used as a HAVING alias +CREATE TABLE t1 (c INT); +INSERT INTO t1 VALUES (1),(1),(2),(2),(3),(3); +CREATE FUNCTION fbit(x INT) RETURNS BIT(8) RETURN x; + +--sorted_result +SELECT SQL_BIG_RESULT c, fbit(COUNT(*) + 63) AS d FROM t1 GROUP BY c + HAVING d > 64; + +DROP FUNCTION fbit; +DROP TABLE t1; + --echo # --echo # End of 10.11 tests --echo # diff --git a/mysql-test/main/olap.result b/mysql-test/main/olap.result index 514bad04f6356..93e0b69260d45 100644 --- a/mysql-test/main/olap.result +++ b/mysql-test/main/olap.result @@ -920,4 +920,41 @@ a 1 NULL drop table t1; +# +# MDEV-40779: HAVING is not checked for ROLLUP rows with DISTINCT +# +create table t1 (a int); +insert into t1 values (1),(2); +# The total row has a=NULL and fails a<0 and a=2. +select distinct a, a+0 as d from t1 group by a with rollup having a<0; +a d +select distinct a, a+0 as d from t1 group by a with rollup having a=2; +a d +2 2 +select distinct a, a+0 as d from t1 group by a with rollup having a is null; +a d +NULL NULL +drop table t1; +# +# MDEV-40779: ROLLUP rows lost with DISTINCT when a group column +# is used only in an expression +# +create table t1 (a int, b int); +insert into t1 values (1,1),(1,2),(2,1); +# The total row has a+0=NULL. +select distinct a+0 from t1 group by a with rollup; +a+0 +1 +2 +NULL +# The subtotal rows have b+0=NULL, the total row has a=NULL too. +select distinct a, b+0 from t1 group by a,b with rollup; +a b+0 +1 1 +1 2 +1 NULL +2 1 +2 NULL +NULL NULL +drop table t1; # End of 11.4 tests diff --git a/mysql-test/main/olap.test b/mysql-test/main/olap.test index cdf553c0b5857..2c099cec2048b 100644 --- a/mysql-test/main/olap.test +++ b/mysql-test/main/olap.test @@ -548,4 +548,37 @@ select distinct a from t1 where a=1 and rand() >= 0 group by a with rollup; drop table t1; +--echo # +--echo # MDEV-40779: HAVING is not checked for ROLLUP rows with DISTINCT +--echo # + +create table t1 (a int); +insert into t1 values (1),(2); + +--echo # The total row has a=NULL and fails a<0 and a=2. +select distinct a, a+0 as d from t1 group by a with rollup having a<0; +--sorted_result +select distinct a, a+0 as d from t1 group by a with rollup having a=2; +--sorted_result +select distinct a, a+0 as d from t1 group by a with rollup having a is null; + +drop table t1; + +--echo # +--echo # MDEV-40779: ROLLUP rows lost with DISTINCT when a group column +--echo # is used only in an expression +--echo # + +create table t1 (a int, b int); +insert into t1 values (1,1),(1,2),(2,1); + +--echo # The total row has a+0=NULL. +--sorted_result +select distinct a+0 from t1 group by a with rollup; +--echo # The subtotal rows have b+0=NULL, the total row has a=NULL too. +--sorted_result +select distinct a, b+0 from t1 group by a,b with rollup; + +drop table t1; + --echo # End of 11.4 tests diff --git a/sql/item.h b/sql/item.h index 4ba325c5fb010..9199d261ac386 100644 --- a/sql/item.h +++ b/sql/item.h @@ -689,7 +689,7 @@ class Tmp_field_src { Field *m_field; Field *m_default_field; - Item_result_field *m_item_result_field; + Item *m_item_result_field; public: Tmp_field_src() :m_field(0), @@ -698,10 +698,10 @@ class Tmp_field_src { } Field *field() const { return m_field; } Field *default_field() const { return m_default_field; } - Item_result_field *item_result_field() const { return m_item_result_field; } + Item *item_result_field() const { return m_item_result_field; } void set_field(Field *field) { m_field= field; } void set_default_field(Field *field) { m_default_field= field; } - void set_item_result_field(Item_result_field *item) + void set_item_result_field(Item *item) { m_item_result_field= item; } }; diff --git a/sql/item_sum.h b/sql/item_sum.h index a576bd2ea69dc..1c4b177baaeac 100644 --- a/sql/item_sum.h +++ b/sql/item_sum.h @@ -1543,10 +1543,19 @@ class Item_sum_sp :public Item_sum, class Item_sum_field :public Item { protected: + /* Aggregate state in the temp table that does the grouping */ Field *field; + /* + Column holding the value of this item in a later temp table, such as + the one for DISTINCT. field points into the grouping table's record + buffer, which holds the last group once that table has been read, so + HAVING, ORDER BY and duplicate removal on the later table must read + this column instead. + */ + Field *result_field; public: Item_sum_field(THD *thd, Item_sum *item) - :Item(thd), field(item->result_field) + :Item(thd), field(item->result_field), result_field(NULL) { name= item->name; set_maybe_null(); @@ -1555,12 +1564,41 @@ class Item_sum_field :public Item unsigned_flag= item->unsigned_flag; } table_map used_tables() const override { return (table_map) 1L; } + Field *get_tmp_table_field() override { return result_field; } Field *create_tmp_field_ex(MEM_ROOT *root, TABLE *table, Tmp_field_src *src, const Tmp_field_param *param) override { - return create_tmp_field_ex_simple(root, table, src, param); + Field *result; + /* + Keep every digit of a DOUBLE value. A column with fixed decimals + rounds it to this item's decimals before HAVING and ORDER BY compare + it. Item_sum_variance::create_tmp_field creates its column the same + way when the temp table does not do the grouping. + */ + if (result_type() == REAL_RESULT) + { + if ((result= new (root) Field_double(max_length, maybe_null(), &name, + decimals, true))) + result->init(table); + } + else + result= create_tmp_field_ex_simple(root, table, src, param); + if (result && param->modify_item()) + { + result_field= result; + src->set_item_result_field(this); + } + return result; + } + void save_in_result_field(bool no_conversions) override + { + save_in_field(result_field, no_conversions); + } + void cleanup() override + { + Item::cleanup(); + result_field= NULL; } - void save_in_result_field(bool no_conversions) override { DBUG_ASSERT(0); } bool check_vcol_func_processor(void *arg) override { return mark_unsupported_function(name.str, arg, VCOL_IMPOSSIBLE); diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 46ecf49c67038..6585209f8f6ec 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -26516,7 +26516,7 @@ end_write_group(JOIN *join, JOIN_TAB *join_tab __attribute__((unused)), { if (unlikely(join->rollup_write_data((uint) (idx+1), join_tab->tmp_table_param, - table))) + table, join_tab->having))) { DBUG_RETURN(NESTED_LOOP_ERROR); } @@ -30841,6 +30841,7 @@ int JOIN::rollup_send_data(uint idx) - 1 = First group changed (a) - 2 = Second group changed (a,b) @param table reference to temp table + @param having_arg JOIN_TAB::having @retval 0 ok @@ -30849,19 +30850,28 @@ int JOIN::rollup_send_data(uint idx) */ int JOIN::rollup_write_data(uint idx, TMP_TABLE_PARAM *tmp_table_param_arg, - TABLE *table_arg) + TABLE *table_arg, Item *having_arg) { uint i; + /* + When the optimizer moves HAVING onto the JOIN_TAB, JOIN::having + becomes NULL. ROLLUP rows must then pass the JOIN_TAB condition. + */ + Item *rollup_having= having_arg ? having_arg : having; for (i= send_group_parts ; i-- > idx ; ) { /* Get reference pointers to sum functions in place */ copy_ref_ptr_array(ref_ptrs, rollup.ref_pointer_arrays[i]); - if ((!having || having->val_bool())) + if ((!rollup_having || rollup_having->val_bool())) { int write_error; - Item *item; - List_iterator_fast it(rollup.fields[i]); - while ((item= it++)) + /* + Store NULL in the temp table columns of the group columns + rolled up at this level. Expressions over a group column are + computed this table is read. The ref array holds the NULL + items of hidden group columns, too. + */ + for (Item *item : rollup.ref_pointer_arrays[i]) { if (item->type() == Item::NULL_ITEM && item->is_result_field()) item->save_in_result_field(1); @@ -30870,11 +30880,11 @@ int JOIN::rollup_write_data(uint idx, TMP_TABLE_PARAM *tmp_table_param_arg, if (unlikely((write_error= table_arg->file->ha_write_tmp_row(table_arg->record[0])))) { - if (create_internal_tmp_table_from_heap(thd, table_arg, + if (create_internal_tmp_table_from_heap(thd, table_arg, tmp_table_param_arg->start_recinfo, &tmp_table_param_arg->recinfo, write_error, 0, NULL)) - return 1; + return 1; } } } diff --git a/sql/sql_select.h b/sql/sql_select.h index 39c15270ee230..97d0344c1929b 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -1838,7 +1838,8 @@ class JOIN :public Sql_alloc bool rollup_make_fields(List &all_fields, List &fields, Item_sum ***func); int rollup_send_data(uint idx); - int rollup_write_data(uint idx, TMP_TABLE_PARAM *tmp_table_param, TABLE *table); + int rollup_write_data(uint idx, TMP_TABLE_PARAM *tmp_table_param, + TABLE *table, Item *having); void join_free(); /** Cleanup this JOIN, possibly for reuse */ void cleanup(bool full);