diff --git a/mysql-test/main/win_min_max.result b/mysql-test/main/win_min_max.result index b3a90262bf7..1d1e43ed2a0 100644 --- a/mysql-test/main/win_min_max.result +++ b/mysql-test/main/win_min_max.result @@ -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 diff --git a/mysql-test/main/win_min_max.test b/mysql-test/main/win_min_max.test index 8efde87ffdf..fe650a2e133 100644 --- a/mysql-test/main/win_min_max.test +++ b/mysql-test/main/win_min_max.test @@ -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 diff --git a/sql/sql_window.cc b/sql/sql_window.cc index e5b4b4de7d6..54729c8862c 100644 --- a/sql/sql_window.cc +++ b/sql/sql_window.cc @@ -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 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; + } } };