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

MDEV-40762: Wrong MIN()/MAX() in loose index scan

Problem: GROUP BY with MIN()/MAX() can return the opposite extremum
when loose index scan chooses among indexes with different ordering.
MDEV-32732 (ec6f320883) added support for
descending indexes in loose index scan. It exposed a previously harmless
incorrect MIN/MAX key-part pointer: index enumeration overwrites
min_max_arg_part even when the candidate is rejected or not selected.
The pointer can then refer to a different index from the chosen plan.
Using its index-specific key_part_flag to determine scan direction
causes the regression.

Fix: Keep the MIN/MAX key-part pointer local to each candidate and
use it for the candidate's gap, infix and predicate checks. Save it
with the chosen index only when that candidate becomes the best plan.

(Cosmetic edits to testcase and commit comment by Sergei Petrunia)
This commit is contained in:
DerZc
2026-09-21 12:46:46 +08:00
committed by Sergei Petrunia
parent 5a3a6b5582
commit dec45ff9f3
3 changed files with 90 additions and 10 deletions

View File

@@ -600,3 +600,46 @@ min(kp2)
3
3
drop table t20;
#
# MDEV-40762: MIN/MAX with ascending and descending index candidates
#
# Ascending index chosen first
CREATE TABLE t1 (g CHAR(4) NOT NULL, v INT NOT NULL,
KEY idx_asc (g,v),
KEY idx_desc (g,v DESC)) ENGINE=InnoDB;
INSERT INTO t1 VALUES ('A',1),('A',2),('A',3);
# Uses key=idx_asc and "Using index for group-by":
EXPLAIN SELECT g, MIN(v) FROM t1 GROUP BY g;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 range NULL idx_asc 16 NULL # Using index for group-by
SELECT g, MIN(v) FROM t1 GROUP BY g;
g MIN(v)
A 1
# Uses key=idx_asc and "Using index for group-by":
EXPLAIN SELECT g, MAX(v) FROM t1 GROUP BY g;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 range NULL idx_asc 16 NULL # Using index for group-by
SELECT g, MAX(v) FROM t1 GROUP BY g;
g MAX(v)
A 3
DROP TABLE t1;
# Descending index chosen first
CREATE TABLE t1 (g CHAR(4) NOT NULL, v INT NOT NULL,
KEY idx_desc (g,v DESC),
KEY idx_asc (g,v)) ENGINE=InnoDB;
INSERT INTO t1 VALUES ('A',1),('A',2),('A',3);
# Uses key=idx_desc and "Using index for group-by":
EXPLAIN SELECT g, MIN(v) FROM t1 GROUP BY g;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 range NULL idx_desc 16 NULL # Using index for group-by
SELECT g, MIN(v) FROM t1 GROUP BY g;
g MIN(v)
A 1
# Uses key=idx_desc and "Using index for group-by":
EXPLAIN SELECT g, MAX(v) FROM t1 GROUP BY g;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 range NULL idx_desc 16 NULL # Using index for group-by
SELECT g, MAX(v) FROM t1 GROUP BY g;
g MAX(v)
A 3
DROP TABLE t1;

View File

@@ -396,3 +396,35 @@ eval EXPLAIN $query;
eval $query;
drop table t20;
--echo #
--echo # MDEV-40762: MIN/MAX with ascending and descending index candidates
--echo #
--echo # Ascending index chosen first
CREATE TABLE t1 (g CHAR(4) NOT NULL, v INT NOT NULL,
KEY idx_asc (g,v),
KEY idx_desc (g,v DESC)) ENGINE=InnoDB;
INSERT INTO t1 VALUES ('A',1),('A',2),('A',3);
--echo # Uses key=idx_asc and "Using index for group-by":
--replace_column 9 #
EXPLAIN SELECT g, MIN(v) FROM t1 GROUP BY g;
SELECT g, MIN(v) FROM t1 GROUP BY g;
--echo # Uses key=idx_asc and "Using index for group-by":
--replace_column 9 #
EXPLAIN SELECT g, MAX(v) FROM t1 GROUP BY g;
SELECT g, MAX(v) FROM t1 GROUP BY g;
DROP TABLE t1;
--echo # Descending index chosen first
CREATE TABLE t1 (g CHAR(4) NOT NULL, v INT NOT NULL,
KEY idx_desc (g,v DESC),
KEY idx_asc (g,v)) ENGINE=InnoDB;
INSERT INTO t1 VALUES ('A',1),('A',2),('A',3);
--echo # Uses key=idx_desc and "Using index for group-by":
--replace_column 9 #
EXPLAIN SELECT g, MIN(v) FROM t1 GROUP BY g;
SELECT g, MIN(v) FROM t1 GROUP BY g;
--echo # Uses key=idx_desc and "Using index for group-by":
--replace_column 9 #
EXPLAIN SELECT g, MAX(v) FROM t1 GROUP BY g;
SELECT g, MAX(v) FROM t1 GROUP BY g;
DROP TABLE t1;

View File

@@ -14849,6 +14849,7 @@ get_best_group_min_max(PARAM *param, SEL_TREE *tree, double read_time)
KEY_PART_INFO *last_part;
KEY_PART_INFO *first_non_group_part;
KEY_PART_INFO *first_non_infix_part;
KEY_PART_INFO *cur_min_max_arg_part= NULL;
uint key_parts;
uint key_infix_parts;
uint cur_group_key_parts= 0;
@@ -15028,7 +15029,7 @@ get_best_group_min_max(PARAM *param, SEL_TREE *tree, double read_time)
cause= "aggregate column not suffix in idx";
goto next_index;
}
min_max_arg_part= cur_index_info->key_part + key_part_nr - 1;
cur_min_max_arg_part= cur_index_info->key_part + key_part_nr - 1;
}
/*
@@ -15062,19 +15063,21 @@ get_best_group_min_max(PARAM *param, SEL_TREE *tree, double read_time)
first_non_group_part= (cur_group_key_parts < key_parts) ?
cur_index_info->key_part + cur_group_key_parts :
NULL;
first_non_infix_part= min_max_arg_part ?
(min_max_arg_part < last_part) ?
min_max_arg_part :
first_non_infix_part= cur_min_max_arg_part ?
(cur_min_max_arg_part < last_part) ?
cur_min_max_arg_part :
NULL :
NULL;
if (first_non_group_part &&
(!min_max_arg_part || (min_max_arg_part - first_non_group_part > 0)))
(!cur_min_max_arg_part ||
(cur_min_max_arg_part - first_non_group_part > 0)))
{
if (tree)
{
SEL_ARG *index_range_tree= tree->keys[cur_param_idx];
if (!get_constant_key_infix(cur_index_info, index_range_tree,
first_non_group_part, min_max_arg_part,
first_non_group_part,
cur_min_max_arg_part,
last_part, thd, cur_key_infix,
&cur_key_infix_len,
&first_non_infix_part))
@@ -15083,8 +15086,8 @@ get_best_group_min_max(PARAM *param, SEL_TREE *tree, double read_time)
goto next_index;
}
}
else if (min_max_arg_part &&
(min_max_arg_part - first_non_group_part > 0))
else if (cur_min_max_arg_part &&
(cur_min_max_arg_part - first_non_group_part > 0))
{
/*
There is a gap but no range tree, thus no predicates at all for the
@@ -15128,7 +15131,8 @@ get_best_group_min_max(PARAM *param, SEL_TREE *tree, double read_time)
if (first_non_infix_part)
{
cur_part= first_non_infix_part +
(min_max_arg_part && (min_max_arg_part < last_part));
(cur_min_max_arg_part &&
(cur_min_max_arg_part < last_part));
for (; cur_part != last_part; cur_part++)
{
if (bitmap_is_set(table->read_set, cur_part->field->field_index))
@@ -15148,7 +15152,7 @@ get_best_group_min_max(PARAM *param, SEL_TREE *tree, double read_time)
{
SEL_ARG *index_range_tree= tree->keys[cur_param_idx];
SEL_ARG *cur_range= NULL;
if (get_sel_arg_for_keypart(min_max_arg_part->field,
if (get_sel_arg_for_keypart(cur_min_max_arg_part->field,
index_range_tree, &cur_range) ||
(cur_range && cur_range->type != SEL_ARG::KEY_RANGE))
{
@@ -15215,6 +15219,7 @@ get_best_group_min_max(PARAM *param, SEL_TREE *tree, double read_time)
if (key_infix_len)
memcpy (key_infix, cur_key_infix, sizeof (key_infix));
used_key_parts= cur_used_key_parts;
min_max_arg_part= cur_min_max_arg_part;
}
next_index: