Skip to content

Commit a9f8f66

Browse files
committed
sstable, db: document SeekPrefixGE for sstable and level iters
This is an attempt to document the current semantics as they are currently implemented. Also see the discussions in cockroachdb#3845 I hope to be looking at a way to resolve some of these complexities, perhaps by adding more tombstone-related logic in the merging iterator (postponing seeks instead of seeking beyond the prefix just to enact a levelIter positioning suitable for a subsequent try-seek-using-next).
1 parent d75cff6 commit a9f8f66

2 files changed

Lines changed: 104 additions & 3 deletions

File tree

level_iter.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,71 @@ func (l *levelIter) internalSeekGE(
703703
return l.verify(kv), kvMeta
704704
}
705705

706+
// SeekPrefixGE implements InternalIterator.SeekPrefixGE. It positions the
707+
// iterator at the first key greater than or equal to key across all files in
708+
// the level. It returns the key-value pair at that position, or nil if no such
709+
// key exists.
710+
//
711+
// The prefix argument is passed to each file's iterator for bloom filter
712+
// checking. If a file's bloom filter indicates that prefix is not present, that
713+
// file is skipped, and the iterator moves to the next file if and only if the
714+
// next file also can contain the prefix. The key argument is used for the
715+
// actual seek positioning.
716+
//
717+
// # Prefix vs key
718+
//
719+
// The prefix is typically the prefix of key (i.e. Split.Prefix(key) == prefix),
720+
// but this is not required. The only requirement is that prefix be less than or
721+
// equal to Split.Prefix(key). This flexibility is used when a higher-level
722+
// range deletion invalidates keys between prefix and Split.Prefix(key).
723+
//
724+
// For example, consider a SeekPrefixGE for prefix b and assume a RANGEDEL
725+
// [a, c@3) from a higher level. In this case, the merging iterator must seek
726+
// past the tombstone so it calls SeekPrefixGE(b, c@3, flags). The bloom filter
727+
// is checked against b (the original prefix), while the actual seek targets
728+
// c@3.
729+
//
730+
// # File positioning and TrySeekUsingNext
731+
//
732+
// The prefix is stored and controls file advancement when the current file is
733+
// exhausted. If the current file's largest key has a prefix greater than the
734+
// seek prefix, the iterator stops rather than advancing to the next file.
735+
//
736+
// This stopping condition is critical for TrySeekUsingNext correctness. Without
737+
// it, the following scenario would produce incorrect results:
738+
//
739+
// 1. SeekPrefixGE(P1, key1) - bloom filter misses on file F, no key found
740+
// 2. Iterator advances to file G (incorrectly, if G's smallest prefix > P1)
741+
// 3. SeekPrefixGE(P2, key2, TrySeekUsingNext) where P1 < P2 < G's smallest prefix
742+
// 4. TrySeekUsingNext starts from G, completely skipping file F which may
743+
// contain keys with prefix P2
744+
//
745+
// The stopping condition prevents step 2: if F's largest prefix > P1, the
746+
// iterator remains at F, allowing the subsequent seek for P2 to correctly
747+
// examine F.
748+
//
749+
// # Return values and iterator state
750+
//
751+
// - If a key is found: returns the key-value pair and positions the iterator
752+
// at that key. The iterator may be positioned in any file that contains
753+
// matching keys.
754+
//
755+
// - If no key is found: returns nil. The iterator is exhausted in the forward
756+
// direction. Subsequent Next() calls will return nil.
757+
//
758+
// # TrySeekUsingNext optimization
759+
//
760+
// The TrySeekUsingNext flag in flags indicates that the caller knows the seek
761+
// key is greater than or equal to the iterator's current position. When set:
762+
//
763+
// - At the file level: the iterator may scan forward through files rather
764+
// than performing a binary search through the file metadata.
765+
//
766+
// - At the sstable level: the optimization is automatically disabled when a
767+
// new file is loaded, since the new file's iterator is not yet positioned.
768+
//
769+
// Note: The caller must ensure that key is greater than or equal to the lower
770+
// bound. SeekPrefixGE checks the upper bound but not the lower bound.
706771
func (l *levelIter) SeekPrefixGE(prefix, key []byte, flags base.SeekGEFlags) (kv *base.InternalKV) {
707772
if treesteps.Enabled && treesteps.IsRecording(l) {
708773
op := treesteps.StartOpf(l, "SeekPrefixGE(%q, %q, %d)", prefix, key, flags)

sstable/reader_iter_single_lvl.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -839,9 +839,45 @@ func (i *singleLevelIterator[I, PI, D, PD]) seekGEHelper(
839839
return i.skipForward(shouldReturnMeta)
840840
}
841841

842-
// SeekPrefixGE implements internalIterator.SeekPrefixGE, as documented in the
843-
// pebble package. Note that SeekPrefixGE only checks the upper bound. It is up
844-
// to the caller to ensure that key is greater than or equal to the lower bound.
842+
// SeekPrefixGE implements InternalIterator.SeekPrefixGE. It positions the
843+
// iterator at the first key greater than or equal to key, as long as that key
844+
// has a prefix matching prefix. It returns the key-value pair at that position,
845+
// or nil if no such key exists in the table.
846+
//
847+
// The prefix argument is used exclusively for bloom filter checking. If the
848+
// table has a bloom filter and the filter indicates that prefix is not present,
849+
// SeekPrefixGE returns nil without positioning the iterator. The key argument
850+
// is used for the actual seek positioning when the bloom filter check passes or
851+
// is not applicable.
852+
//
853+
// The prefix is typically the prefix of key (i.e. Split.Prefix(key) == prefix),
854+
// but this is not required. The only requirement is that prefix be less than or
855+
// equal to Split.Prefix(key). This flexibility is used by higher-layer
856+
// iterators.
857+
//
858+
// Return values and iterator state:
859+
//
860+
// - If a key is found: returns the key-value pair and positions the iterator
861+
// at that key. Subsequent Next() calls will return following keys.
862+
//
863+
// - If no key is found because the bloom filter excluded the prefix: returns
864+
// nil. The iterator is not positioned; calling Next() or Prev() after this
865+
// is not permitted.
866+
//
867+
// - If no key is found but the bloom filter matched (or was not used): returns
868+
// nil. The iterator may be exhausted (reached bounds or end of table).
869+
// Calling Next() when exhausted forward will panic.
870+
//
871+
// The TrySeekUsingNext flag in flags indicates that the caller knows the seek
872+
// key is greater than or equal to the iterator's current position. When set,
873+
// the iterator may optimize by scanning forward from its current position
874+
// rather than performing a full seek. This optimization is automatically
875+
// disabled (i.e. SeekPrefixGE ignores the TrySeekUsingNext flag) when the
876+
// previous SeekPrefixGE returned nil due to a bloom filter miss, since the
877+
// iterator was not repositioned in that case.
878+
//
879+
// Note: SeekPrefixGE only checks the upper bound. It is up to the caller to
880+
// ensure that key is greater than or equal to the lower bound.
845881
func (i *singleLevelIterator[I, PI, D, PD]) SeekPrefixGE(
846882
prefix, key []byte, flags base.SeekGEFlags,
847883
) (kv *base.InternalKV) {

0 commit comments

Comments
 (0)