Skip to content

Commit 1e6b5eb

Browse files
committed
Allow WHERE terms to be pushed down into sub-queries that contain window functions, provided that the WHERE term is made up of entirely of constants and copies of expressions found in the PARTITION BY clauses of all window functions in the sub-query.
1 parent 447bbf4 commit 1e6b5eb

File tree

4 files changed

+232
-15
lines changed

4 files changed

+232
-15
lines changed

src/select.c

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4489,6 +4489,35 @@ static int propagateConstants(
44894489
return nChng;
44904490
}
44914491

4492+
#if !defined(SQLITE_OMIT_SUBQUERY) || !defined(SQLITE_OMIT_VIEW)
4493+
# if !defined(SQLITE_OMIT_WINDOWFUNC)
4494+
/*
4495+
** This function is called to determine whether or not it is safe to
4496+
** push WHERE clause expression pExpr down to FROM clause sub-query
4497+
** pSubq, which contains at least one window function. Return 1
4498+
** if it is safe and the expression should be pushed down, or 0
4499+
** otherwise.
4500+
**
4501+
** It is only safe to push the expression down if it consists only
4502+
** of constants and copies of expressions that appear in the PARTITION
4503+
** BY clause of all window function used by the sub-query. It is safe
4504+
** to filter out entire partitions, but not rows within partitions, as
4505+
** this may change the results of the window functions.
4506+
**
4507+
** At the time this function is called it is guaranteed that
4508+
**
4509+
** * the sub-query uses only one distinct window frame, and
4510+
** * that the window frame has a PARTITION BY clase.
4511+
*/
4512+
static int pushDownWindowCheck(Parse *pParse, Select *pSubq, Expr *pExpr){
4513+
assert( pSubq->pWin->pPartition );
4514+
assert( (pSubq->selFlags & SF_MultiPart)==0 );
4515+
assert( pSubq->pPrior==0 );
4516+
return sqlite3ExprIsConstantOrGroupBy(pParse, pExpr, pSubq->pWin->pPartition);
4517+
}
4518+
# endif /* SQLITE_OMIT_WINDOWFUNC */
4519+
#endif /* !defined(SQLITE_OMIT_SUBQUERY) || !defined(SQLITE_OMIT_VIEW) */
4520+
44924521
#if !defined(SQLITE_OMIT_SUBQUERY) || !defined(SQLITE_OMIT_VIEW)
44934522
/*
44944523
** Make copies of relevant WHERE clause terms of the outer query into
@@ -4536,9 +4565,20 @@ static int propagateConstants(
45364565
** But if the (b2=2) term were to be pushed down into the bb subquery,
45374566
** then the (1,1,NULL) row would be suppressed.
45384567
**
4539-
** (6) The inner query features one or more window-functions (since
4540-
** changes to the WHERE clause of the inner query could change the
4541-
** window over which window functions are calculated).
4568+
** (6) Window functions make things tricky as changes to the WHERE clause
4569+
** of the inner query could change the window over which window
4570+
** functions are calculated. Therefore, do not attempt the optimization
4571+
** if:
4572+
**
4573+
** (6a) The inner query uses multiple incompatible window partitions.
4574+
**
4575+
** (6b) The inner query is a compound and uses window-functions.
4576+
**
4577+
** (6c) The WHERE clause does not consist entirely of constants and
4578+
** copies of expressions found in the PARTITION BY clause of
4579+
** all window-functions used by the sub-query. It is safe to
4580+
** filter out entire partitions, as this does not change the
4581+
** window over which any window-function is calculated.
45424582
**
45434583
** (7) The inner query is a Common Table Expression (CTE) that should
45444584
** be materialized. (This restriction is implemented in the calling
@@ -4556,13 +4596,17 @@ static int pushDownWhereTerms(
45564596
){
45574597
Expr *pNew;
45584598
int nChng = 0;
4559-
Select *pSel;
45604599
if( pWhere==0 ) return 0;
4561-
if( pSubq->selFlags & SF_Recursive ) return 0; /* restriction (2) */
4600+
if( pSubq->selFlags & (SF_Recursive|SF_MultiPart) ) return 0;
45624601

45634602
#ifndef SQLITE_OMIT_WINDOWFUNC
4564-
for(pSel=pSubq; pSel; pSel=pSel->pPrior){
4565-
if( pSel->pWin ) return 0; /* restriction (6) */
4603+
if( pSubq->pPrior ){
4604+
Select *pSel;
4605+
for(pSel=pSubq; pSel; pSel=pSel->pPrior){
4606+
if( pSel->pWin ) return 0; /* restriction (6b) */
4607+
}
4608+
}else{
4609+
if( pSubq->pWin && pSubq->pWin->pPartition==0 ) return 0;
45664610
}
45674611
#endif
45684612

@@ -4609,6 +4653,14 @@ static int pushDownWhereTerms(
46094653
x.isLeftJoin = 0;
46104654
x.pEList = pSubq->pEList;
46114655
pNew = substExpr(&x, pNew);
4656+
#ifndef SQLITE_OMIT_WINDOWFUNC
4657+
if( pSubq->pWin && 0==pushDownWindowCheck(pParse, pSubq, pNew) ){
4658+
/* Restriction 6c has prevented push-down in this case */
4659+
sqlite3ExprDelete(pParse->db, pNew);
4660+
nChng--;
4661+
break;
4662+
}
4663+
#endif
46124664
if( pSubq->selFlags & SF_Aggregate ){
46134665
pSubq->pHaving = sqlite3ExprAnd(pParse, pSubq->pHaving, pNew);
46144666
}else{

src/sqliteInt.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3204,6 +3204,7 @@ struct Select {
32043204
#define SF_NoopOrderBy 0x0400000 /* ORDER BY is ignored for this query */
32053205
#define SF_UpdateFrom 0x0800000 /* Statement is an UPDATE...FROM */
32063206
#define SF_PushDown 0x1000000 /* SELECT has be modified by push-down opt */
3207+
#define SF_MultiPart 0x2000000 /* Has multiple incompatible PARTITIONs */
32073208

32083209
/*
32093210
** The results of a SELECT can be distributed in several ways, as defined

src/window.c

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,15 +1304,19 @@ void sqlite3WindowAttach(Parse *pParse, Expr *p, Window *pWin){
13041304
** SELECT, or (b) the windows already linked use a compatible window frame.
13051305
*/
13061306
void sqlite3WindowLink(Select *pSel, Window *pWin){
1307-
if( pSel!=0
1308-
&& (0==pSel->pWin || 0==sqlite3WindowCompare(0, pSel->pWin, pWin, 0))
1309-
){
1310-
pWin->pNextWin = pSel->pWin;
1311-
if( pSel->pWin ){
1312-
pSel->pWin->ppThis = &pWin->pNextWin;
1307+
if( pSel ){
1308+
if( 0==pSel->pWin || 0==sqlite3WindowCompare(0, pSel->pWin, pWin, 0) ){
1309+
pWin->pNextWin = pSel->pWin;
1310+
if( pSel->pWin ){
1311+
pSel->pWin->ppThis = &pWin->pNextWin;
1312+
}
1313+
pSel->pWin = pWin;
1314+
pWin->ppThis = &pSel->pWin;
1315+
}else{
1316+
if( sqlite3ExprListCompare(pWin->pPartition, pSel->pWin->pPartition,-1) ){
1317+
pSel->selFlags |= SF_MultiPart;
1318+
}
13131319
}
1314-
pSel->pWin = pWin;
1315-
pWin->ppThis = &pSel->pWin;
13161320
}
13171321
}
13181322

test/windowpushd.test

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# 2021 February 23
2+
#
3+
# The author disclaims copyright to this source code. In place of
4+
# a legal notice, here is a blessing:
5+
#
6+
# May you do good and not evil.
7+
# May you find forgiveness for yourself and forgive others.
8+
# May you share freely, never taking more than you give.
9+
#
10+
#***********************************************************************
11+
# This file implements regression tests for SQLite library. The
12+
# focus of this file is testing the push-down optimization when
13+
# WHERE constraints are pushed down into a sub-query that uses
14+
# window functions.
15+
#
16+
17+
set testdir [file dirname $argv0]
18+
source $testdir/tester.tcl
19+
set testprefix windowpushd
20+
21+
do_execsql_test 1.0 {
22+
CREATE TABLE t1(id INTEGER PRIMARY KEY, grp_id);
23+
CREATE INDEX i1 ON t1(grp_id);
24+
CREATE VIEW lll AS SELECT
25+
row_number() OVER (PARTITION BY grp_id),
26+
grp_id, id
27+
FROM t1
28+
}
29+
30+
do_execsql_test 1.1 {
31+
INSERT INTO t1 VALUES
32+
(1, 2), (2, 3), (3, 3), (4, 1), (5, 1),
33+
(6, 1), (7, 1), (8, 1), (9, 3), (10, 3),
34+
(11, 2), (12, 3), (13, 3), (14, 2), (15, 1),
35+
(16, 2), (17, 1), (18, 2), (19, 3), (20, 2)
36+
}
37+
38+
do_execsql_test 1.2 {
39+
SELECT * FROM lll
40+
} {
41+
1 1 4 2 1 5 3 1 6 4 1 7 5 1 8 6 1 15 7 1 17
42+
1 2 1 2 2 11 3 2 14 4 2 16 5 2 18 6 2 20
43+
1 3 2 2 3 3 3 3 9 4 3 10 5 3 12 6 3 13 7 3 19
44+
}
45+
46+
do_execsql_test 1.3 {
47+
SELECT * FROM lll WHERE grp_id=2
48+
} {
49+
1 2 1 2 2 11 3 2 14 4 2 16 5 2 18 6 2 20
50+
}
51+
52+
do_eqp_test 1.4 {
53+
SELECT * FROM lll WHERE grp_id=2
54+
} {SEARCH TABLE t1 USING COVERING INDEX i1 (grp_id=?)}
55+
56+
#-------------------------------------------------------------------------
57+
reset_db
58+
do_execsql_test 2.0 {
59+
CREATE TABLE t1(a, b, c, d);
60+
INSERT INTO t1 VALUES('A', 'C', 1, 0.1);
61+
INSERT INTO t1 VALUES('A', 'D', 2, 0.2);
62+
INSERT INTO t1 VALUES('A', 'E', 3, 0.3);
63+
INSERT INTO t1 VALUES('A', 'C', 4, 0.4);
64+
INSERT INTO t1 VALUES('B', 'D', 5, 0.5);
65+
INSERT INTO t1 VALUES('B', 'E', 6, 0.6);
66+
INSERT INTO t1 VALUES('B', 'C', 7, 0.7);
67+
INSERT INTO t1 VALUES('B', 'D', 8, 0.8);
68+
INSERT INTO t1 VALUES('C', 'E', 9, 0.9);
69+
INSERT INTO t1 VALUES('C', 'C', 10, 1.0);
70+
INSERT INTO t1 VALUES('C', 'D', 11, 1.1);
71+
INSERT INTO t1 VALUES('C', 'E', 12, 1.2);
72+
73+
CREATE INDEX i1 ON t1(a);
74+
CREATE INDEX i2 ON t1(b);
75+
76+
CREATE VIEW v1 AS SELECT a, c, max(c) OVER (PARTITION BY a) FROM t1;
77+
78+
CREATE VIEW v2 AS SELECT a, c,
79+
max(c) OVER (PARTITION BY a),
80+
row_number() OVER ()
81+
FROM t1;
82+
83+
CREATE VIEW v3 AS SELECT b, d,
84+
max(d) OVER (PARTITION BY b),
85+
row_number() OVER (PARTITION BY b)
86+
FROM t1;
87+
}
88+
89+
foreach tn {0 1} {
90+
optimization_control db push-down $tn
91+
92+
do_execsql_test 2.$tn.1.1 {
93+
SELECT * FROM v1;
94+
} {
95+
A 1 4 A 2 4 A 3 4 A 4 4
96+
B 5 8 B 6 8 B 7 8 B 8 8
97+
C 9 12 C 10 12 C 11 12 C 12 12
98+
}
99+
100+
do_execsql_test 2.$tn.1.2 {
101+
SELECT * FROM v1 WHERE a IN ('A', 'B');
102+
} {
103+
A 1 4 A 2 4 A 3 4 A 4 4
104+
B 5 8 B 6 8 B 7 8 B 8 8
105+
}
106+
107+
do_execsql_test 2.$tn.1.3 {
108+
SELECT * FROM v1 WHERE a IS 'C'
109+
} {
110+
C 9 12 C 10 12 C 11 12 C 12 12
111+
}
112+
113+
if {$tn==1} {
114+
do_eqp_test 2.$tn.1.4 {
115+
SELECT * FROM v1 WHERE a IN ('A', 'B');
116+
} {USING INDEX i1 (a=?)}
117+
118+
do_eqp_test 2.$tn.1.5 {
119+
SELECT * FROM v1 WHERE a = 'c' COLLATE nocase
120+
} {USING INDEX i1}
121+
}
122+
123+
do_execsql_test 2.$tn.2.1 {
124+
SELECT * FROM v2;
125+
} {
126+
A 1 4 1 A 2 4 2 A 3 4 3 A 4 4 4
127+
B 5 8 5 B 6 8 6 B 7 8 7 B 8 8 8
128+
C 9 12 9 C 10 12 10 C 11 12 11 C 12 12 12
129+
}
130+
131+
do_execsql_test 2.$tn.2.2 {
132+
SELECT * FROM v2 WHERE a = 'C';
133+
} {
134+
C 9 12 9 C 10 12 10 C 11 12 11 C 12 12 12
135+
}
136+
137+
do_execsql_test 2.$tn.3.1 { SELECT * FROM v3; } {
138+
C 0.1 1.0 1 C 0.4 1.0 2 C 0.7 1.0 3 C 1.0 1.0 4
139+
D 0.2 1.1 1 D 0.5 1.1 2 D 0.8 1.1 3 D 1.1 1.1 4
140+
E 0.3 1.2 1 E 0.6 1.2 2 E 0.9 1.2 3 E 1.2 1.2 4
141+
}
142+
143+
do_execsql_test 2.$tn.3.2 { SELECT * FROM v3 WHERE b<'E' } {
144+
C 0.1 1.0 1 C 0.4 1.0 2 C 0.7 1.0 3 C 1.0 1.0 4
145+
D 0.2 1.1 1 D 0.5 1.1 2 D 0.8 1.1 3 D 1.1 1.1 4
146+
}
147+
148+
if {$tn==1} {
149+
do_eqp_test 2.$tn.3.3 {
150+
SELECT * FROM v3 WHERE b='E'
151+
} {USING INDEX i2 (b=?)}
152+
}
153+
154+
}
155+
156+
157+
158+
159+
finish_test
160+

0 commit comments

Comments
 (0)