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

MDEV-24684 Quadratic time for MIN/MAX/STD/VARIANCE as window functions

Frame_scan_cursor cleared and re-scanned the whole frame for every row.
For MIN, MAX, STD and VARIANCE keep the result if the frame did not
change (whole-partition frames, peers), and add only the new rows if
just the bottom moved down. Other functions without remove() (UDF and
stored aggregates) are still re-scanned. Argument conversion warnings
are no longer repeated when the frame is reused or extended.
This commit is contained in:
forkfun
2026-10-01 17:44:58 +02:00
parent 5ab51d6248
commit 4dbf847e68
3 changed files with 302 additions and 20 deletions

View File

@@ -862,3 +862,155 @@ pk a b c min1 max1 min2 max2
11 4 200 eleven 100 300 100 300
drop table t2;
drop table t1;
#
# MDEV-24684 MIN/MAX as window functions are slow when the frame is not changing
#
create table t1 (g int, a int, b int);
insert into t1 values
(1,1,10),(1,1,20),(1,2,NULL),(1,2,5),(1,3,7),
(2,1,NULL),(2,1,NULL),
(3,4,3),(3,5,9),(3,5,1),(3,6,2);
select g, a, b, min(b) over (partition by g) mn, max(b) over (partition by g) mx
from t1 order by g, a, b;
g a b mn mx
1 1 10 5 20
1 1 20 5 20
1 2 NULL 5 20
1 2 5 5 20
1 3 7 5 20
2 1 NULL NULL NULL
2 1 NULL NULL NULL
3 4 3 1 9
3 5 1 1 9
3 5 9 1 9
3 6 2 1 9
select g, a, b, min(b) over (partition by g order by a) mn,
max(b) over (partition by g order by a) mx,
std(b) over (partition by g order by a) sd,
variance(b) over (partition by g order by a) vr
from t1 order by g, a, b;
g a b mn mx sd vr
1 1 10 10 20 5.0000 25.0000
1 1 20 10 20 5.0000 25.0000
1 2 NULL 5 20 6.2361 38.8889
1 2 5 5 20 6.2361 38.8889
1 3 7 5 20 5.7663 33.2500
2 1 NULL NULL NULL NULL NULL
2 1 NULL NULL NULL NULL NULL
3 4 3 3 3 0.0000 0.0000
3 5 1 1 9 3.3993 11.5556
3 5 9 1 9 3.3993 11.5556
3 6 2 1 9 3.1125 9.6875
select g, a, b, min(b) over (partition by g order by a, b
rows between 1 following and 2 following) mn
from t1 order by g, a, b;
g a b mn
1 1 10 20
1 1 20 5
1 2 NULL 5
1 2 5 7
1 3 7 NULL
2 1 NULL NULL
2 1 NULL NULL
3 4 3 1
3 5 1 2
3 5 9 2
3 6 2 NULL
select g, a, b,
min(b) over (partition by g order by a, b rows between unbounded preceding and current row) c1,
max(b) over (partition by g order by a, b rows between unbounded preceding and 1 following) c2,
std(b) over (partition by g order by a, b rows between unbounded preceding and 1 preceding) c3
from t1 order by g, a, b;
g a b c1 c2 c3
1 1 10 10 20 NULL
1 1 20 10 20 0.0000
1 2 NULL 10 20 5.0000
1 2 5 5 20 5.0000
1 3 7 5 20 6.2361
2 1 NULL NULL NULL NULL
2 1 NULL NULL NULL NULL
3 4 3 3 3 NULL
3 5 1 1 9 0.0000
3 5 9 1 9 1.0000
3 6 2 1 9 3.3993
select g, a, b,
min(b) over (partition by g order by a, b rows between 1 preceding and 1 following) mn,
max(b) over (partition by g order by a, b rows between 1 preceding and 1 following) mx
from t1 order by g, a, b;
g a b mn mx
1 1 10 10 20
1 1 20 10 20
1 2 NULL 5 20
1 2 5 5 7
1 3 7 5 7
2 1 NULL NULL NULL
2 1 NULL NULL NULL
3 4 3 1 3
3 5 1 1 9
3 5 9 1 9
3 6 2 2 9
select max(b) over () from t1 order by 1 limit 3;
max(b) over ()
20
20
20
drop table t1;
create table t1 (a int);
insert into t1 values (0),(4),(5),(6),(20);
select a, min(a) over (order by a range between 10 following and 5 following) mn,
max(a) over (order by a range between 10 following and 5 following) mx,
std(a) over (order by a range between 10 following and 5 following) sd
from t1;
a mn mx sd
0 NULL NULL NULL
4 NULL NULL NULL
5 NULL NULL NULL
6 NULL NULL NULL
20 NULL NULL NULL
drop table t1;
create table t2 (i int, v varchar(10) collate utf8mb4_general_ci, d decimal(10,2), t datetime);
insert into t2 values
(1,'b',1.50,'2020-01-03 00:00:00'),(2,'A',-2.25,'2020-01-01 00:00:00'),
(3,'c',NULL,NULL),(4,'D',4.00,'2020-01-02 00:00:00'),(5,NULL,0.75,'2020-01-05 00:00:00');
select i, v, d, t,
min(v) over (order by i) mnv,
max(v) over (order by i desc) mxv,
max(d) over (order by i rows between unbounded preceding and 1 following) mxd,
std(d) over (order by i rows between unbounded preceding and current row) sdd,
min(t) over (order by i desc) mnt
from t2 order by i;
i v d t mnv mxv mxd sdd mnt
1 b 1.50 2020-01-03 00:00:00 b D 1.50 0.000000 2020-01-01 00:00:00
2 A -2.25 2020-01-01 00:00:00 A D 1.50 1.875000 2020-01-01 00:00:00
3 c NULL NULL A D 4.00 1.875000 2020-01-02 00:00:00
4 D 4.00 2020-01-02 00:00:00 A D 4.00 2.568506 2020-01-02 00:00:00
5 NULL 0.75 2020-01-05 00:00:00 A NULL 4.00 2.229069 2020-01-05 00:00:00
drop table t2;
create table t1 (a int);
insert into t1 select seq from seq_1_to_2000;
flush status;
select max(s) from (select max(a) over () s from t1) w;
max(s)
2000
select variable_value < 100000 from information_schema.session_status
where variable_name = 'handler_read_rnd';
variable_value < 100000
1
flush status;
select max(s) from (select min(a) over (order by a) s from t1) w;
max(s)
1
select variable_value < 100000 from information_schema.session_status
where variable_name = 'handler_read_rnd';
variable_value < 100000
1
flush status;
select max(s) from (select std(a) over (order by a) s from t1) w;
max(s)
577.3502
select variable_value < 100000 from information_schema.session_status
where variable_name = 'handler_read_rnd';
variable_value < 100000
1
drop table t1;
# End of 13.1 tests

View File

@@ -368,3 +368,86 @@ from t2;
drop table t2;
drop table t1;
--source include/have_sequence.inc
--echo #
--echo # MDEV-24684 MIN/MAX as window functions are slow when the frame is not changing
--echo #
create table t1 (g int, a int, b int);
insert into t1 values
(1,1,10),(1,1,20),(1,2,NULL),(1,2,5),(1,3,7),
(2,1,NULL),(2,1,NULL),
(3,4,3),(3,5,9),(3,5,1),(3,6,2);
select g, a, b, min(b) over (partition by g) mn, max(b) over (partition by g) mx
from t1 order by g, a, b;
select g, a, b, min(b) over (partition by g order by a) mn,
max(b) over (partition by g order by a) mx,
std(b) over (partition by g order by a) sd,
variance(b) over (partition by g order by a) vr
from t1 order by g, a, b;
select g, a, b, min(b) over (partition by g order by a, b
rows between 1 following and 2 following) mn
from t1 order by g, a, b;
select g, a, b,
min(b) over (partition by g order by a, b rows between unbounded preceding and current row) c1,
max(b) over (partition by g order by a, b rows between unbounded preceding and 1 following) c2,
std(b) over (partition by g order by a, b rows between unbounded preceding and 1 preceding) c3
from t1 order by g, a, b;
select g, a, b,
min(b) over (partition by g order by a, b rows between 1 preceding and 1 following) mn,
max(b) over (partition by g order by a, b rows between 1 preceding and 1 following) mx
from t1 order by g, a, b;
select max(b) over () from t1 order by 1 limit 3;
drop table t1;
create table t1 (a int);
insert into t1 values (0),(4),(5),(6),(20);
select a, min(a) over (order by a range between 10 following and 5 following) mn,
max(a) over (order by a range between 10 following and 5 following) mx,
std(a) over (order by a range between 10 following and 5 following) sd
from t1;
drop table t1;
create table t2 (i int, v varchar(10) collate utf8mb4_general_ci, d decimal(10,2), t datetime);
insert into t2 values
(1,'b',1.50,'2020-01-03 00:00:00'),(2,'A',-2.25,'2020-01-01 00:00:00'),
(3,'c',NULL,NULL),(4,'D',4.00,'2020-01-02 00:00:00'),(5,NULL,0.75,'2020-01-05 00:00:00');
select i, v, d, t,
min(v) over (order by i) mnv,
max(v) over (order by i desc) mxv,
max(d) over (order by i rows between unbounded preceding and 1 following) mxd,
std(d) over (order by i rows between unbounded preceding and current row) sdd,
min(t) over (order by i desc) mnt
from t2 order by i;
drop table t2;
create table t1 (a int);
insert into t1 select seq from seq_1_to_2000;
flush status;
select max(s) from (select max(a) over () s from t1) w;
select variable_value < 100000 from information_schema.session_status
where variable_name = 'handler_read_rnd';
flush status;
select max(s) from (select min(a) over (order by a) s from t1) w;
select variable_value < 100000 from information_schema.session_status
where variable_name = 'handler_read_rnd';
flush status;
select max(s) from (select std(a) over (order by a) s from t1) w;
select variable_value < 100000 from information_schema.session_status
where variable_name = 'handler_read_rnd';
drop table t1;
--echo # End of 13.1 tests

View File

@@ -2201,16 +2201,34 @@ private:
}
};
/* Sum functions whose add() only accumulates and whose result can be read
repeatedly */
static bool is_scan_result_reusable(Item_sum::Sumfunctype sum_func)
{
switch (sum_func)
{
case Item_sum::MIN_FUNC:
case Item_sum::MAX_FUNC:
case Item_sum::STD_FUNC:
case Item_sum::VARIANCE_FUNC:
return true;
default:
return false;
}
}
/*
A cursor that performs a table scan between two indices. The indices
are provided by the two cursors representing the top and bottom bound
of the window function's frame definition.
Each scan clears the sum function.
If the sum functions allow it, the result is kept if the frame is unchanged
and extended if only the bottom moved down. Else the frame is rescanned.
NOTE:
The cursor does not alter the top and bottom cursors.
This type of cursor is expensive computational wise. This is only to be
A rescan is expensive computational wise. This is only to be
used when the sum functions do not support removal.
*/
class Frame_scan_cursor : public Frame_cursor
@@ -2218,21 +2236,23 @@ class Frame_scan_cursor : public Frame_cursor
public:
Frame_scan_cursor(const Frame_cursor &top_bound,
const Frame_cursor &bottom_bound) :
top_bound(top_bound), bottom_bound(bottom_bound) {}
top_bound(top_bound), bottom_bound(bottom_bound), have_result(false),
can_append(false) {}
void init(READ_RECORD *info) override
{
cursor.init(info);
can_append= true;
List_iterator_fast<Item_sum> it(sum_functions);
Item_sum *item_sum;
while ((item_sum= it++))
can_append&= is_scan_result_reusable(item_sum->sum_func());
}
void pre_next_partition(ha_rows rownum) override
{
/* TODO(cvicentiu) Sum functions get cleared on next partition anyway during
the window function computation algorithm. Either perform this only in
cursors, or remove it from pre_next_partition.
*/
curr_rownum= rownum;
clear_sum_functions();
have_result= false;
}
void next_partition(ha_rows rownum) override
@@ -2240,11 +2260,6 @@ public:
compute_values_for_current_row();
}
void pre_next_row() override
{
clear_sum_functions();
}
void next_row() override
{
curr_rownum++;
@@ -2261,31 +2276,63 @@ private:
const Frame_cursor &bottom_bound;
Table_read_cursor cursor;
ha_rows curr_rownum;
/* The sum functions hold the result for rows last_top..last_bottom */
bool have_result;
ha_rows last_top, last_bottom;
bool can_append;
/* Scan the rows between the top bound and bottom bound. Add all the values
between them, top bound row and bottom bound row inclusive. */
/* Make the sum functions hold the rows between the top bound and bottom
bound, both inclusive: reuse, extend or rescan the previous result. */
void compute_values_for_current_row()
{
THD *thd= current_thd;
if (top_bound.is_outside_computation_bounds() ||
bottom_bound.is_outside_computation_bounds())
{
clear_sum_functions();
have_result= false;
return;
}
ha_rows start_rownum= top_bound.get_curr_rownum();
ha_rows bottom_rownum= bottom_bound.get_curr_rownum();
DBUG_PRINT("info", ("COMPUTING (%llu %llu)", start_rownum, bottom_rownum));
cursor.move_to(start_rownum);
for (ha_rows idx= start_rownum; idx <= bottom_rownum
&& ((idx & 0xFF) || !thd->check_killed(true)); idx++)
ha_rows first_rownum= start_rownum;
if (can_append && have_result && start_rownum == last_top)
{
if (cursor.fetch()) //EOF
if (bottom_rownum == last_bottom)
return;
/* Extend only a previous frame ending at most 1 row above the top:
after an inverted frame ending higher, the rows in between would
be added */
if (bottom_rownum > last_bottom && last_bottom + 1 >= start_rownum)
first_rownum= last_bottom + 1;
}
if (first_rownum == start_rownum)
clear_sum_functions();
have_result= false;
cursor.move_to(first_rownum);
ha_rows idx= first_rownum;
while (idx <= bottom_rownum
&& ((idx & 0xFF) || !thd->check_killed(true)))
{
if (cursor.fetch()) // EOF or read error
break;
add_value_to_items();
idx++;
if (cursor.next()) // EOF
break;
}
/* Cache the result only if the scan completed */
if (idx > bottom_rownum)
{
have_result= true;
last_top= start_rownum;
last_bottom= bottom_rownum;
}
}
};