Skip to content

Commit 4fa7c3f

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 #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 4fa7c3f

2 files changed

Lines changed: 102 additions & 3 deletions

File tree

level_iter.go

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

sstable/reader_iter_single_lvl.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -839,9 +839,44 @@ 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 when the previous SeekPrefixGE returned nil due to a bloom filter
876+
// miss, since the iterator was not repositioned in that case.
877+
//
878+
// Note: SeekPrefixGE only checks the upper bound. It is up to the caller to
879+
// ensure that key is greater than or equal to the lower bound.
845880
func (i *singleLevelIterator[I, PI, D, PD]) SeekPrefixGE(
846881
prefix, key []byte, flags base.SeekGEFlags,
847882
) (kv *base.InternalKV) {

0 commit comments

Comments
 (0)