mirror of
https://github.com/postgres/postgres.git
synced 2025-10-15 05:46:52 +03:00
Make row compares robust during nbtree array scans.
Recent nbtree bugfix commit5f4d98d4
added a special case to the code that sets up a page-level prefix of keys that are definitely satisfied by every tuple on the page: whenever _bt_set_startikey reached a row compare key, we'd refuse to apply the pstate.forcenonrequired behavior in scans where that usually happens (scans with a higher-order array key). That hack made the scan avoid essentially the same infinite cycling behavior that also affected nbtree scans with redundant keys (keys that preprocessing could not eliminate) prior to commitf09816a0
. There are now serious doubts about this row compare workaround. Testing has shown that a scan with a row compare key and an array key could still read the same leaf page twice (without the scan's direction changing), which isn't supposed to be possible following the SAOP enhancements added by Postgres 17 commit5bf748b8
. Also, we still allowed a required row compare key to be used with forcenonrequired mode when its header key happened to be beyond the pstate.ikey set by _bt_set_startikey, which was complicated and brittle. The underlying problem was that row compares had inconsistent rules around how scans start (which keys can be used for initial positioning purposes) and how scans end (which keys can set continuescan=false). Quals with redundant keys that could not be eliminated by preprocessing also had that same quality to them prior to today's bugfixf09816a0
. It now seems prudent to bring row compare keys in line with the new charter for required keys, by making the start and end rules symmetric. This commit fixes two points of disagreement between _bt_first and _bt_check_rowcompare. Firstly, _bt_check_rowcompare was capable of ending the scan at the point where it needed to compare an ISNULL-marked row compare member that came immediately after a required row compare member. _bt_first now has symmetric handling for NULL row compares. Secondly, _bt_first had its own ideas about which keys were safe to use for initial positioning purposes. It could use fewer or more keys than _bt_check_rowcompare. _bt_first now uses the same requiredness markings as _bt_check_rowcompare for this. Now that _bt_first and _bt_check_rowcompare agree on how to start and end scans, we can get rid of the forcenonrequired special case, without any risk of infinite cycling. This approach also makes row compare keys behave more like regular scalar keys, particularly within _bt_first. Fixing these inconsistencies necessitates dealing with a related issue with the way that row compares were marked required by preprocessing: we didn't mark any lower-order row members required following 2016 bugfix commita298a1e0
. That approach was over broad. The bug in question was actually an oversight in how _bt_check_rowcompare dealt with tuple NULL values that failed to satisfy a scan key marked required in the opposite scan direction (it was a bug in 2011 commits6980f817
and882368e8
, not a bug in 2006 commit3a0a16cb
). Go back to marking row compare members as required using the original 2006 rules, and fix the 2016 bug in a more principled way: by limiting use of the "set continuescan=false with a key required in the opposite scan direction upon encountering a NULL tuple value" optimization to the first/most significant row member key. While it isn't safe to use an implied IS NOT NULL qualifier to end the scan when it comes from a required lower-order row compare member key, it _is_ generally safe for such a required member key to end the scan -- provided the key is marked required in the _current_ scan direction. This fixes what was arguably an oversight in either commit5f4d98d4
or commit8a510275
. It is a direct follow-up to today's commitf09816a0
. Author: Peter Geoghegan <pg@bowt.ie> Reviewed-By: Heikki Linnakangas <heikki.linnakangas@iki.fi> Discussion: https://postgr.es/m/CAH2-Wz=pcijHL_mA0_TJ5LiTB28QpQ0cGtT-ccFV=KzuunNDDQ@mail.gmail.com Backpatch-through: 18
This commit is contained in:
@@ -2442,32 +2442,8 @@ _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate)
|
||||
}
|
||||
if (key->sk_flags & SK_ROW_HEADER)
|
||||
{
|
||||
/*
|
||||
* RowCompare inequality.
|
||||
*
|
||||
* Only the first subkey from a RowCompare can ever be marked
|
||||
* required (that happens when the row header is marked required).
|
||||
* There is no simple, general way for us to transitively deduce
|
||||
* whether or not every tuple on the page satisfies a RowCompare
|
||||
* key based only on firsttup and lasttup -- so we just give up.
|
||||
*/
|
||||
if (!start_past_saop_eq && !so->skipScan)
|
||||
break; /* unsafe to go further */
|
||||
|
||||
/*
|
||||
* We have to be even more careful with RowCompares that come
|
||||
* after an array: we assume it's unsafe to even bypass the array.
|
||||
* Calling _bt_start_array_keys to recover the scan's arrays
|
||||
* following use of forcenonrequired mode isn't compatible with
|
||||
* _bt_check_rowcompare's continuescan=false behavior with NULL
|
||||
* row compare members. _bt_advance_array_keys must not make a
|
||||
* decision on the basis of a key not being satisfied in the
|
||||
* opposite-to-scan direction until the scan reaches a leaf page
|
||||
* where the same key begins to be satisfied in scan direction.
|
||||
* The _bt_first !used_all_subkeys behavior makes this limitation
|
||||
* hard to work around some other way.
|
||||
*/
|
||||
return; /* completely unsafe to set pstate.startikey */
|
||||
/* RowCompare inequalities currently aren't supported */
|
||||
break; /* "unsafe" */
|
||||
}
|
||||
if (key->sk_strategy != BTEqualStrategyNumber)
|
||||
{
|
||||
@@ -2964,76 +2940,7 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
|
||||
|
||||
Assert(subkey->sk_flags & SK_ROW_MEMBER);
|
||||
|
||||
if (subkey->sk_attno > tupnatts)
|
||||
{
|
||||
/*
|
||||
* This attribute is truncated (must be high key). The value for
|
||||
* this attribute in the first non-pivot tuple on the page to the
|
||||
* right could be any possible value. Assume that truncated
|
||||
* attribute passes the qual.
|
||||
*/
|
||||
Assert(BTreeTupleIsPivot(tuple));
|
||||
cmpresult = 0;
|
||||
if (subkey->sk_flags & SK_ROW_END)
|
||||
break;
|
||||
subkey++;
|
||||
continue;
|
||||
}
|
||||
|
||||
datum = index_getattr(tuple,
|
||||
subkey->sk_attno,
|
||||
tupdesc,
|
||||
&isNull);
|
||||
|
||||
if (isNull)
|
||||
{
|
||||
if (forcenonrequired)
|
||||
{
|
||||
/* treating scan's keys as non-required */
|
||||
}
|
||||
else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
|
||||
{
|
||||
/*
|
||||
* Since NULLs are sorted before non-NULLs, we know we have
|
||||
* reached the lower limit of the range of values for this
|
||||
* index attr. On a backward scan, we can stop if this qual
|
||||
* is one of the "must match" subset. We can stop regardless
|
||||
* of whether the qual is > or <, so long as it's required,
|
||||
* because it's not possible for any future tuples to pass. On
|
||||
* a forward scan, however, we must keep going, because we may
|
||||
* have initially positioned to the start of the index.
|
||||
* (_bt_advance_array_keys also relies on this behavior during
|
||||
* forward scans.)
|
||||
*/
|
||||
if ((subkey->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
|
||||
ScanDirectionIsBackward(dir))
|
||||
*continuescan = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* Since NULLs are sorted after non-NULLs, we know we have
|
||||
* reached the upper limit of the range of values for this
|
||||
* index attr. On a forward scan, we can stop if this qual is
|
||||
* one of the "must match" subset. We can stop regardless of
|
||||
* whether the qual is > or <, so long as it's required,
|
||||
* because it's not possible for any future tuples to pass. On
|
||||
* a backward scan, however, we must keep going, because we
|
||||
* may have initially positioned to the end of the index.
|
||||
* (_bt_advance_array_keys also relies on this behavior during
|
||||
* backward scans.)
|
||||
*/
|
||||
if ((subkey->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) &&
|
||||
ScanDirectionIsForward(dir))
|
||||
*continuescan = false;
|
||||
}
|
||||
|
||||
/*
|
||||
* In any case, this indextuple doesn't match the qual.
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
/* When a NULL row member is compared, the row never matches */
|
||||
if (subkey->sk_flags & SK_ISNULL)
|
||||
{
|
||||
/*
|
||||
@@ -3058,6 +2965,114 @@ _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (subkey->sk_attno > tupnatts)
|
||||
{
|
||||
/*
|
||||
* This attribute is truncated (must be high key). The value for
|
||||
* this attribute in the first non-pivot tuple on the page to the
|
||||
* right could be any possible value. Assume that truncated
|
||||
* attribute passes the qual.
|
||||
*/
|
||||
Assert(BTreeTupleIsPivot(tuple));
|
||||
return true;
|
||||
}
|
||||
|
||||
datum = index_getattr(tuple,
|
||||
subkey->sk_attno,
|
||||
tupdesc,
|
||||
&isNull);
|
||||
|
||||
if (isNull)
|
||||
{
|
||||
int reqflags;
|
||||
|
||||
if (forcenonrequired)
|
||||
{
|
||||
/* treating scan's keys as non-required */
|
||||
}
|
||||
else if (subkey->sk_flags & SK_BT_NULLS_FIRST)
|
||||
{
|
||||
/*
|
||||
* Since NULLs are sorted before non-NULLs, we know we have
|
||||
* reached the lower limit of the range of values for this
|
||||
* index attr. On a backward scan, we can stop if this qual
|
||||
* is one of the "must match" subset. However, on a forwards
|
||||
* scan, we must keep going, because we may have initially
|
||||
* positioned to the start of the index.
|
||||
*
|
||||
* All required NULLS FIRST > row members can use NULL tuple
|
||||
* values to end backwards scans, just like with other values.
|
||||
* A qual "WHERE (a, b, c) > (9, 42, 'foo')" can terminate a
|
||||
* backwards scan upon reaching the index's rightmost "a = 9"
|
||||
* tuple whose "b" column contains a NULL (if not sooner).
|
||||
* Since "b" is NULLS FIRST, we can treat its NULLs as "<" 42.
|
||||
*/
|
||||
reqflags = SK_BT_REQBKWD;
|
||||
|
||||
/*
|
||||
* When a most significant required NULLS FIRST < row compare
|
||||
* member sees NULL tuple values during a backwards scan, it
|
||||
* signals the end of matches for the whole row compare/scan.
|
||||
* A qual "WHERE (a, b, c) < (9, 42, 'foo')" will terminate a
|
||||
* backwards scan upon reaching the rightmost tuple whose "a"
|
||||
* column has a NULL. The "a" NULL value is "<" 9, and yet
|
||||
* our < row compare will still end the scan. (This isn't
|
||||
* safe with later/lower-order row members. Notice that it
|
||||
* can only happen with an "a" NULL some time after the scan
|
||||
* completely stops needing to use its "b" and "c" members.)
|
||||
*/
|
||||
if (subkey == (ScanKey) DatumGetPointer(skey->sk_argument))
|
||||
reqflags |= SK_BT_REQFWD; /* safe, first row member */
|
||||
|
||||
if ((subkey->sk_flags & reqflags) &&
|
||||
ScanDirectionIsBackward(dir))
|
||||
*continuescan = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* Since NULLs are sorted after non-NULLs, we know we have
|
||||
* reached the upper limit of the range of values for this
|
||||
* index attr. On a forward scan, we can stop if this qual is
|
||||
* one of the "must match" subset. However, on a backward
|
||||
* scan, we must keep going, because we may have initially
|
||||
* positioned to the end of the index.
|
||||
*
|
||||
* All required NULLS LAST < row members can use NULL tuple
|
||||
* values to end forwards scans, just like with other values.
|
||||
* A qual "WHERE (a, b, c) < (9, 42, 'foo')" can terminate a
|
||||
* forwards scan upon reaching the index's leftmost "a = 9"
|
||||
* tuple whose "b" column contains a NULL (if not sooner).
|
||||
* Since "b" is NULLS LAST, we can treat its NULLs as ">" 42.
|
||||
*/
|
||||
reqflags = SK_BT_REQFWD;
|
||||
|
||||
/*
|
||||
* When a most significant required NULLS LAST > row compare
|
||||
* member sees NULL tuple values during a forwards scan, it
|
||||
* signals the end of matches for the whole row compare/scan.
|
||||
* A qual "WHERE (a, b, c) > (9, 42, 'foo')" will terminate a
|
||||
* forwards scan upon reaching the leftmost tuple whose "a"
|
||||
* column has a NULL. The "a" NULL value is ">" 9, and yet
|
||||
* our > row compare will end the scan. (This isn't safe with
|
||||
* later/lower-order row members. Notice that it can only
|
||||
* happen with an "a" NULL some time after the scan completely
|
||||
* stops needing to use its "b" and "c" members.)
|
||||
*/
|
||||
if (subkey == (ScanKey) DatumGetPointer(skey->sk_argument))
|
||||
reqflags |= SK_BT_REQBKWD; /* safe, first row member */
|
||||
|
||||
if ((subkey->sk_flags & reqflags) &&
|
||||
ScanDirectionIsForward(dir))
|
||||
*continuescan = false;
|
||||
}
|
||||
|
||||
/*
|
||||
* In any case, this indextuple doesn't match the qual.
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Perform the test --- three-way comparison not bool operator */
|
||||
cmpresult = DatumGetInt32(FunctionCall2Coll(&subkey->sk_func,
|
||||
subkey->sk_collation,
|
||||
|
Reference in New Issue
Block a user