1
0
mirror of https://github.com/sqlite/sqlite.git synced 2026-10-23 21:49:02 +03:00

Avoid excessive reprepares when there is an unindexed "col=?" constraint on a join query. This should only be reprepared when the bound value toggles between integer values 0 or 1 and any other value. Report [bugs:/info/2026-10-11T16:06:46Z | 2026-10-11T16:06:46Z].

FossilOrigin-Name: 7f154192db5a3a79f4666edd31dfc29fd73904ccd3630952e3b032e49fee6455
This commit is contained in:
dan
2026-10-12 13:36:00 +00:00
parent eb61c5229b
commit 48fedaa403
6 changed files with 154 additions and 31 deletions

View File

@@ -1,5 +1,5 @@
C When\sconfiguring\swith\s--editline,\sbe\ssure\sto\sdefine\sHAVE_EDITLINE.\sAddresses\s[forum:8b9b30f6f4|forum\spost\s2026-10-12T12:33:03Z].
D 2026-10-12T12:48:17.427
C Avoid\sexcessive\sreprepares\swhen\sthere\sis\san\sunindexed\s"col=?"\sconstraint\son\sa\sjoin\squery.\sThis\sshould\sonly\sbe\sreprepared\swhen\sthe\sbound\svalue\stoggles\sbetween\sinteger\svalues\s0\sor\s1\sand\sany\sother\svalue.\sReport\s[bugs:/info/2026-10-11T16:06:46Z\s|\s2026-10-11T16:06:46Z].
D 2026-10-12T13:36:00.777
F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
@@ -752,7 +752,7 @@ F src/date.c 080641f59097d14fc7341f8bc0458d273b3a62d8381916db8ccbc89c2aae07b3
F src/dbpage.c ec8590c5e7a564cef28ca3c6156521316a4d26cb572a34841b0bcb26b67b3f97
F src/dbstat.c 34d4e825fbf763640ce3dbc56910ab080495ef4a6efb48b2b1852a43d43763fc
F src/delete.c 59eeca3fb88c29329afc41bb803ee568b120d9dd7470b5f38ab55cc38390b451
F src/expr.c 439921b2b411ca54176f7f069af93743fe587194f6a17032bbb8fa93cf201b27
F src/expr.c fd606b947a176f3a4567e41a80f535e0260822a94027c3d493481648895351e5
F src/fault.c 460f3e55994363812d9d60844b2a6de88826e007
F src/fkey.c 26788ae6c785d70f9829c4176e9764447014fc73e6ba31e9f1881720a64ae7f1
F src/func.c 54d96eeafcb6ec4cff92028ea0f7bb09e27b375b248d969aeb80a61e560ef302
@@ -871,8 +871,8 @@ F src/vacuum.c d3d35d8ae893d419ade5fa196d761a83bddcbb62137a1a157ae751ef38b26e82
F src/vdbe.c 2954169319fef68d5119e9855637e7a44dc64f34ee474d68001a05c03072cb48
F src/vdbe.h b6ffb28da04363fd5d439b8204118fd3f2e751abe4f8e3edb089155fb90d4607
F src/vdbeInt.h 68e5ab63f184f2871780c83ce78b425d0360869611baea4c65575f91f76ac1d3
F src/vdbeapi.c 8015628172d959885d5df592b9ddc07c66a803f61f86f1197cc17929c095b2ca
F src/vdbeaux.c 45685e76ffa1a24bd6e898005c8384868cfd629d6759413890e3ec9316eb3cf1
F src/vdbeapi.c 5929b6010966f1f8f5be8605323cfa004b8f68cfa056735f8f283d85fae81391
F src/vdbeaux.c 27d92528e7953a80f64782ef75e65a6694277214082e87d125e74e52d9e7ecd5
F src/vdbeblob.c b3f0640db9642fbdc88bd6ebcc83d6009514cafc98f062f675f2c8d505d82692
F src/vdbemem.c b2113c949e2ed918294e16a5ebaacdaca6be4f820f670517e3362e94c7019432
F src/vdbesort.c a72ad2a629df2901fde4aa588e38590d0aa27b7d5bc3b2a416e5f295de839cc1
@@ -1622,6 +1622,7 @@ F test/regexp1.sql a57e8d2777c7d62142bc802a7f18791ed5f17561ce6a1ca6ff287ba1a8c87
F test/regexp1.test 0023eae4073265641b826a70d81ba34d4dd66ad71871a5b4a1b7cf500d5c0c51
F test/regexp2.test 64f9726b2ddc71aea06725fcad53231833d038d58b936d49083ace658b370a13
F test/reindex.test cd9d6021729910ece82267b4f5e1b5ac2911a7566c43b43c176a6a4732e2118d
F test/reprepare.test ef52e80ecdc4b0e500324d1891c49a26c51b615a9fdf458ede807993f8e6337f
F test/reservebytes.test 6163640b5a5120c0dee6591481e673a0fa0bf0d12d4da7513bad692c1a49a162
F test/resetdb.test 54c06f18bc832ac6d6319e5ab23d5c8dd49fdbeec7c696d791682a8006bd5fc3
F test/resolver01.test f4022acafda7f4d40eca94dbf16bc5fc4ac30ceb
@@ -2283,9 +2284,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee
F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2
F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c
P 313ab6c4e517e599fa92102a1aa367fc97e9fd15a513f12ab9e8f4afebdf31f8
Q +85adb10e22ac33500f974d756bcf1fac46b7d98868cb4e00e59f30c075001b5b
R 6fdccfc782b336a52a781e68d86c9142
U stephan
Z a6f2f5825502fd610ee2383a8dcc572b
P af570c8fc22dc682ab71d8e97c20268971fb33032530d3fa9908e1995bed529e
R 6661c559ed600e427ce67ac1d1dfb05f
U dan
Z a2c5b3297ece60c69bf73817556b3a17
# Remove this line to create a well-formed Fossil manifest.

View File

@@ -1 +1 @@
af570c8fc22dc682ab71d8e97c20268971fb33032530d3fa9908e1995bed529e
7f154192db5a3a79f4666edd31dfc29fd73904ccd3630952e3b032e49fee6455

View File

@@ -2957,10 +2957,7 @@ int sqlite3ExprContainsSubquery(Expr *p){
** is marked so as to cause the statement to be reprepared each time a new
** value is bound to it. Except, if parameter bRSI is true, then the statement
** will only be reprepared if the rebind changes the value to or from a
** "small integer" (either 0 or 1). Note that if p is a variable then
** reprepare is always enabled for that variable, regardless of its current
** binding. The RSI is only enabled if the current binding is a small
** integer. "RSI" stands for "Reprepare Small Integers".
** "small integer" (either 0 or 1).
*/
int sqlite3ExprIsInteger(const Expr *p, int *pValue, Parse *pParse, int bRSI){
int iSign = 1; /* Either +1 or -1. */
@@ -2983,7 +2980,6 @@ int sqlite3ExprIsInteger(const Expr *p, int *pValue, Parse *pParse, int bRSI){
}
if( p->op==TK_VARIABLE && pParse!=0 ){
sqlite3_value *pVal; /* The variable */
int isSmall = 0; /* Only reprepare if change to/from small integer */
int rc = 0; /* 1 if successful, 0 if failed */
assert( iSign==1 );
if( NEVER(pParse->pVdbe==0) ) break;
@@ -2997,14 +2993,11 @@ int sqlite3ExprIsInteger(const Expr *p, int *pValue, Parse *pParse, int bRSI){
&& vv<=0x7fffffff
){
*pValue = (int)vv;
if( bRSI ){
isSmall = vv<=1;
}
rc = 1;
}
sqlite3ValueFree(pVal);
}
sqlite3VdbeReprepareOnBind(pParse->pVdbe, p->iColumn, isSmall);
sqlite3VdbeReprepareOnBind(pParse->pVdbe, p->iColumn, bRSI);
return rc;
}
break;

View File

@@ -1736,10 +1736,8 @@ static int vdbeUnbind(Vdbe *p, unsigned int i){
sqlite3_mutex_leave(p->db->mutex);
return SQLITE_RANGE;
}
pVar = &p->aVar[i];
sqlite3VdbeMemRelease(pVar);
pVar->flags = MEM_Null;
p->db->errCode = SQLITE_OK;
/* If the bit corresponding to this variable in Vdbe.expmask is set, then
** binding a new value to this variable invalidates the current query plan.
@@ -1753,11 +1751,20 @@ static int vdbeUnbind(Vdbe *p, unsigned int i){
assert( (p->prepFlags & SQLITE_PREPARE_SAVESQL)!=0 || p->expmask==0 );
assert( (p->expmask & p->smimask)==p->smimask );
if( p->expmask!=0 && (p->expmask & (i>=31 ? 0x80000000 : (u32)1<<i))!=0 ){
/* We might avoid a reprepare here if p->smimask is set and the old
** value is an integer other than (0,1). But that is such a corner
** case that it does not seem worth the extra code to implement. */
p->expired = 1;
/* If the p->smimask bit is also set, then only expire the statement if
** the value being clobbered was integer value 0 or 1. */
if( (p->smimask==0)
|| (p->smimask & (i>=31 ? 0x80000000 : (u32)1<<i))==0
|| (pVar->flags & MEM_Int)!=0 && pVar->u.i>=0 && pVar->u.i<=1
){
p->expired = 1;
}
}
sqlite3VdbeMemRelease(pVar);
pVar->flags = MEM_Null;
p->db->errCode = SQLITE_OK;
return SQLITE_OK;
}
@@ -1885,8 +1892,8 @@ int sqlite3_bind_int64(sqlite3_stmt *pStmt, int i, sqlite_int64 iValue){
if( sm1!=sm2 ){
p->expired = 1;
}
}else if( (pVar->flags & MEM_Int)==0 || pVar->u.i!=iValue ){
/* Always expire if the value really is changing */
}else{
/* Always expire if the smimask bit is clear */
p->expired = 1;
}
}

View File

@@ -5351,10 +5351,12 @@ void sqlite3VdbeReprepareOnBind(Vdbe *v, int iVar, int bSmallint){
|| (v->db->mDbFlags & DBFLAG_InternalFunc)!=0 );
m = (iVar>=32) ? 0x80000000 : ((u32)1 << (iVar-1));
v->expmask |= m;
if( bSmallint ){
v->smimask |= m;
if( (v->expmask & m)==0 ) v->smimask |= m;
}else{
v->smimask &= ~m;
}
v->expmask |= m;
/* smimask is always a subset of expmask */
assert( (v->smimask & v->expmask) == v->smimask );

121
test/reprepare.test Normal file
View File

@@ -0,0 +1,121 @@
# 2026 October 12
#
# The author disclaims copyright to this source code. In place of
# a legal notice, here is a blessing:
#
# May you do good and not evil.
# May you find forgiveness for yourself and forgive others.
# May you share freely, never taking more than you give.
#
#***********************************************************************
# This file implements regression tests for SQLite library.
#
# Specifically, it tests that statements that should only be reprepared
# when a bound variable is changed to or from a small integer value
# (0 or 1), really is only reprepared in those cases.
#
set testdir [file dirname $argv0]
source $testdir/tester.tcl
set testprefix reprepare
do_execsql_test 1.0 {
CREATE TABLE t1(id INTEGER PRIMARY KEY, nm TEXT);
INSERT INTO t1 VALUES(4294967296,'a'), (4294967297,'b');
}
proc do_reprepare_count_test {tn sql lInt nReprepare} {
set stmt [sqlite3_prepare_v2 db $sql -1 dummy]
foreach ii $lInt {
set ii [expr $ii]
sqlite3_bind_int64 $stmt 1 $ii
sqlite3_step $stmt
sqlite3_reset $stmt
}
do_test $tn [list \
sqlite3_stmt_status $stmt SQLITE_STMTSTATUS_REPREPARE 0
] [list $nReprepare]
sqlite3_finalize $stmt
}
do_reprepare_count_test 1.1 {
SELECT * FROM t1 WHERE +t1.id=?1
} {4294967296 4294967297 4294967296 4294967297} 0
do_reprepare_count_test 1.2 {
SELECT a.nm, b.nm FROM t1 a, t1 b WHERE a.id=?1 AND b.id=a.id
} {4294967296 4294967297 4294967296 4294967297} 0
proc do_reprepare_test {tn sql lTest} {
set stmt [sqlite3_prepare_v2 db $sql -1 dummy]
set cnt 0
foreach {t res} $lTest {
incr cnt
eval $t
sqlite3_step $stmt
sqlite3_reset $stmt
do_test $tn.$cnt [list \
sqlite3_stmt_status $stmt SQLITE_STMTSTATUS_REPREPARE 1
] [list $res]
}
sqlite3_finalize $stmt
}
do_reprepare_test 1.3 {
SELECT a.nm, b.nm FROM t1 a, t1 b WHERE a.id=?1 AND b.id=a.id
} {
{ sqlite3_bind_int64 $stmt 1 45 } 0
{ sqlite3_bind_int64 $stmt 1 0 } 1
{ sqlite3_bind_int64 $stmt 1 1 } 0
{ sqlite3_bind_int64 $stmt 1 101 } 1
{ sqlite3_bind_null $stmt 1 } 0
{ sqlite3_bind_text $stmt 1 hello -1 } 0
{ sqlite3_bind_int64 $stmt 1 1 } 1
{ sqlite3_bind_text $stmt 1 world -1 } 1
{ sqlite3_bind_int64 $stmt 1 450 } 0
{ sqlite3_bind_int64 $stmt 1 0 } 1
{ sqlite3_bind_null $stmt 1 } 1
{ sqlite3_bind_int64 $stmt 1 0 } 1
{ sqlite3_bind_int64 $stmt 1 1 } 0
{ sqlite3_bind_int64 $stmt 1 2 } 1
}
#-------------------------------------------------------------------------
# Test that if a variable is used both in a context where only a change
# to or from a small integer requires a reprepare (an equality comparison)
# and in a context where any change requires a reprepare (a LIMIT clause),
# the statement is reprepared every time a new value is bound.
#
do_execsql_test 2.0 {
CREATE TABLE t2(x INTEGER PRIMARY KEY, y);
WITH s(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM s WHERE i<10)
INSERT INTO t2 SELECT i, i FROM s;
}
proc do_limit_test {tn sql lTest} {
set stmt [sqlite3_prepare_v2 db $sql -1 dummy]
set cnt 0
foreach {val res} $lTest {
incr cnt
sqlite3_bind_int64 $stmt 1 $val
set nRow 0
while {[sqlite3_step $stmt]=="SQLITE_ROW"} { incr nRow }
sqlite3_reset $stmt
do_test $tn.$cnt [list set {} $nRow] [list $res]
}
sqlite3_finalize $stmt
}
do_limit_test 2.1 {
SELECT b.x FROM t2 a, t2 b WHERE a.x=?1 AND b.x>=a.x LIMIT ?1
} {
2 2 3 3 0 0 4 4 6 5 1 1 5 5 7 4 4 4
}
finish_test