diff --git a/mysql-test/main/win_streaming.result b/mysql-test/main/win_streaming.result new file mode 100644 index 0000000000000..1be4a306d18f5 --- /dev/null +++ b/mysql-test/main/win_streaming.result @@ -0,0 +1,846 @@ +CREATE TABLE t1 (pk INT PRIMARY KEY, a INT, b INT); +INSERT INTO t1 VALUES (1, 1, 10); +INSERT INTO t1 VALUES (2, 1, 10); +INSERT INTO t1 VALUES (3, 1, 20); +INSERT INTO t1 VALUES (4, 2, 20); +INSERT INTO t1 VALUES (5, 2, 20); +INSERT INTO t1 VALUES (6, 2, 30); +INSERT INTO t1 VALUES (7, 3, 10); +INSERT INTO t1 VALUES (8, 3, 30); +INSERT INTO t1 VALUES (9, 3, 30); +EXPLAIN FORMAT=JSON SELECT pk, a, b, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (PARTITION BY a ORDER BY b, pk); +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": "COST_REPLACED", + "nested_loop": [ + { + "read_sorted_file": { + "filesort": { + "sort_key": "t1.a, t1.b, t1.pk", + "table": { + "table_name": "t1", + "access_type": "ALL", + "loops": 1, + "rows": 9, + "cost": "COST_REPLACED", + "filtered": 100 + } + } + } + } + ] + } +} +EXPLAIN EXTENDED SELECT pk, a, b, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (PARTITION BY a ORDER BY b, pk); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +EXPLAIN EXTENDED SELECT SQL_BUFFER_RESULT pk, a, b, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (PARTITION BY a ORDER BY b, pk); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +SELECT pk, a, b, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (PARTITION BY a ORDER BY b, pk); +pk a b rn rnk drnk +1 1 10 1 1 1 +2 1 10 2 2 2 +3 1 20 3 3 3 +4 2 20 1 1 1 +5 2 20 2 2 2 +6 2 30 3 3 3 +7 3 10 1 1 1 +8 3 30 2 2 2 +9 3 30 3 3 3 +SELECT SQL_BUFFER_RESULT pk, a, b, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (PARTITION BY a ORDER BY b, pk); +pk a b rn rnk drnk +1 1 10 1 1 1 +2 1 10 2 2 2 +3 1 20 3 3 3 +4 2 20 1 1 1 +5 2 20 2 2 2 +6 2 30 3 3 3 +7 3 10 1 1 1 +8 3 30 2 2 2 +9 3 30 3 3 3 +EXPLAIN EXTENDED SELECT pk, a, b, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (ORDER BY a); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +SELECT pk, a, b, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (ORDER BY a); +pk a b rnk drnk +1 1 10 1 1 +2 1 10 1 1 +3 1 20 1 1 +4 2 20 4 2 +5 2 20 4 2 +6 2 30 4 2 +7 3 10 7 3 +8 3 30 7 3 +9 3 30 7 3 +SELECT SQL_BUFFER_RESULT pk, a, b, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (ORDER BY a); +pk a b rnk drnk +1 1 10 1 1 +2 1 10 1 1 +3 1 20 1 1 +4 2 20 4 2 +5 2 20 4 2 +6 2 30 4 2 +7 3 10 7 3 +8 3 30 7 3 +9 3 30 7 3 +EXPLAIN EXTENDED SELECT pk, a, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (ORDER BY a, pk); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +SELECT pk, a, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (ORDER BY a, pk); +pk a rn rnk drnk +1 1 1 1 1 +2 1 2 2 2 +3 1 3 3 3 +4 2 4 4 4 +5 2 5 5 5 +6 2 6 6 6 +7 3 7 7 7 +8 3 8 8 8 +9 3 9 9 9 +SELECT SQL_BUFFER_RESULT pk, a, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (ORDER BY a, pk); +pk a rn rnk drnk +1 1 1 1 1 +2 1 2 2 2 +3 1 3 3 3 +4 2 4 4 4 +5 2 5 5 5 +6 2 6 6 6 +7 3 7 7 7 +8 3 8 8 8 +9 3 9 9 9 +EXPLAIN FORMAT=JSON SELECT pk, a, b, rank() OVER (ORDER BY a, b) AS rnk FROM t1 ORDER BY a; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": "COST_REPLACED", + "nested_loop": [ + { + "read_sorted_file": { + "filesort": { + "sort_key": "t1.a, t1.b", + "table": { + "table_name": "t1", + "access_type": "ALL", + "loops": 1, + "rows": 9, + "cost": "COST_REPLACED", + "filtered": 100 + } + } + } + } + ] + } +} +SELECT pk, a, b, rank() OVER (ORDER BY a, b) AS rnk FROM t1 ORDER BY a; +pk a b rnk +1 1 10 1 +2 1 10 1 +3 1 20 3 +4 2 20 4 +5 2 20 4 +6 2 30 6 +7 3 10 7 +8 3 30 8 +9 3 30 8 +SELECT SQL_BUFFER_RESULT pk, a, b, rank() OVER (ORDER BY a, b) AS rnk FROM t1 ORDER BY a; +pk a b rnk +1 1 10 1 +2 1 10 1 +3 1 20 3 +4 2 20 4 +5 2 20 4 +6 2 30 6 +7 3 10 7 +8 3 30 8 +9 3 30 8 +EXPLAIN FORMAT=JSON SELECT pk, a, b, rank() OVER (ORDER BY a) AS rnk FROM t1 ORDER BY a, b; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": "COST_REPLACED", + "nested_loop": [ + { + "read_sorted_file": { + "filesort": { + "sort_key": "t1.a, t1.b", + "table": { + "table_name": "t1", + "access_type": "ALL", + "loops": 1, + "rows": 9, + "cost": "COST_REPLACED", + "filtered": 100 + } + } + } + } + ] + } +} +SELECT pk, a, b, rank() OVER (ORDER BY a) AS rnk FROM t1 ORDER BY a, b; +pk a b rnk +1 1 10 1 +2 1 10 1 +3 1 20 1 +4 2 20 4 +5 2 20 4 +6 2 30 4 +7 3 10 7 +8 3 30 7 +9 3 30 7 +SELECT SQL_BUFFER_RESULT pk, a, b, rank() OVER (ORDER BY a) AS rnk FROM t1 ORDER BY a, b; +pk a b rnk +1 1 10 1 +2 1 10 1 +3 1 20 1 +4 2 20 4 +5 2 20 4 +6 2 30 4 +7 3 10 7 +8 3 30 7 +9 3 30 7 +EXPLAIN FORMAT=JSON SELECT pk, a, rank() OVER (ORDER BY a) AS rnk FROM t1 ORDER BY a; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": "COST_REPLACED", + "nested_loop": [ + { + "read_sorted_file": { + "filesort": { + "sort_key": "t1.a", + "table": { + "table_name": "t1", + "access_type": "ALL", + "loops": 1, + "rows": 9, + "cost": "COST_REPLACED", + "filtered": 100 + } + } + } + } + ] + } +} +SELECT pk, a, rank() OVER (ORDER BY a) AS rnk FROM t1 ORDER BY a; +pk a rnk +1 1 1 +2 1 1 +3 1 1 +4 2 4 +5 2 4 +6 2 4 +7 3 7 +8 3 7 +9 3 7 +SELECT SQL_BUFFER_RESULT pk, a, rank() OVER (ORDER BY a) AS rnk FROM t1 ORDER BY a; +pk a rnk +1 1 1 +2 1 1 +3 1 1 +4 2 4 +5 2 4 +6 2 4 +7 3 7 +8 3 7 +9 3 7 +EXPLAIN EXTENDED SELECT pk, a, rank() OVER w AS r, rank() OVER w + 1 AS r_plus, rank() OVER w - dense_rank() OVER w AS diff FROM t1 WINDOW w AS (ORDER BY a); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +SELECT pk, a, rank() OVER w AS r, rank() OVER w + 1 AS r_plus, rank() OVER w - dense_rank() OVER w AS diff FROM t1 WINDOW w AS (ORDER BY a); +pk a r r_plus diff +1 1 1 2 0 +2 1 1 2 0 +3 1 1 2 0 +4 2 4 5 2 +5 2 4 5 2 +6 2 4 5 2 +7 3 7 8 4 +8 3 7 8 4 +9 3 7 8 4 +SELECT SQL_BUFFER_RESULT pk, a, rank() OVER w AS r, rank() OVER w + 1 AS r_plus, rank() OVER w - dense_rank() OVER w AS diff FROM t1 WINDOW w AS (ORDER BY a); +pk a r r_plus diff +1 1 1 2 0 +2 1 1 2 0 +3 1 1 2 0 +4 2 4 5 2 +5 2 4 5 2 +6 2 4 5 2 +7 3 7 8 4 +8 3 7 8 4 +9 3 7 8 4 +CREATE TABLE t2 (pk INT PRIMARY KEY, c INT); +INSERT INTO t2 VALUES (1, 300); +INSERT INTO t2 VALUES (2, 100); +INSERT INTO t2 VALUES (3, 200); +INSERT INTO t2 VALUES (4, 400); +INSERT INTO t2 VALUES (5, 600); +INSERT INTO t2 VALUES (6, 500); +INSERT INTO t2 VALUES (7, 700); +INSERT INTO t2 VALUES (8, 800); +INSERT INTO t2 VALUES (9, 900); +EXPLAIN EXTENDED SELECT t1.pk, t1.a, t1.b, rank() OVER (ORDER BY t1.b, t1.pk) AS rnk FROM t1 JOIN t2 ON t1.pk = t2.pk; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL PRIMARY NULL NULL NULL 9 100.00 Using filesort +1 SIMPLE t2 eq_ref PRIMARY PRIMARY 4 test.t1.pk 1 100.00 Using index +SELECT t1.pk, t1.a, t1.b, rank() OVER (ORDER BY t1.b, t1.pk) AS rnk FROM t1 JOIN t2 ON t1.pk = t2.pk; +pk a b rnk +1 1 10 1 +2 1 10 2 +3 1 20 4 +4 2 20 5 +5 2 20 6 +6 2 30 7 +7 3 10 3 +8 3 30 8 +9 3 30 9 +SELECT SQL_BUFFER_RESULT t1.pk, t1.a, t1.b, rank() OVER (ORDER BY t1.b, t1.pk) AS rnk FROM t1 JOIN t2 ON t1.pk = t2.pk; +pk a b rnk +1 1 10 1 +2 1 10 2 +3 1 20 4 +4 2 20 5 +5 2 20 6 +6 2 30 7 +7 3 10 3 +8 3 30 8 +9 3 30 9 +EXPLAIN EXTENDED SELECT t1.pk, t1.a, t2.c, rank() OVER (ORDER BY t1.a, t2.c) AS rnk FROM t1 JOIN t2 ON t1.pk = t2.pk; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL PRIMARY NULL NULL NULL 9 100.00 Using temporary +1 SIMPLE t2 eq_ref PRIMARY PRIMARY 4 test.t1.pk 1 100.00 +SELECT t1.pk, t1.a, t2.c, rank() OVER (ORDER BY t1.a, t2.c) AS rnk FROM t1 JOIN t2 ON t1.pk = t2.pk; +pk a c rnk +2 1 100 1 +3 1 200 2 +1 1 300 3 +4 2 400 4 +6 2 500 5 +5 2 600 6 +7 3 700 7 +8 3 800 8 +9 3 900 9 +DROP TABLE t2; +EXPLAIN EXTENDED SELECT d.pk, d.a, d.b, rank() OVER (PARTITION BY d.a ORDER BY d.b) AS rnk, dense_rank() OVER (PARTITION BY d.a ORDER BY d.b) AS drnk FROM (SELECT pk, a, b FROM t1 WHERE a > 1) AS d; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using where; Using filesort +SELECT d.pk, d.a, d.b, rank() OVER (PARTITION BY d.a ORDER BY d.b) AS rnk, dense_rank() OVER (PARTITION BY d.a ORDER BY d.b) AS drnk FROM (SELECT pk, a, b FROM t1 WHERE a > 1) AS d; +pk a b rnk drnk +4 2 20 1 1 +5 2 20 1 1 +6 2 30 3 2 +7 3 10 1 1 +8 3 30 2 2 +9 3 30 2 2 +SELECT SQL_BUFFER_RESULT d.pk, d.a, d.b, rank() OVER (PARTITION BY d.a ORDER BY d.b) AS rnk, dense_rank() OVER (PARTITION BY d.a ORDER BY d.b) AS drnk FROM (SELECT pk, a, b FROM t1 WHERE a > 1) AS d; +pk a b rnk drnk +4 2 20 1 1 +5 2 20 1 1 +6 2 30 3 2 +7 3 10 1 1 +8 3 30 2 2 +9 3 30 2 2 +EXPLAIN EXTENDED SELECT d.a, d.rnk FROM (SELECT a, rank() OVER (ORDER BY a) AS rnk FROM t1) AS d; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 PRIMARY ALL NULL NULL NULL NULL 9 100.00 +2 DERIVED t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +SELECT d.a, d.rnk FROM (SELECT a, rank() OVER (ORDER BY a) AS rnk FROM t1) AS d; +a rnk +1 1 +1 1 +1 1 +2 4 +2 4 +2 4 +3 7 +3 7 +3 7 +ANALYZE FORMAT=JSON SELECT pk, rank() OVER (ORDER BY pk) AS rnk FROM t1 LIMIT 2; +ANALYZE +{ + "query_optimization": { + "r_total_time_ms": "REPLACED" + }, + "query_block": { + "select_id": 1, + "cost": "REPLACED", + "r_loops": 1, + "r_total_time_ms": "REPLACED", + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "index", + "key": "PRIMARY", + "key_length": "4", + "used_key_parts": ["pk"], + "loops": 1, + "r_loops": 1, + "rows": 9, + "r_rows": 2, + "cost": "REPLACED", + "r_table_time_ms": "REPLACED", + "r_other_time_ms": "REPLACED", + "r_engine_stats": REPLACED, + "filtered": 100, + "r_total_filtered": 100, + "r_filtered": 100, + "using_index": true + } + } + ] + } +} +CREATE TABLE tg (a INT, b INT, KEY(a, b)); +INSERT INTO tg VALUES (1, 1); +INSERT INTO tg VALUES (1, 2); +INSERT INTO tg VALUES (2, 1); +INSERT INTO tg VALUES (2, 2); +INSERT INTO tg VALUES (2, 3); +INSERT INTO tg VALUES (3, 1); +EXPLAIN EXTENDED SELECT a, b, rank() OVER (ORDER BY a) AS rnk, dense_rank() OVER (ORDER BY a) AS drnk FROM tg GROUP BY a, b; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE tg range NULL a 10 NULL 6 100.00 Using index for group-by +SELECT a, b, rank() OVER (ORDER BY a) AS rnk, dense_rank() OVER (ORDER BY a) AS drnk FROM tg GROUP BY a, b; +a b rnk drnk +1 1 1 1 +1 2 1 1 +2 1 3 2 +2 2 3 2 +2 3 3 2 +3 1 6 3 +SELECT SQL_BUFFER_RESULT a, b, rank() OVER (ORDER BY a) AS rnk, dense_rank() OVER (ORDER BY a) AS drnk FROM tg GROUP BY a, b; +a b rnk drnk +1 1 1 1 +1 2 1 1 +2 1 3 2 +2 2 3 2 +2 3 3 2 +3 1 6 3 +EXPLAIN EXTENDED SELECT a, rank() OVER (ORDER BY a, b) AS rnk, dense_rank() OVER (ORDER BY a, b) AS drnk FROM tg GROUP BY a; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE tg range NULL a 5 NULL 6 100.00 Using index for group-by +SELECT a, rank() OVER (ORDER BY a, b) AS rnk, dense_rank() OVER (ORDER BY a, b) AS drnk FROM tg GROUP BY a; +a rnk drnk +1 1 1 +2 2 2 +3 3 3 +SELECT SQL_BUFFER_RESULT a, rank() OVER (ORDER BY a, b) AS rnk, dense_rank() OVER (ORDER BY a, b) AS drnk FROM tg GROUP BY a; +a rnk drnk +1 1 1 +2 2 2 +3 3 3 +EXPLAIN EXTENDED SELECT a, b, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM tg GROUP BY a, b WINDOW w AS (PARTITION BY a ORDER BY b); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE tg range NULL a 10 NULL 6 100.00 Using index for group-by +SELECT a, b, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM tg GROUP BY a, b WINDOW w AS (PARTITION BY a ORDER BY b); +a b rn rnk drnk +1 1 1 1 1 +1 2 2 2 2 +2 1 1 1 1 +2 2 2 2 2 +2 3 3 3 3 +3 1 1 1 1 +SELECT SQL_BUFFER_RESULT a, b, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM tg GROUP BY a, b WINDOW w AS (PARTITION BY a ORDER BY b); +a b rn rnk drnk +1 1 1 1 1 +1 2 2 2 2 +2 1 1 1 1 +2 2 2 2 2 +2 3 3 3 3 +3 1 1 1 1 +EXPLAIN EXTENDED SELECT rank() OVER (ORDER BY a), rank() OVER (ORDER BY b) FROM t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +EXPLAIN EXTENDED SELECT rank() OVER (PARTITION BY max(a) ORDER BY b) FROM t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +EXPLAIN EXTENDED SELECT max(a), rank() OVER (ORDER BY b) FROM t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +EXPLAIN EXTENDED SELECT rank() OVER (ORDER BY a) FROM t1 GROUP BY a; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary; Using filesort +CREATE TABLE t2 (a INT, x INT, KEY(a)); +INSERT INTO t2 VALUES (1,10); +INSERT INTO t2 VALUES (1,20); +INSERT INTO t2 VALUES (2,20); +INSERT INTO t2 VALUES (2,30); +EXPLAIN SELECT tg.a, rank() OVER (ORDER BY tg.a) FROM tg JOIN t2 ON tg.a=t2.a GROUP BY tg.a; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 index a a 5 NULL 4 Using where; Using index; Using temporary +1 SIMPLE tg ref a a 5 test.t2.a 1 Using index +EXPLAIN SELECT a, x FROM t2 FORCE INDEX FOR GROUP BY (a) GROUP BY a; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 index NULL a 5 NULL 4 +EXPLAIN SELECT a, x, rank() OVER (ORDER BY a) FROM t2 FORCE INDEX FOR GROUP BY (a) GROUP BY a; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 index NULL a 5 NULL 4 Using temporary +EXPLAIN EXTENDED SELECT rank() OVER (ORDER BY a) FROM tg GROUP BY 1+2; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE tg index NULL a 10 NULL 6 100.00 Using index; Using temporary +EXPLAIN EXTENDED SELECT pk, a, b FROM t1 GROUP BY pk; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +EXPLAIN EXTENDED SELECT pk, a, rank() OVER (ORDER BY pk) AS rnk FROM t1 GROUP BY pk; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary; Using filesort +EXPLAIN EXTENDED SELECT pk, rank() OVER (ORDER BY a) AS x FROM t1 ORDER BY x; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary; Using filesort +CREATE FUNCTION nd(x INT) RETURNS INT NOT DETERMINISTIC +BEGIN +RETURN x; +END| +EXPLAIN FORMAT=JSON SELECT pk, a, b, rank() OVER (ORDER BY a, nd(b), pk) AS r FROM t1 ORDER BY a; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.017171443, + "filesort": { + "sort_key": "t1.a", + "window_functions_computation": { + "sorts": [ + { + "filesort": { + "sort_key": "t1.a, nd(t1.b), t1.pk" + } + } + ], + "temporary_table": { + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "ALL", + "loops": 1, + "rows": 9, + "cost": 0.011443245, + "filtered": 100 + } + } + ] + } + } + } + } +} +DROP FUNCTION nd; +DROP TABLE tg, t2; +CREATE TABLE th (a INT, b INT, KEY(a, b)); +INSERT INTO th VALUES (1,1); +INSERT INTO th VALUES (1,2); +INSERT INTO th VALUES (2,2); +INSERT INTO th VALUES (2,3); +INSERT INTO th VALUES (3,3); +INSERT INTO th VALUES (3,4); +EXPLAIN FORMAT=JSON SELECT a, b, rank() OVER (ORDER BY a) AS rnk FROM th GROUP BY a, b HAVING b > 1; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.007637811, + "nested_loop": [ + { + "table": { + "table_name": "th", + "access_type": "range", + "key": "a", + "key_length": "10", + "used_key_parts": ["a", "b"], + "loops": 1, + "rows": 6, + "cost": 0.003827362, + "filtered": 100, + "attached_condition": "th.b > 1", + "using_index_for_group_by": true + } + } + ] + } +} +SELECT a, b, rank() OVER (ORDER BY a) AS rnk FROM th GROUP BY a, b HAVING b > 1; +a b rnk +1 2 1 +2 2 2 +2 3 2 +3 3 4 +3 4 4 +EXPLAIN FORMAT=JSON SELECT a, b, rank() OVER (ORDER BY a) AS rnk FROM th GROUP BY a HAVING b > 2; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.007637811, + "having_condition": "th.b > 2", + "nested_loop": [ + { + "table": { + "table_name": "th", + "access_type": "range", + "key": "a", + "key_length": "5", + "used_key_parts": ["a"], + "loops": 1, + "rows": 6, + "cost": 0.003827362, + "filtered": 100, + "using_index_for_group_by": true + } + } + ] + } +} +SELECT a, b, rank() OVER (ORDER BY a) AS rnk FROM th GROUP BY a HAVING b > 2; +a b rnk +3 3 1 +DROP TABLE th; +EXPLAIN EXTENDED SELECT pk, a, b, count(*) OVER w AS cnt, sum(a + b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (ORDER BY b, pk ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +SELECT pk, a, b, count(*) OVER w AS cnt, sum(a + b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (ORDER BY b, pk ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW); +pk a b cnt s av mn mx +1 1 10 1 11 10.0000 10 10 +2 1 10 2 22 10.0000 10 10 +3 1 20 4 56 12.5000 10 20 +4 2 20 5 78 14.0000 10 20 +5 2 20 6 100 15.0000 10 20 +6 2 30 7 132 17.1429 10 30 +7 3 10 3 35 10.0000 10 10 +8 3 30 8 165 18.7500 10 30 +9 3 30 9 198 20.0000 10 30 +SELECT SQL_BUFFER_RESULT pk, a, b, count(*) OVER w AS cnt, sum(a + b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (ORDER BY b, pk ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW); +pk a b cnt s av mn mx +1 1 10 1 11 10.0000 10 10 +2 1 10 2 22 10.0000 10 10 +3 1 20 4 56 12.5000 10 20 +4 2 20 5 78 14.0000 10 20 +5 2 20 6 100 15.0000 10 20 +6 2 30 7 132 17.1429 10 30 +7 3 10 3 35 10.0000 10 10 +8 3 30 8 165 18.7500 10 30 +9 3 30 9 198 20.0000 10 30 +EXPLAIN EXTENDED SELECT pk, a, b, count(*) OVER w AS cnt, sum(b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (PARTITION BY a ORDER BY b, pk ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +SELECT pk, a, b, count(*) OVER w AS cnt, sum(b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (PARTITION BY a ORDER BY b, pk ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW); +pk a b cnt s av mn mx +1 1 10 1 10 10.0000 10 10 +2 1 10 2 20 10.0000 10 10 +3 1 20 3 40 13.3333 10 20 +4 2 20 1 20 20.0000 20 20 +5 2 20 2 40 20.0000 20 20 +6 2 30 3 70 23.3333 20 30 +7 3 10 1 10 10.0000 10 10 +8 3 30 2 40 20.0000 10 30 +9 3 30 3 70 23.3333 10 30 +SELECT SQL_BUFFER_RESULT pk, a, b, count(*) OVER w AS cnt, sum(b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (PARTITION BY a ORDER BY b, pk ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW); +pk a b cnt s av mn mx +1 1 10 1 10 10.0000 10 10 +2 1 10 2 20 10.0000 10 10 +3 1 20 3 40 13.3333 10 20 +4 2 20 1 20 20.0000 20 20 +5 2 20 2 40 20.0000 20 20 +6 2 30 3 70 23.3333 20 30 +7 3 10 1 10 10.0000 10 10 +8 3 30 2 40 20.0000 10 30 +9 3 30 3 70 23.3333 10 30 +EXPLAIN EXTENDED SELECT pk, a, b, count(*) OVER w AS cnt, sum(b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (ORDER BY pk); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +SELECT pk, a, b, count(*) OVER w AS cnt, sum(b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (ORDER BY pk); +pk a b cnt s av mn mx +1 1 10 1 10 10.0000 10 10 +2 1 10 2 20 10.0000 10 10 +3 1 20 3 40 13.3333 10 20 +4 2 20 4 60 15.0000 10 20 +5 2 20 5 80 16.0000 10 20 +6 2 30 6 110 18.3333 10 30 +7 3 10 7 120 17.1429 10 30 +8 3 30 8 150 18.7500 10 30 +9 3 30 9 180 20.0000 10 30 +SELECT SQL_BUFFER_RESULT pk, a, b, count(*) OVER w AS cnt, sum(b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (ORDER BY pk); +pk a b cnt s av mn mx +1 1 10 1 10 10.0000 10 10 +2 1 10 2 20 10.0000 10 10 +3 1 20 3 40 13.3333 10 20 +4 2 20 4 60 15.0000 10 20 +5 2 20 5 80 16.0000 10 20 +6 2 30 6 110 18.3333 10 30 +7 3 10 7 120 17.1429 10 30 +8 3 30 8 150 18.7500 10 30 +9 3 30 9 180 20.0000 10 30 +EXPLAIN EXTENDED SELECT pk, a, b, sum(b) OVER w AS s, max(b) OVER w AS mx FROM t1 WINDOW w AS (PARTITION BY a ORDER BY pk); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +SELECT pk, a, b, sum(b) OVER w AS s, max(b) OVER w AS mx FROM t1 WINDOW w AS (PARTITION BY a ORDER BY pk); +pk a b s mx +1 1 10 10 10 +2 1 10 20 10 +3 1 20 40 20 +4 2 20 20 20 +5 2 20 40 20 +6 2 30 70 30 +7 3 10 10 10 +8 3 30 40 30 +9 3 30 70 30 +SELECT SQL_BUFFER_RESULT pk, a, b, sum(b) OVER w AS s, max(b) OVER w AS mx FROM t1 WINDOW w AS (PARTITION BY a ORDER BY pk); +pk a b s mx +1 1 10 10 10 +2 1 10 20 10 +3 1 20 40 20 +4 2 20 20 20 +5 2 20 40 20 +6 2 30 70 30 +7 3 10 10 10 +8 3 30 40 30 +9 3 30 70 30 +EXPLAIN EXTENDED SELECT pk, a, b, sum(b) OVER w AS s, rank() OVER w AS rnk FROM t1 WINDOW w AS (ORDER BY pk); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using filesort +SELECT pk, a, b, sum(b) OVER w AS s, rank() OVER w AS rnk FROM t1 WINDOW w AS (ORDER BY pk); +pk a b s rnk +1 1 10 10 1 +2 1 10 20 2 +3 1 20 40 3 +4 2 20 60 4 +5 2 20 80 5 +6 2 30 110 6 +7 3 10 120 7 +8 3 30 150 8 +9 3 30 180 9 +SELECT SQL_BUFFER_RESULT pk, a, b, sum(b) OVER w AS s, rank() OVER w AS rnk FROM t1 WINDOW w AS (ORDER BY pk); +pk a b s rnk +1 1 10 10 1 +2 1 10 20 2 +3 1 20 40 3 +4 2 20 60 4 +5 2 20 80 5 +6 2 30 110 6 +7 3 10 120 7 +8 3 30 150 8 +9 3 30 180 9 +EXPLAIN EXTENDED SELECT pk, sum(b) OVER (ORDER BY a) FROM t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +CREATE TABLE tu ( +id INT PRIMARY KEY, +u INT NOT NULL UNIQUE, +n INT UNIQUE, +v INT +); +INSERT INTO tu VALUES (1, 100, 1, 5); +INSERT INTO tu VALUES (2, 200, NULL, 5); +INSERT INTO tu VALUES (3, 300, 3, 7); +INSERT INTO tu VALUES (4, 400, NULL, 7); +INSERT INTO tu VALUES (5, 500, 5, 9); +EXPLAIN EXTENDED SELECT id, u, sum(v) OVER w AS s, count(*) OVER w AS cnt FROM tu WINDOW w AS (ORDER BY u); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE tu ALL NULL NULL NULL NULL 5 100.00 Using filesort +SELECT id, u, sum(v) OVER w AS s, count(*) OVER w AS cnt FROM tu WINDOW w AS (ORDER BY u); +id u s cnt +1 100 5 1 +2 200 10 2 +3 300 17 3 +4 400 24 4 +5 500 33 5 +SELECT SQL_BUFFER_RESULT id, u, sum(v) OVER w AS s, count(*) OVER w AS cnt FROM tu WINDOW w AS (ORDER BY u); +id u s cnt +1 100 5 1 +2 200 10 2 +3 300 17 3 +4 400 24 4 +5 500 33 5 +EXPLAIN EXTENDED SELECT id, sum(v) OVER (ORDER BY n) FROM tu; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE tu ALL NULL NULL NULL NULL 5 100.00 Using temporary +DROP TABLE tu; +CREATE TABLE tc (x INT NOT NULL, y INT NOT NULL, v INT, PRIMARY KEY (x, y)); +INSERT INTO tc VALUES (1, 1, 10); +INSERT INTO tc VALUES (1, 2, 20); +INSERT INTO tc VALUES (2, 1, 30); +INSERT INTO tc VALUES (2, 2, 40); +INSERT INTO tc VALUES (3, 1, 50); +EXPLAIN EXTENDED SELECT x, y, sum(v) OVER w AS s, min(v) OVER w AS mn FROM tc WINDOW w AS (ORDER BY x, y); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE tc ALL NULL NULL NULL NULL 5 100.00 Using filesort +SELECT x, y, sum(v) OVER w AS s, min(v) OVER w AS mn FROM tc WINDOW w AS (ORDER BY x, y); +x y s mn +1 1 10 10 +1 2 30 10 +2 1 60 10 +2 2 100 10 +3 1 150 10 +SELECT SQL_BUFFER_RESULT x, y, sum(v) OVER w AS s, min(v) OVER w AS mn FROM tc WINDOW w AS (ORDER BY x, y); +x y s mn +1 1 10 10 +1 2 30 10 +2 1 60 10 +2 2 100 10 +3 1 150 10 +EXPLAIN EXTENDED SELECT x, sum(v) OVER (ORDER BY x) FROM tc; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE tc ALL NULL NULL NULL NULL 5 100.00 Using temporary +EXPLAIN EXTENDED SELECT x, y, sum(v) OVER w AS s FROM tc WINDOW w AS (PARTITION BY x ORDER BY y); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE tc ALL NULL NULL NULL NULL 5 100.00 Using filesort +SELECT x, y, sum(v) OVER w AS s FROM tc WINDOW w AS (PARTITION BY x ORDER BY y); +x y s +1 1 10 +1 2 30 +2 1 30 +2 2 70 +3 1 50 +SELECT SQL_BUFFER_RESULT x, y, sum(v) OVER w AS s FROM tc WINDOW w AS (PARTITION BY x ORDER BY y); +x y s +1 1 10 +1 2 30 +2 1 30 +2 2 70 +3 1 50 +DROP TABLE tc; +CREATE TABLE t2 (pk INT PRIMARY KEY, c INT); +INSERT INTO t2 VALUES (1, 10); +INSERT INTO t2 VALUES (2, 20); +INSERT INTO t2 VALUES (3, 30); +INSERT INTO t2 VALUES (4, 40); +INSERT INTO t2 VALUES (5, 50); +INSERT INTO t2 VALUES (6, 60); +INSERT INTO t2 VALUES (7, 70); +INSERT INTO t2 VALUES (8, 80); +INSERT INTO t2 VALUES (9, 90); +EXPLAIN EXTENDED SELECT t1.pk, sum(t1.b) OVER (ORDER BY t1.pk) FROM t1 JOIN t2 ON t1.pk = t2.pk; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL PRIMARY NULL NULL NULL 9 100.00 Using temporary +1 SIMPLE t2 eq_ref PRIMARY PRIMARY 4 test.t1.pk 1 100.00 Using index +DROP TABLE t2; +EXPLAIN EXTENDED SELECT sum(b) OVER (ORDER BY b) FROM t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +EXPLAIN EXTENDED SELECT count(*) OVER (ORDER BY a RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +EXPLAIN EXTENDED SELECT count(*) OVER (ORDER BY a ROWS BETWEEN UNBOUNDED PRECEDING AND 5 FOLLOWING) FROM t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +EXPLAIN EXTENDED SELECT sum(b) OVER (ORDER BY b, pk ROWS BETWEEN CURRENT ROW AND CURRENT ROW) FROM t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +EXPLAIN EXTENDED SELECT sum(b) OVER () FROM t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +EXPLAIN EXTENDED SELECT sum(b) OVER (PARTITION BY a) FROM t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 9 100.00 Using temporary +SELECT sum(DISTINCT b) OVER (ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t1; +ERROR 42000: This version of MariaDB doesn't yet support 'SUM(DISTINCT) aggregate as window function' +DROP TABLE t1; diff --git a/mysql-test/main/win_streaming.test b/mysql-test/main/win_streaming.test new file mode 100644 index 0000000000000..17e930a6b2df0 --- /dev/null +++ b/mysql-test/main/win_streaming.test @@ -0,0 +1,412 @@ +# +# Streaming Window Functions Tests +# + +# For each streamable case we run the query twice: once as-is (streaming path) +# and once with SQL_BUFFER_RESULT, which forces a temp table and so the old +# materialized path. The two must agree, which is what proves the streamed +# values are correct. +# We wrap both in --sorted_result because the streaming path emits rows in the +# window's sort order (or query's, if it's longer) while the buffered path emits them from the temp table, +# so the row order can differ even when every value matches. Sorting both and +# selecting the key columns (pk,a,b) lets us compare them as multisets. + +# Since adding --sorted_result prohibits us from testing the the actual query sorting, EXPLAIN FORMAT=JSON is used +# to show what sort key is used for the streaming path. This is used in cases where the window order is +# longer than the main query order, and when partition + order by are used in a window, to show that the sort key +# uses that of the window function and that one sort is done for the whole query. + +# I add this because EXPLAIN EXTENDED emits a Note 1003 with the reconstructed query for every +# statement, which I think is not necessary and clutters result. +--disable_warnings + +CREATE TABLE t1 (pk INT PRIMARY KEY, a INT, b INT); +INSERT INTO t1 VALUES (1, 1, 10); +INSERT INTO t1 VALUES (2, 1, 10); +INSERT INTO t1 VALUES (3, 1, 20); +INSERT INTO t1 VALUES (4, 2, 20); +INSERT INTO t1 VALUES (5, 2, 20); +INSERT INTO t1 VALUES (6, 2, 30); +INSERT INTO t1 VALUES (7, 3, 10); +INSERT INTO t1 VALUES (8, 3, 30); +INSERT INTO t1 VALUES (9, 3, 30); + +--let $q= pk, a, b, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (PARTITION BY a ORDER BY b, pk) +# FORMAT=JSON to lock the sort-key shape: the partition columns are prepended to +# the window's ORDER BY, so the single streaming sort is (a, b, pk). +--source include/explain-no-costs.inc +eval EXPLAIN FORMAT=JSON SELECT $q; +eval EXPLAIN EXTENDED SELECT $q; +eval EXPLAIN EXTENDED SELECT SQL_BUFFER_RESULT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Tests peer handling as duplicates exist when pk is not in the order list. +# row_number() is dropped here because the number assigned to a given row can differ between streaming and materialization. +# Filesort is not stable and there is no tie breaking. +--let $q= pk, a, b, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (ORDER BY a) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Order-only (no partition): all three functions, total order. +--let $q= pk, a, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM t1 WINDOW w AS (ORDER BY a, pk) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Windows reusing the main query order (whichever order is longer is used for a +# single sort). We use EXPLAIN FORMAT=JSON here to show which sort key is used. +# window order longer than main ORDER BY +--source include/explain-no-costs.inc +--let $q= pk, a, b, rank() OVER (ORDER BY a, b) AS rnk FROM t1 ORDER BY a +eval EXPLAIN FORMAT=JSON SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# main ORDER BY longer than window order +--source include/explain-no-costs.inc +--let $q= pk, a, b, rank() OVER (ORDER BY a) AS rnk FROM t1 ORDER BY a, b +eval EXPLAIN FORMAT=JSON SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# window order equals main ORDER BY +--source include/explain-no-costs.inc +--let $q= pk, a, rank() OVER (ORDER BY a) AS rnk FROM t1 ORDER BY a +eval EXPLAIN FORMAT=JSON SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Window functions inside expressions still stream as long as they're not +# aggregate functions that already require materialization. +--let $q= pk, a, rank() OVER w AS r, rank() OVER w + 1 AS r_plus, rank() OVER w - dense_rank() OVER w AS diff FROM t1 WINDOW w AS (ORDER BY a) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Multi-table joins +CREATE TABLE t2 (pk INT PRIMARY KEY, c INT); +INSERT INTO t2 VALUES (1, 300); +INSERT INTO t2 VALUES (2, 100); +INSERT INTO t2 VALUES (3, 200); +INSERT INTO t2 VALUES (4, 400); +INSERT INTO t2 VALUES (5, 600); +INSERT INTO t2 VALUES (6, 500); +INSERT INTO t2 VALUES (7, 700); +INSERT INTO t2 VALUES (8, 800); +INSERT INTO t2 VALUES (9, 900); + +--let $q= t1.pk, t1.a, t1.b, rank() OVER (ORDER BY t1.b, t1.pk) AS rnk FROM t1 JOIN t2 ON t1.pk = t2.pk +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# The window order list references columns from a table other than the first in the join, should materialize. +--let $q= t1.pk, t1.a, t2.c, rank() OVER (ORDER BY t1.a, t2.c) AS rnk FROM t1 JOIN t2 ON t1.pk = t2.pk +eval EXPLAIN EXTENDED SELECT $q; +eval SELECT $q; + +DROP TABLE t2; + +# Derived table in FROM: window functions in the outer query over a subquery. +--let $q= d.pk, d.a, d.b, rank() OVER (PARTITION BY d.a ORDER BY d.b) AS rnk, dense_rank() OVER (PARTITION BY d.a ORDER BY d.b) AS drnk FROM (SELECT pk, a, b FROM t1 WHERE a > 1) AS d +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Window function INSIDE a subquery +EXPLAIN EXTENDED SELECT d.a, d.rnk FROM (SELECT a, rank() OVER (ORDER BY a) AS rnk FROM t1) AS d; +SELECT d.a, d.rnk FROM (SELECT a, rank() OVER (ORDER BY a) AS rnk FROM t1) AS d; + +# r_rows should be equal to the limit. +--source include/analyze-format.inc +ANALYZE FORMAT=JSON SELECT pk, rank() OVER (ORDER BY pk) AS rnk FROM t1 LIMIT 2; + +# GROUP BY can stream: when the rows come out of the join already +# grouped (in the case of a single table loose index scan), and +# if the window order is compatible with the group list the window +# functions are computed on the streamed grouped rows. +# Note that this applies even if the window function order list is longer than the group list. +# As long as the group list is a prefix of the longest window order list. +CREATE TABLE tg (a INT, b INT, KEY(a, b)); +INSERT INTO tg VALUES (1, 1); +INSERT INTO tg VALUES (1, 2); +INSERT INTO tg VALUES (2, 1); +INSERT INTO tg VALUES (2, 2); +INSERT INTO tg VALUES (2, 3); +INSERT INTO tg VALUES (3, 1); + +# GROUP BY longer than the window order +--let $q= a, b, rank() OVER (ORDER BY a) AS rnk, dense_rank() OVER (ORDER BY a) AS drnk FROM tg GROUP BY a, b +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Group by shorter than the window order +--let $q= a, rank() OVER (ORDER BY a, b) AS rnk, dense_rank() OVER (ORDER BY a, b) AS drnk FROM tg GROUP BY a +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Loose index scan with partition +--let $q= a, b, row_number() OVER w AS rn, rank() OVER w AS rnk, dense_rank() OVER w AS drnk FROM tg GROUP BY a, b WINDOW w AS (PARTITION BY a ORDER BY b) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# +# Cases that fall back to materialization +# + +# Incompatible orders between the two functions +EXPLAIN EXTENDED SELECT rank() OVER (ORDER BY a), rank() OVER (ORDER BY b) FROM t1; + +# Aggregate inside the PARTITION BY list +EXPLAIN EXTENDED SELECT rank() OVER (PARTITION BY max(a) ORDER BY b) FROM t1; + +# A non-window aggregate anywhere in the select list +EXPLAIN EXTENDED SELECT max(a), rank() OVER (ORDER BY b) FROM t1; + +# GROUP BY with no usable index needs a temp table for the grouping, materialize +EXPLAIN EXTENDED SELECT rank() OVER (ORDER BY a) FROM t1 GROUP BY a; + +CREATE TABLE t2 (a INT, x INT, KEY(a)); +INSERT INTO t2 VALUES (1,10); +INSERT INTO t2 VALUES (1,20); +INSERT INTO t2 VALUES (2,20); +INSERT INTO t2 VALUES (2,30); + +# GROUP BY across a multi-table join +EXPLAIN SELECT tg.a, rank() OVER (ORDER BY tg.a) FROM tg JOIN t2 ON tg.a=t2.a GROUP BY tg.a; + +# GROUP BY that uses a tight index scan (select list is not satisfied by the index) +EXPLAIN SELECT a, x FROM t2 FORCE INDEX FOR GROUP BY (a) GROUP BY a; +EXPLAIN SELECT a, x, rank() OVER (ORDER BY a) FROM t2 FORCE INDEX FOR GROUP BY (a) GROUP BY a; + +# Implicit/constant grouping (GROUP BY a constant expression) collapses to a +# single group (grouping optimized away) and does not stream +EXPLAIN EXTENDED SELECT rank() OVER (ORDER BY a) FROM tg GROUP BY 1+2; + +# GROUP BY on a unique NOT NULL index (here the PRIMARY KEY) is optimized away: +# every group is exactly one row, so no grouping operation is performed and the +# rows go straight through end_send() with no temp table (the GROUP BY is just +# rewritten to an ORDER BY). Shown here without a window function: +EXPLAIN EXTENDED SELECT pk, a, b FROM t1 GROUP BY pk; +# Adding a window function disables that unique-index optimization for not +# so the GROUP BY is kept. (This is not yet fixed for streaming) +EXPLAIN EXTENDED SELECT pk, a, rank() OVER (ORDER BY pk) AS rnk FROM t1 GROUP BY pk; + +# Ordering the outer query by the window function value, it needs to save the value first. +EXPLAIN EXTENDED SELECT pk, rank() OVER (ORDER BY a) AS x FROM t1 ORDER BY x; + +# User defined functions / stored procedures in the order list force materialization. +# The outer sort key should be only 'a' and not 'a, nd(b), pk' +DELIMITER |; +CREATE FUNCTION nd(x INT) RETURNS INT NOT DETERMINISTIC +BEGIN + RETURN x; +END| +DELIMITER ;| + +EXPLAIN FORMAT=JSON SELECT pk, a, b, rank() OVER (ORDER BY a, nd(b), pk) AS r FROM t1 ORDER BY a; + +DROP FUNCTION nd; + +DROP TABLE tg, t2; + +CREATE TABLE th (a INT, b INT, KEY(a, b)); +INSERT INTO th VALUES (1,1); +INSERT INTO th VALUES (1,2); +INSERT INTO th VALUES (2,2); +INSERT INTO th VALUES (2,3); +INSERT INTO th VALUES (3,3); +INSERT INTO th VALUES (3,4); + +# Loose index scan is used and HAVING is pushed down to WHERE +--let $q= SELECT a, b, rank() OVER (ORDER BY a) AS rnk FROM th GROUP BY a, b HAVING b > 1 +eval EXPLAIN FORMAT=JSON $q; +eval $q; + +# Loose index scan is used and HAVING is not pushed down to WHERE +--let $q= SELECT a, b, rank() OVER (ORDER BY a) AS rnk FROM th GROUP BY a HAVING b > 2 +eval EXPLAIN FORMAT=JSON $q; +eval $q; + +DROP TABLE th; + +# +# Streamable aggregate window functions: SUM / COUNT / AVG / MIN / MAX. +# +# Aggregate window functions are streamable for ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW (which does not require removal) + +--let $q= pk, a, b, count(*) OVER w AS cnt, sum(a + b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (ORDER BY b, pk ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +--let $q= pk, a, b, count(*) OVER w AS cnt, sum(b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (PARTITION BY a ORDER BY b, pk ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + + +# Default frame (RANGE) over a unique non-NULL order for aggregate functions can stream +# It's proven no peers exist so the RANGE frame is equivalent to a ROWS frame, which does not require lookahead. +# This is only for single table queries, as uniqueness of a base table index does not imply that of the join output. + + +# Single-column PRIMARY KEY as the order +--let $q= pk, a, b, count(*) OVER w AS cnt, sum(b) OVER w AS s, avg(b) OVER w AS av, min(b) OVER w AS mn, max(b) OVER w AS mx FROM t1 WINDOW w AS (ORDER BY pk) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# PARTITION BY a with the unique order pk +--let $q= pk, a, b, sum(b) OVER w AS s, max(b) OVER w AS mx FROM t1 WINDOW w AS (PARTITION BY a ORDER BY pk) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +--let $q= pk, a, b, sum(b) OVER w AS s, rank() OVER w AS rnk FROM t1 WINDOW w AS (ORDER BY pk) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Non-unique order on the same table: peers exist, materializes. +EXPLAIN EXTENDED SELECT pk, sum(b) OVER (ORDER BY a) FROM t1; + +# A unique NOT NULL index other than the PRIMARY KEY also proves uniqueness; a +# NULLable UNIQUE index does not. +CREATE TABLE tu ( + id INT PRIMARY KEY, + u INT NOT NULL UNIQUE, + n INT UNIQUE, + v INT +); +INSERT INTO tu VALUES (1, 100, 1, 5); +INSERT INTO tu VALUES (2, 200, NULL, 5); +INSERT INTO tu VALUES (3, 300, 3, 7); +INSERT INTO tu VALUES (4, 400, NULL, 7); +INSERT INTO tu VALUES (5, 500, 5, 9); + +# Order by the NOT NULL UNIQUE column: streams. +--let $q= id, u, sum(v) OVER w AS s, count(*) OVER w AS cnt FROM tu WINDOW w AS (ORDER BY u) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Order by the NULLable UNIQUE column: not provably unique, materializes. +EXPLAIN EXTENDED SELECT id, sum(v) OVER (ORDER BY n) FROM tu; + +DROP TABLE tu; + +# Composite unique NOT NULL key: the full key in the order streams; a prefix of +# it does not; PARTITION BY + ORDER BY that together cover the key streams. +CREATE TABLE tc (x INT NOT NULL, y INT NOT NULL, v INT, PRIMARY KEY (x, y)); +INSERT INTO tc VALUES (1, 1, 10); +INSERT INTO tc VALUES (1, 2, 20); +INSERT INTO tc VALUES (2, 1, 30); +INSERT INTO tc VALUES (2, 2, 40); +INSERT INTO tc VALUES (3, 1, 50); + +# Full composite key in the order: streams. +--let $q= x, y, sum(v) OVER w AS s, min(v) OVER w AS mn FROM tc WINDOW w AS (ORDER BY x, y) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +# Prefix of the composite key only materializes. +EXPLAIN EXTENDED SELECT x, sum(v) OVER (ORDER BY x) FROM tc; + +# PARTITION BY x plus ORDER BY y together cover the composite key: streams. +--let $q= x, y, sum(v) OVER w AS s FROM tc WINDOW w AS (PARTITION BY x ORDER BY y) +eval EXPLAIN EXTENDED SELECT $q; +--sorted_result +eval SELECT $q; +--sorted_result +eval SELECT SQL_BUFFER_RESULT $q; + +DROP TABLE tc; + +# Single table only is covered, because the uniqueness of a base table order does not +# imply the uniqueness of the join output (1:N joins), this materializes. +CREATE TABLE t2 (pk INT PRIMARY KEY, c INT); +INSERT INTO t2 VALUES (1, 10); +INSERT INTO t2 VALUES (2, 20); +INSERT INTO t2 VALUES (3, 30); +INSERT INTO t2 VALUES (4, 40); +INSERT INTO t2 VALUES (5, 50); +INSERT INTO t2 VALUES (6, 60); +INSERT INTO t2 VALUES (7, 70); +INSERT INTO t2 VALUES (8, 80); +INSERT INTO t2 VALUES (9, 90); +EXPLAIN EXTENDED SELECT t1.pk, sum(t1.b) OVER (ORDER BY t1.pk) FROM t1 JOIN t2 ON t1.pk = t2.pk; +DROP TABLE t2; + +# +# Aggregate window functions that do NOT stream (still "Using temporary"). +# + +# Default-frame (RANGE) over a NON-unique order: peers exist, so a row's value +# is unknown until the peer group is scanned ahead and buffered. Materializes. +# (Contrast the unique-order default-frame cases above, which stream.) +EXPLAIN EXTENDED SELECT sum(b) OVER (ORDER BY b) FROM t1; +# Explicit RANGE, same peer-group reason. +EXPLAIN EXTENDED SELECT count(*) OVER (ORDER BY a RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t1; +# ROWS but bottom bound past CURRENT ROW needs lookahead. +EXPLAIN EXTENDED SELECT count(*) OVER (ORDER BY a ROWS BETWEEN UNBOUNDED PRECEDING AND 5 FOLLOWING) FROM t1; +# ROWS but top bound is CURRENT ROW (not UNBOUNDED PRECEDING): the frame start +# moves and removes rows, which needs random access to the departing row. +EXPLAIN EXTENDED SELECT sum(b) OVER (ORDER BY b, pk ROWS BETWEEN CURRENT ROW AND CURRENT ROW) FROM t1; + +# Those span whole partitions, the whole partition must be buffered before the row is emitted, +# hence no streaming. +EXPLAIN EXTENDED SELECT sum(b) OVER () FROM t1; +EXPLAIN EXTENDED SELECT sum(b) OVER (PARTITION BY a) FROM t1; + +# DISTINCT aggregates as window functions are not supported on either path (streaming / materialization). +--error ER_NOT_SUPPORTED_YET +SELECT sum(DISTINCT b) OVER (ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t1; + +DROP TABLE t1; + +--enable_warnings diff --git a/sql/item_sum.h b/sql/item_sum.h index 39ed79e7c0203..b019ca36e23b6 100644 --- a/sql/item_sum.h +++ b/sql/item_sum.h @@ -418,6 +418,7 @@ class Item_sum :public Item_func_or_sum Item_sum(THD *thd, Item_sum *item); enum Type type() const override { return SUM_FUNC_ITEM; } virtual enum Sumfunctype sum_func () const=0; + virtual inline bool is_streamable() const { return false; } bool is_aggr_sum_func() { switch (sum_func()) { @@ -872,6 +873,8 @@ class Item_sum_sum :public Item_sum_num, return true; } + bool is_streamable() const override { return true; } + private: void add_helper(bool perform_removal); ulonglong count; @@ -948,6 +951,8 @@ class Item_sum_count :public Item_sum_int return true; } + bool is_streamable() const override { return true; } + protected: Item *shallow_copy(THD *thd) const override { return get_item_copy(thd, this); } @@ -1007,6 +1012,8 @@ class Item_sum_avg :public Item_sum_sum return true; } + bool is_streamable() const override { return true; } + protected: Item *shallow_copy(THD *thd) const override { return get_item_copy(thd, this); } @@ -1210,6 +1217,13 @@ class Item_sum_min_max :public Item_sum_hybrid Field *create_tmp_field(MEM_ROOT *root, bool group, TABLE *table) override; void setup_caches(THD *thd) override { setup_hybrid(thd, arguments()[0], NULL); } + + /* + MIN and MAX skip the creation of a Frame_scan_cursor in the case of ROWS + BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which does not require any + removal of rows and thus is the streaming case. + */ + bool is_streamable() const override { return true; } }; diff --git a/sql/item_windowfunc.h b/sql/item_windowfunc.h index bff614372e2fc..994d85525a6c5 100644 --- a/sql/item_windowfunc.h +++ b/sql/item_windowfunc.h @@ -150,6 +150,8 @@ class Item_sum_row_number: public Item_sum_int return name; } + inline bool is_streamable() const override { return true; } + protected: Item *shallow_copy(THD *thd) const override { return get_item_copy(thd, this); } @@ -215,6 +217,8 @@ class Item_sum_rank: public Item_sum_int return name; } + inline bool is_streamable() const override { return true; } + void setup_window_func(THD *thd, Window_spec *window_spec) override; void cleanup() override @@ -290,6 +294,8 @@ class Item_sum_dense_rank: public Item_sum_int return name; } + inline bool is_streamable() const override { return true; } + void setup_window_func(THD *thd, Window_spec *window_spec) override; void cleanup() override diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index 86555657deea8..82e06d6005098 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -3793,7 +3793,7 @@ uint st_select_lex::get_cardinality_of_ref_ptrs_slice(uint order_group_num_arg) select_n_where_fields * winfunc_factor + order_group_num * 2 * winfunc_factor + hidden_bit_fields + - fields_in_window_functions + 1; + fields_in_window_functions + 1; // consider this case for streaming return n; } diff --git a/sql/sql_select.cc b/sql/sql_select.cc index b2c6929f20311..a545dd528afaa 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -26,6 +26,8 @@ */ #include "mariadb.h" +#include "my_dbug.h" +#include "sql_list.h" #include "sql_priv.h" #include "unireg.h" #include "sql_select.h" @@ -71,6 +73,9 @@ #include "opt_hints.h" #include "opt_group_by_cardinality.h" +#include "sql_window.h" +#include "item_windowfunc.h" + /* A key part number that means we're using a fulltext scan. @@ -228,6 +233,8 @@ static enum_nested_loop_state end_update(JOIN *join, JOIN_TAB *join_tab, bool end_of_records); static enum_nested_loop_state end_unique_update(JOIN *join, JOIN_TAB *join_tab, bool end_of_records); +static enum_nested_loop_state +end_compute_win_func(JOIN *join, JOIN_TAB *join_tab, bool end_of_records); static int join_read_const_table(THD *thd, JOIN_TAB *tab, POSITION *pos); static int join_read_system(JOIN_TAB *tab); @@ -283,7 +290,9 @@ static bool test_if_skip_sort_order(JOIN_TAB *tab,ORDER *order, const key_map *map, bool *fatal_error); static bool list_contains_unique_index(TABLE *table, - bool (*find_func) (Field *, void *), void *data); + bool (*find_func)(Field *, void *), + void *data, + bool ignore_table_maybe_null= false); static bool find_field_in_item_list (Field *field, void *data); static bool find_field_in_order_list (Field *field, void *data); int create_sort_index(THD *thd, JOIN *join, JOIN_TAB *tab, Filesort *fsort); @@ -1600,6 +1609,7 @@ JOIN::prepare(TABLE_LIST *tables_init, COND *conds_init, uint og_num, DBUG_RETURN(-1); thd->lex->current_select->context_analysis_place= save_place; + // this sets window functions up if (setup_without_group(thd, ref_ptrs, tables_list, select_lex->leaf_tables, fields_list, all_fields, &conds, order, group_list, @@ -2811,6 +2821,7 @@ int JOIN::optimize_stage2() ulonglong select_opts_for_readinfo; uint no_jbuf_after; JOIN_TAB *tab; + ORDER *save_order= order; DBUG_ENTER("JOIN::optimize_stage2"); if (subq_exit_fl) @@ -3330,17 +3341,62 @@ int JOIN::optimize_stage2() } } + /* + A default aggregate window uses a RANGE UNBOUNDED PRECEDING + AND CURRENT ROW frame, whose "current row" spans all peers - which needs to + lookahead and so can't stream. But if the window's order list is provably + unique and NOT NULL, no lookahead is needed and the RANGE can be treated as + ROWS instead. + + This works for queries with a single non-const table only. Uniqueness of a + base-table index does not imply uniqueness of the join output. + */ + { + List_iterator_fast wit(select_lex->window_funcs); + Item_window_func *wf; + while ((wf= wit++)) + { + Window_spec *spec= wf->window_spec; + if (wf->window_func()->is_streamable() && !wf->is_frame_prohibited() && + !spec->window_frame) + { + spec->join_partition_and_order_lists(); + spec->order_is_unique= + (table_count - const_tables == 1) && + list_contains_unique_index( + join_tab[const_tables].table, find_field_in_order_list, + (void *) spec->partition_list->first, true); + spec->disjoin_partition_and_order_lists(); + } + } + } + + /* + Checks streamability of window functions, which will be used to choose the + streaming path if a temp table is not needed for other reasons + */ + if (select_lex->n_sum_items == select_lex->window_funcs.elements && + !only_const_tables() && + have_streaming_window_funcs( + thd, select_lex->window_funcs, win_func_longest_order, order, + group_list, streaming_wf_order_is_longer, + join_tab[const_tables].table->map, const_table_map)) + streamable_window_funcs= true; + need_tmp= test_if_need_tmp_table(); /* - If window functions are present then we can't have simple_order set to - TRUE as the window function needs a temp table for computation. - ORDER BY is computed after the window function computation is done, so - the sort will be done on the temp table. + If window functions are present and not streamable, then we can't have + simple_order set to TRUE as the window function needs a temp table for + computation. In this case, ORDER BY is computed after the window function + computation is done, so the sort will be done on the temp table. */ - if (select_lex->have_window_funcs()) + if (select_lex->have_window_funcs() && !streamable_window_funcs) simple_order= FALSE; + if (!need_tmp && simple_order && streaming_wf_order_is_longer) + order= win_func_longest_order; + /* If the hint FORCE INDEX FOR ORDER BY/GROUP BY is used for the table whose columns are required to be returned in a sorted order, then @@ -3445,6 +3501,15 @@ int JOIN::optimize_stage2() } } + /* + If streaming window functions are present and test_if_need_tmp_table() is + false, then the main query order was replaced with the longest window + function order. In this case if we fall back to materialization, we need + to restore the main query order to avoid sorting redundant keys. + */ + if (need_tmp) + order= save_order; + /* Because filesort always does a full table scan or a quick range scan we must add the removed reference to the select for the table. @@ -3576,6 +3641,23 @@ int JOIN::optimize_stage2() if (make_aggr_tables_info()) DBUG_RETURN(1); + if (streamable_window_funcs && !need_tmp) + { + JOIN_TAB *last_real_tab= join_tab + exec_join_tab_cnt() - 1; + DBUG_ASSERT(last_real_tab->next_select == end_send); + + if (!(last_real_tab->window_funcs_streaming_step= + new Window_funcs_sort_streaming(thd))) + DBUG_RETURN(true); + if (last_real_tab->window_funcs_streaming_step->setup( + select_lex->window_funcs)) + DBUG_RETURN(true); + + last_real_tab->next_select= end_compute_win_func; + /* Count that we're using window functions. */ + status_var_increment(thd->status_var.feature_window_functions); + } + init_join_cache_and_keyread(); if (init_range_rowid_filters()) @@ -4327,7 +4409,7 @@ bool JOIN::make_aggr_tables_info() - duplicate value removal Both of these operations are done after window function computation step. */ - if (select_lex->window_funcs.elements) + if (select_lex->window_funcs.elements && need_tmp) { curr_tab= join_tab + total_join_tab_cnt(); if (!(curr_tab->window_funcs_step= new Window_funcs_computation)) @@ -16783,6 +16865,11 @@ void JOIN_TAB::cleanup() cache->free(); cache= 0; } + if (window_funcs_streaming_step) + { + window_funcs_streaming_step->cleanup(); + window_funcs_streaming_step= NULL; + } limit= 0; // Free select that was created for filesort outside of create_sort_index if (filesort && filesort->select && !filesort->own_select) @@ -24732,7 +24819,6 @@ sub_select(JOIN *join,JOIN_TAB *join_tab,bool end_of_records) join_tab->loosescan_key_len); skip_over= TRUE; } - error= info->read_record(); if (skip_over && likely(!error)) @@ -26038,20 +26124,24 @@ end_send(JOIN *join, JOIN_TAB *join_tab, bool end_of_records) DBUG_RETURN(NESTED_LOOP_OK); } - if (join->table_count && - join->join_tab->is_using_loose_index_scan()) + // If a window streaming step exists, then this was applied earlier already + // in end_compute_win_func() + if (!(join_tab && (join_tab - 1)->window_funcs_streaming_step != NULL)) { - /* Copy non-aggregated fields when loose index scan is used. */ - copy_fields(&join->tmp_table_param); - } - if (join->having && join->having->val_bool() == 0) - { - /* + if (join->table_count && join->join_tab->is_using_loose_index_scan()) + { + /* Copy non-aggregated fields when loose index scan is used. */ + copy_fields(&join->tmp_table_param); + } + if (join->having && join->having->val_bool() == 0) + { + /* If we have HAVING clause and it is not satisfied, we don't send the row to the client, but rownum should be incremented. - */ - join->accepted_rows++; - DBUG_RETURN(NESTED_LOOP_OK); // Didn't match having + */ + join->accepted_rows++; + DBUG_RETURN(NESTED_LOOP_OK); // Didn't match having + } } if (join->procedure) { @@ -26162,6 +26252,46 @@ end_send(JOIN *join, JOIN_TAB *join_tab, bool end_of_records) DBUG_RETURN(NESTED_LOOP_OK); } +/* + @brief + Compute streaming window functions and call end_send to send the row to the + client. + + @detail + This is attached to the last real table instead of end_send, given that: + - Window functions are streamable (see have_streaming_window_funcs) + - No temp table is needed for any other reason + - The query would have attached end_send to the last real table anyway + (incoming rows from the join loop need no further accumulation) +*/ +enum_nested_loop_state end_compute_win_func(JOIN *join, JOIN_TAB *join_tab, + bool end_of_records) +{ + DBUG_ENTER("end_compute_win_func"); + + if (!end_of_records) + { + /* + If a loose index scan is used (the only case for group by + streaming), + then a HAVING that was not pushed down should be applied before the + window functions process the rows. + */ + if (join->table_count && join->join_tab->is_using_loose_index_scan()) + { + copy_fields(&join->tmp_table_param); + } + if (join->having && join->having->val_bool() == 0) + { + join->accepted_rows++; + DBUG_RETURN(NESTED_LOOP_OK); + } + + if ((join_tab - 1)->window_funcs_streaming_step->process_row()) + DBUG_RETURN(NESTED_LOOP_ERROR); + } + + DBUG_RETURN(end_send(join, join_tab, end_of_records)); +} /* @brief @@ -27431,9 +27561,10 @@ test_if_subkey(ORDER *order, TABLE *table, uint ref, uint ref_key_parts, 0 not found. */ -static bool -list_contains_unique_index(TABLE *table, - bool (*find_func) (Field *, void *), void *data) +static bool list_contains_unique_index(TABLE *table, + bool (*find_func)(Field *, void *), + void *data, + bool ignore_table_maybe_null) { for (uint keynr= 0; keynr < table->s->keys; keynr++) { @@ -27448,8 +27579,14 @@ list_contains_unique_index(TABLE *table, key_part < key_part_end; key_part++) { - if (key_part->field->maybe_null() || - !find_func(key_part->field, data)) + /* + ignore_table_maybe_null callers only care about the column's own + declared nullability, not TABLE::maybe_null + */ + bool part_is_nullable= ignore_table_maybe_null + ? key_part->field->real_maybe_null() + : key_part->field->maybe_null(); + if (part_is_nullable || !find_func(key_part->field, data)) break; } if (key_part == key_part_end) diff --git a/sql/sql_select.h b/sql/sql_select.h index 7216938ebfd00..001a155cf48a2 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -200,6 +200,8 @@ enum join_type { JT_UNKNOWN,JT_SYSTEM,JT_CONST,JT_EQ_REF,JT_REF,JT_MAYBE_REF, class JOIN; +class Window_funcs_sort_streaming; + enum enum_nested_loop_state { NESTED_LOOP_KILLED= -2, NESTED_LOOP_ERROR= -1, @@ -533,6 +535,12 @@ typedef struct st_join_table { */ Window_funcs_computation* window_funcs_step; + /* + Non-NULL value means this join_tab (last real table) must do stream window + function computation before sending to the client. + */ + Window_funcs_sort_streaming *window_funcs_streaming_step; + /** List of topmost expressions in the select list. The *next* JOIN_TAB in the plan should use it to obtain correct values. Same applicable to @@ -1753,9 +1761,27 @@ class JOIN :public Sql_alloc */ Sql_cmd_dml *sql_cmd_dml; + /* + True if the query has window functions passing the streaming criteria, + defined by have_streaming_window_funcs() + Note: this does not guarantee they will be streamed, if the query requires + a temp table for any other reason, the window functions follow the + materialization path. + */ + bool streamable_window_funcs= false; + + /* + These are set in have_streaming_window_funcs(). + streaming_wf_order_is_longer is True if the partition + order list of the + longest window function is longer than AND compatible with the ORDER BY + clause of the main query. + */ + bool streaming_wf_order_is_longer= false; + ORDER *win_func_longest_order= NULL; + JOIN(THD *thd_arg, List &fields_arg, ulonglong select_options_arg, select_result *result_arg) - :fields_list(fields_arg) + : fields_list(fields_arg) { init(thd_arg, fields_arg, select_options_arg, result_arg); } @@ -1898,18 +1924,29 @@ class JOIN :public Sql_alloc - We are using an ORDER BY or GROUP BY on fields not in the first table - We are using different ORDER BY and GROUP BY orders - The user wants us to buffer the result. - - We are using WINDOW functions. - When the WITH ROLLUP modifier is present, we cannot skip temporary table - creation for the DISTINCT clause just because there are only const tables. + - We are using WINDOW functions that cannot be computed by streaming. + The streaming step attaches to end_send, so it is only viable when the + last next_select is end_send. We must fall back to a temp table when: + * the window functions fail the streaming criteria + (see have_streaming_window_funcs()), or + * there are no real tables to stream from (only_const_tables()), or + * the plan would run an executor-side grouping step (end_send_group) + rather than end_send: i.e. grouping was optimized away to a single + implicit group (group_optimized_away), or there is a GROUP BY not + satisfied by a loose index scan. */ bool test_if_need_tmp_table() { return ((const_tables != table_count && - ((select_distinct || !simple_order || !simple_group) || - (group_list && order) || - MY_TEST(select_options & OPTION_BUFFER_RESULT))) || + ((select_distinct || !simple_order || !simple_group) || + (group_list && order) || + MY_TEST(select_options & OPTION_BUFFER_RESULT))) || (rollup.state != ROLLUP::STATE_NONE && select_distinct) || - select_lex->have_window_funcs()); + (select_lex->have_window_funcs() && + (!streamable_window_funcs || only_const_tables() || + group_optimized_away || + (group_list && + !join_tab[const_tables].is_using_loose_index_scan())))); } bool choose_subquery_plan(table_map join_tables); void get_partial_cost_and_fanout(int end_tab_idx, diff --git a/sql/sql_window.cc b/sql/sql_window.cc index e5b4b4de7d644..329c7fd1c83af 100644 --- a/sql/sql_window.cc +++ b/sql/sql_window.cc @@ -15,14 +15,19 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "mariadb.h" +#include "mysql/plugin.h" #include "sql_parse.h" #include "sql_select.h" #include "sql_list.h" #include "item_windowfunc.h" #include "filesort.h" #include "sql_base.h" +#include "item.h" +#include #include "sql_window.h" +static ORDER *concat_order_lists(MEM_ROOT *mem_root, ORDER *list1, + ORDER *list2); bool Window_spec::check_window_names(List_iterator_fast &it) @@ -497,27 +502,28 @@ int compare_order_elements(ORDER *ord1, int weight1, return cmp > 0 ? CMP_GT : CMP_LT; } -static -int compare_order_lists(SQL_I_List *part_list1, - int spec_number1, - SQL_I_List *part_list2, - int spec_number2) +static int compare_order_lists(ORDER *list1, int spec_number1, ORDER *list2, + int spec_number2) { - if (part_list1 == part_list2) + if (!list1 && !list2) return CMP_EQ; - ORDER *elem1= part_list1->first; - ORDER *elem2= part_list2->first; - for ( ; elem1 && elem2; elem1= elem1->next, elem2= elem2->next) + if (!list1) + return CMP_LT_C; + if (!list2) + return CMP_GT_C; + ORDER *elem1= list1; + ORDER *elem2= list2; + for (; elem1 && elem2; elem1= elem1->next, elem2= elem2->next) { int cmp; // remove all constants as we don't need them for comparision - while(elem1 && ((*elem1->item)->real_item())->const_item()) + while (elem1 && ((*elem1->item)->real_item())->const_item()) { elem1= elem1->next; continue; } - while(elem2 && ((*elem2->item)->real_item())->const_item()) + while (elem2 && ((*elem2->item)->real_item())->const_item()) { elem2= elem2->next; continue; @@ -526,8 +532,8 @@ int compare_order_lists(SQL_I_List *part_list1, if (!elem1 || !elem2) break; - if ((cmp= compare_order_elements(elem1, spec_number1, - elem2, spec_number2))) + if ((cmp= + compare_order_elements(elem1, spec_number1, elem2, spec_number2))) return cmp; } if (elem1) @@ -537,6 +543,14 @@ int compare_order_lists(SQL_I_List *part_list1, return CMP_EQ; } +static int compare_order_lists(SQL_I_List *part_list1, int spec_number1, + SQL_I_List *part_list2, int spec_number2) +{ + if (part_list1 == part_list2) + return CMP_EQ; + return compare_order_lists(part_list1->first, spec_number1, + part_list2->first, spec_number2); +} static int compare_window_frame_bounds(Window_frame_bound *win_frame_bound1, @@ -781,6 +795,187 @@ void order_window_funcs_by_window_specs(List *win_func_list) } } +/* + Returns true for ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. + + This is the only frame for which the set of rows in the frame grows by one + per row and never shrinks. +*/ +static inline bool frame_is_streaming_compatible(Window_spec *win_spec) +{ + Window_frame *frame= win_spec->window_frame; + // This means a RANGE frame by default for aggregate functions. + if (!frame || frame->units != Window_frame::Frame_units::UNITS_ROWS) + return false; + + if (!(frame->top_bound->precedence_type == Window_frame_bound::PRECEDING && + frame->top_bound->is_unbounded())) + return false; + return (frame->bottom_bound->precedence_type == Window_frame_bound::CURRENT); +} + +static Item_window_func * +find_longest_compatible_order(const List &win_funcs) +{ + if (win_funcs.elements == 0) + return nullptr; + int longest_order_elements= -1; + Item_window_func *longest, *win_func; + List tmp_win_funcs= win_funcs; + List_iterator_fast it(tmp_win_funcs); + while ((win_func= it++)) + { + Window_spec *spec= win_func->window_spec; + int win_func_order_elements= + spec->partition_list->elements + spec->order_list->elements; + if (win_func_order_elements > longest_order_elements) + { + longest_order_elements= win_func_order_elements; + longest= win_func; + } + } + it.rewind(); + + Window_spec *longest_spec= longest->window_spec; + longest_spec->join_partition_and_order_lists(); + + // Check compatibility with other window function frames + while ((win_func= it++)) + { + if (win_func == longest) + continue; + Window_spec *spec= win_func->window_spec; + spec->join_partition_and_order_lists(); + int cmp= compare_order_lists(longest_spec->partition_list, + longest_spec->win_spec_number, + spec->partition_list, spec->win_spec_number); + spec->disjoin_partition_and_order_lists(); + if (!(cmp == CMP_EQ || cmp == CMP_GT_C)) + { + longest= nullptr; + break; + } + } + longest_spec->disjoin_partition_and_order_lists(); + return longest; +} + +/* + Decide whether all window functions in the SELECT can be computed in a single + streaming pass over the join output (no temporary table), and if so work out + the sort order that pass must use. + + Returns true if: + + 1. All window specs share one most-specific ordering: one window's + PARTITION BY + ORDER BY is a prefix-compatible superset of every other's + (find_longest_compatible_order()). That longest order becomes the sort + key. + + 2. Every function is streamable: + - window_func()->is_streamable() (ROW_NUMBER, RANK, DENSE_RANK and the + running aggregates SUM, COUNT, AVG, MIN, MAX), + - it is not a DISTINCT aggregate (the streaming path uses the SIMPLE + aggregator and cannot deduplicate), and + + 3. The frame is prohibited or streamable: for aggregates it must be exactly + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + (frame_is_streaming_compatible()). + + 4. The longest window order is compatible (equal, or one a prefix of the + other) with the main query ORDER BY, and with the GROUP BY list when + present, so a single sort satisfies both the window(s) and the query. + + 5. If a window order is longer than the main query order, then it + only references the first real table in the JOIN or const tables, is + deterministic and has no sum functions. (like how remove_const() does + for the main query ORDER BY). + + On success it sets: + longest_wf_order - the order the streaming sort should use. + streaming_wf_order_is_longer - true if that order extends the main query + ORDER BY (so it replaces it as the sort + key). +*/ +bool have_streaming_window_funcs(THD *thd, List &win_funcs, + ORDER *&longest_wf_order, + ORDER *main_query_order, + ORDER *main_query_group_list, + bool &streaming_wf_order_is_longer, + table_map first_table_map, + table_map const_table_map) +{ + if (win_funcs.elements == 0) + return false; + + Item_window_func *win_func_with_longest_order= + find_longest_compatible_order(win_funcs); + if (!win_func_with_longest_order) + return false; + + List_iterator_fast it(win_funcs); + Item_window_func *win_func; + int cmp; + + while ((win_func= it++)) + { + Window_spec *spec= win_func->window_spec; + Item_sum *sum_func= win_func->window_func(); + if (!sum_func->is_streamable() || sum_func->has_with_distinct() || + (!win_func->is_frame_prohibited() && !spec->order_is_unique && + !frame_is_streaming_compatible(spec))) + return false; + } + + longest_wf_order= concat_order_lists( + thd->mem_root, + win_func_with_longest_order->window_spec->partition_list->first, + win_func_with_longest_order->window_spec->order_list->first); + + cmp= compare_order_lists( + longest_wf_order, + win_func_with_longest_order->window_spec->win_spec_number, + main_query_order, -1); + + if (!(CMP_LT_C <= cmp && cmp <= CMP_GT_C)) + return false; + if (cmp == CMP_GT_C) + streaming_wf_order_is_longer= true; + else + streaming_wf_order_is_longer= false; + + if (streaming_wf_order_is_longer) + { + for (ORDER *o= longest_wf_order; o; o= o->next) + { + table_map used= (*o->item)->used_tables() & ~const_table_map; + if ((used & ~first_table_map) || + (used & (RAND_TABLE_BIT | OUTER_REF_TABLE_BIT)) || + (*o->item)->with_sum_func()) + return false; + } + } + + /* + Ordering keys after the complete GROUP BY key does not affect the ordering + of the grouped result: there is exactly one row per group key, so a + trailing key can never be reached as a tie-breaker. Hence it is safe to + drop the trailing keys even if the window function references non-grouped + columns, whose values are plan-dependent but cannot affect the ordering + between grouped rows. (Assumes the whole GROUP BY key is matched as a + prefix, and no WITH ROLLUP.) + */ + if (main_query_group_list) + { + cmp= compare_order_lists( + longest_wf_order, + win_func_with_longest_order->window_spec->win_spec_number, + main_query_group_list, -1); + if (!(CMP_LT_C <= cmp && cmp <= CMP_GT_C)) + return false; + } + return true; +} ///////////////////////////////////////////////////////////////////////////// @@ -1258,8 +1453,6 @@ class Cursor_manager List cursors; }; - - ////////////////////////////////////////////////////////////////////////////// // RANGE-type frames ////////////////////////////////////////////////////////////////////////////// @@ -2433,7 +2626,8 @@ class Frame_positional_cursor : public Frame_cursor /* Get a Frame_cursor for a frame bound. This is a "factory function". */ -Frame_cursor *get_frame_cursor(THD *thd, Window_spec *spec, bool is_top_bound) +Frame_cursor *get_frame_cursor(THD *thd, Window_spec *spec, bool is_top_bound, + bool for_streaming= false) { Window_frame *frame= spec->window_frame; if (!frame) @@ -2460,10 +2654,16 @@ Frame_cursor *get_frame_cursor(THD *thd, Window_spec *spec, bool is_top_bound) return new Frame_unbounded_preceding(thd, spec->partition_list, spec->order_list); - else - return new Frame_range_current_row_bottom(thd, - spec->partition_list, - spec->order_list); + /* + When streaming and the order is provably unique (no peers), the default + RANGE UNBOUNDED PRECEDING AND CURRENT ROW frame is equivalent to the ROWS + frame. We use it instead for streaming so as to not call + walk_till_non_peer(). + */ + if (for_streaming && spec->order_is_unique) + return new Frame_rows_current_row_bottom; + return new Frame_range_current_row_bottom(thd, spec->partition_list, + spec->order_list); } Window_frame_bound *bound= is_top_bound? frame->top_bound : @@ -2700,9 +2900,8 @@ static bool is_computed_with_remove(Item_sum::Sumfunctype sum_func) those window functions will be registered to the same cursor. */ bool get_window_functions_required_cursors( - THD *thd, - List& window_functions, - List *cursor_managers) + THD *thd, List &window_functions, + List *cursor_managers, bool for_streaming= false) { List_iterator_fast it(window_functions); Item_window_func* item_win_func; @@ -2759,8 +2958,8 @@ bool get_window_functions_required_cursors( continue; } - Frame_cursor *frame_bottom= get_frame_cursor(thd, - item_win_func->window_spec, false); + Frame_cursor *frame_bottom= get_frame_cursor( + thd, item_win_func->window_spec, false, for_streaming); Frame_cursor *frame_top= get_frame_cursor(thd, item_win_func->window_spec, true); @@ -2776,7 +2975,7 @@ bool get_window_functions_required_cursors( cursor_manager->add_cursor(frame_bottom); cursor_manager->add_cursor(frame_top); if (is_computed_with_remove(sum_func->sum_func()) && - !sum_func->supports_removal()) + !sum_func->supports_removal() && !for_streaming) { frame_bottom->set_no_action(); frame_top->set_no_action(); @@ -3065,6 +3264,7 @@ bool Window_func_runner::exec(THD *thd, TABLE *tbl, SORT_INFO *filesort_result) Item_window_func *win_func; while ((win_func= it++)) { + // i need this so it reads live not from result_field win_func->set_phase_to_computation(); // TODO(cvicentiu) Setting the aggregator should probably be done during // setup of Window_funcs_sort. @@ -3073,6 +3273,7 @@ bool Window_func_runner::exec(THD *thd, TABLE *tbl, SORT_INFO *filesort_result) } it.rewind(); + // i would skip this now List cursor_managers; if (get_window_functions_required_cursors(thd, window_functions, &cursor_managers)) @@ -3123,7 +3324,7 @@ bool Window_funcs_sort::setup(THD *thd, SQL_SELECT *sel, JOIN_TAB *join_tab) { Window_spec *spec; - Item_window_func *win_func= it.peek(); + Item_window_func *win_func= it.peek(); Item_window_func *win_func_with_longest_order= NULL; int longest_order_elements= -1; @@ -3153,6 +3354,8 @@ bool Window_funcs_sort::setup(THD *thd, SQL_SELECT *sel, in a way that the result is valid for all window functions belonging to this Window_funcs_sort. */ + // all this i should have done earlier for streaming (on base table, or + // reusing the main query order (for later)) spec= win_func_with_longest_order->window_spec; ORDER* sort_order= concat_order_lists(thd->mem_root, @@ -3254,6 +3457,80 @@ void Window_funcs_computation::cleanup() } } +bool Window_funcs_sort_streaming::setup(List &window_funcs) +{ + order_window_funcs_by_window_specs(&window_funcs); + + List_iterator_fast it(window_funcs); + Item_window_func *win_func; + if (get_window_functions_required_cursors(thd, window_funcs, + &cursor_managers, true)) + return true; + + Group_bound_tracker *tracker; + while ((win_func= it++)) + { + tracker= + new Group_bound_tracker(thd, win_func->window_spec->partition_list); + tracker->init(); + partition_trackers.push_back(tracker); + + // So that end_send gets the live value of the window function on calling + // val_*(), and not the value from result_field. + win_func->set_phase_to_computation(); + + // sets peer tracker inside rank() + Item_sum *sum_func= win_func->window_func(); + sum_func->setup_window_func(thd, win_func->window_spec); + + // for handling aggregate functions (not done yet, still need to define + // frame for those). + win_func->window_func()->set_aggregator(thd, + Aggregator::SIMPLE_AGGREGATOR); + } + this->win_funcs= window_funcs; // internal variable points to the list + return false; +} + +bool Window_funcs_sort_streaming::process_row() +{ + List_iterator_fast iter_win_funcs(win_funcs); + List_iterator_fast iter_part_trackers( + partition_trackers); + List_iterator_fast iter_cursor_managers(cursor_managers); + Item_window_func *win_func; + Cursor_manager *cursor_manager; + Group_bound_tracker *tracker; + // i copied this for now from compute_window_func + while ((win_func= iter_win_funcs++) && (tracker= iter_part_trackers++) && + (cursor_manager= iter_cursor_managers++)) + { + if (tracker->check_if_next_group() || (rownum == 0)) + { + /* TODO(cvicentiu) + Clearing window functions should happen through cursors. */ + win_func->window_func()->clear(); + cursor_manager->notify_cursors_partition_changed(rownum); + } + else + { + cursor_manager->notify_cursors_next_row(); + } + + /* Check if we found any error in the window function while adding values + through cursors. */ + if (unlikely(thd->is_error() || thd->is_killed())) + return true; + } + rownum++; + return false; +} + +void Window_funcs_sort_streaming::cleanup() +{ + cursor_managers.delete_elements(); + partition_trackers.delete_elements(); +} Explain_aggr_window_funcs* Window_funcs_computation::save_explain_plan(MEM_ROOT *mem_root, diff --git a/sql/sql_window.h b/sql/sql_window.h index 7009b8895a667..aa38b601393b1 100644 --- a/sql/sql_window.h +++ b/sql/sql_window.h @@ -20,7 +20,10 @@ #include "filesort.h" class Item_window_func; - +class Item_sum; +class Group_bound_tracker; +class Frame_cursor; +class Cursor_manager; /* Window functions module. @@ -127,12 +130,23 @@ class Window_spec : public Sql_alloc */ int win_spec_number; + /* + True when, for a single-table query, this window's order list covers a + unique NOT-NULL index of that table. Then no two rows are peers, so the + default / RANGE UNBOUNDED PRECEDING AND CURRENT ROW frame is equivalent to + the ROWS frame and can be streamed. + This is used for aggregate functions. + */ + bool order_is_unique; + Window_spec(LEX_CSTRING *win_ref, SQL_I_List *part_list, SQL_I_List *ord_list, Window_frame *win_frame) - : window_names_are_checked(false), window_ref(win_ref), - partition_list(part_list), save_partition_list(NULL), - order_list(ord_list), save_order_list(NULL), - window_frame(win_frame), referenced_win_spec(NULL) {} + : window_names_are_checked(false), window_ref(win_ref), + partition_list(part_list), save_partition_list(NULL), + order_list(ord_list), save_order_list(NULL), window_frame(win_frame), + referenced_win_spec(NULL), order_is_unique(false) + { + } virtual const Lex_ident_window name() { return Lex_ident_window(); } @@ -181,6 +195,13 @@ int setup_windows(THD *thd, Ref_ptr_array ref_pointer_array, TABLE_LIST *tables, List &fields, List &all_fields, List &win_specs, List &win_funcs); +bool have_streaming_window_funcs(THD *thd, List &win_funcs, + ORDER *&longest_wf_order, + ORDER *main_query_order, + ORDER *main_query_group_list, + bool &streaming_wf_order_is_longer, + table_map first_table_map, + table_map const_table_map); ////////////////////////////////////////////////////////////////////////////// // Classes that make window functions computation a part of SELECT's query plan @@ -256,5 +277,27 @@ class Window_funcs_computation : public Sql_alloc void cleanup(); }; +class Window_funcs_sort_streaming : public Sql_alloc +{ +public: + Window_funcs_sort_streaming(THD *thd) : thd(thd) {} + bool setup(List &win_funcs); + /* + The object is attached to the last real JOIN_TAB in the query. This + function is called by end_compute_win_func() to run the window functions + computation over the current row in the JOIN output, assuming the row sits + in TABLE::record[0]. Then end_send calls val_*() methods of the window + functions to retrieve the live computed values and sends the row to output. + */ + bool process_row(); + void cleanup(); + +private: + int rownum= 0; // Internal state for process row + THD *thd= nullptr; + List win_funcs; + List cursor_managers; + List partition_trackers; +}; #endif /* SQL_WINDOW_INCLUDED */