Repository: cockroachdb/pebble
Pull request #4095 (2024-10-22 16:32:19)

sstable: shard categorized iterator stats
Shard the categorized iterator stats to avoid mutex contention in high-read workloads that are frequently closing sstable iterators.

cockroach-teamcity: 

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/cockroachdb/pebble/4095)
<!-- Reviewable:end -->

RaduBerinde: *[`sstable/category_stats.go` line 209 at r1](https://reviewable.io/reviews/cockroachdb/pebble/4095#-O9p4YaVDEvg2bEbcasZ:-O9p7GR_-Bpfbyqxlnxx:bjmkxlu) ([raw file](https://github.com/cockroachdb/pebble/blob/0b0653a5ff5b1deb0513671aa0cf66e2469ca23f/sstable/category_stats.go#L209)):*
<details><summary><i>Previously, sumeerbhola wrote…</i></summary><blockquote>

You could use the Fibonacci hash trick we use in other places:

```
// Fibonacci hash https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/  
simpleHash := (11400714819323198485 * uint64(ptr)) % numShards
```
</blockquote></details>

Heh, I was going to suggest `((uint64(x) * 25214903917) >> 32) & 7`   (multiplier taken from this table: https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use). Either should work just fine


<!-- Sent from Reviewable.io -->

RaduBerinde: *[`sstable/category_stats.go` line 209 at r1](https://reviewable.io/reviews/cockroachdb/pebble/4095#-O9p4YaVDEvg2bEbcasZ:-O9pAvNrE69Q68IET8s5:bgi7sdm) ([raw file](https://github.com/cockroachdb/pebble/blob/0b0653a5ff5b1deb0513671aa0cf66e2469ca23f/sstable/category_stats.go#L209)):*
<details><summary><i>Previously, RaduBerinde wrote…</i></summary><blockquote>

Heh, I was going to suggest `((uint64(x) * 25214903917) >> 32) & 7`   (multiplier taken from this table: https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use). Either should work just fine
</blockquote></details>

Note that Fibonacci hash as described in the article uses the *high* bits of the result, not the low ones (we would do `>> (64-3)` instead of `% 8`)


<!-- Sent from Reviewable.io -->


Patch:
diff --git a/db.go b/db.go
index dc9dd3c3..3adb2d5e 100644
--- a/db.go
+++ b/db.go
@@ -12,6 +12,7 @@ import (
 	"sync"
 	"sync/atomic"
 	"time"
+	"unsafe"
 
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/internal/arenaskl"
@@ -1037,20 +1038,20 @@ type newIterOpts struct {
 // newIter constructs a new iterator, merging in batch iterators as an extra
 // level.
 func (d *DB) newIter(
-	ctx context.Context, batch *Batch, internalOpts newIterOpts, o *IterOptions,
+	ctx context.Context, batch *Batch, newIterOpts newIterOpts, o *IterOptions,
 ) *Iterator {
-	if internalOpts.batch.batchOnly {
+	if newIterOpts.batch.batchOnly {
 		if batch == nil {
 			panic("batchOnly is true, but batch is nil")
 		}
-		if internalOpts.snapshot.vers != nil {
+		if newIterOpts.snapshot.vers != nil {
 			panic("batchOnly is true, but snapshotIterOpts is initialized")
 		}
 	}
 	if err := d.closed.Load(); err != nil {
 		panic(err)
 	}
-	seqNum := internalOpts.snapshot.seqNum
+	seqNum := newIterOpts.snapshot.seqNum
 	if o != nil && o.RangeKeyMasking.Suffix != nil && o.KeyTypes != IterKeyTypePointsAndRanges {
 		panic("pebble: range key masking requires IterKeyTypePointsAndRanges")
 	}
@@ -1064,13 +1065,13 @@ func (d *DB) newIter(
 	var readState *readState
 	var newIters tableNewIters
 	var newIterRangeKey keyspanimpl.TableNewSpanIter
-	if !internalOpts.batch.batchOnly {
+	if !newIterOpts.batch.batchOnly {
 		// Grab and reference the current readState. This prevents the underlying
 		// files in the associated version from being deleted if there is a current
 		// compaction. The readState is unref'd by Iterator.Close().
-		if internalOpts.snapshot.vers == nil {
-			if internalOpts.snapshot.readState != nil {
-				readState = internalOpts.snapshot.readState
+		if newIterOpts.snapshot.vers == nil {
+			if newIterOpts.snapshot.readState != nil {
+				readState = newIterOpts.snapshot.readState
 				readState.ref()
 			} else {
 				// NB: loadReadState() calls readState.ref().
@@ -1078,7 +1079,7 @@ func (d *DB) newIter(
 			}
 		} else {
 			// vers != nil
-			internalOpts.snapshot.vers.Ref()
+			newIterOpts.snapshot.vers.Ref()
 		}
 
 		// Determine the seqnum to read at after grabbing the read state (current and
@@ -1100,15 +1101,16 @@ func (d *DB) newIter(
 		merge:               d.merge,
 		comparer:            *d.opts.Comparer,
 		readState:           readState,
-		version:             internalOpts.snapshot.vers,
+		version:             newIterOpts.snapshot.vers,
 		keyBuf:              buf.keyBuf,
 		prefixOrFullSeekKey: buf.prefixOrFullSeekKey,
 		boundsBuf:           buf.boundsBuf,
 		batch:               batch,
+		tc:                  d.tableCache,
 		newIters:            newIters,
 		newIterRangeKey:     newIterRangeKey,
 		seqNum:              seqNum,
-		batchOnlyIter:       internalOpts.batch.batchOnly,
+		batchOnlyIter:       newIterOpts.batch.batchOnly,
 	}
 	if o != nil {
 		dbi.opts = *o
@@ -1399,7 +1401,19 @@ func (i *Iterator) constructPointIter(
 		// Already have one.
 		return
 	}
-	internalOpts := internalIterOpts{stats: &i.stats.InternalStats}
+	internalOpts := internalIterOpts{
+		stats: &i.stats.InternalStats,
+	}
+	// If the table cache has a sstable stats collector, ask it for an
+	// accumulator for this iterator's configured category and QoS. All SSTable
+	// iterators created by this Iterator will accumulate their stats to it as
+	// they Close during iteration.
+	if collector := i.tc.dbOpts.sstStatsCollector; collector != nil {
+		internalOpts.iterStatsAccumulator = collector.Accumulator(
+			uint64(uintptr(unsafe.Pointer(i))),
+			i.opts.CategoryAndQoS,
+		)
+	}
 	if i.opts.RangeKeyMasking.Filter != nil {
 		internalOpts.boundLimitedFilter = &i.rangeKeyMasking
 	}
diff --git a/external_iterator.go b/external_iterator.go
index 72c7a147..03dca978 100644
--- a/external_iterator.go
+++ b/external_iterator.go
@@ -134,6 +134,11 @@ func createExternalPointIter(ctx context.Context, it *Iterator) (topLevelIterato
 	}
 	mlevels := it.alloc.mlevels[:0]
 
+	// TODO(jackson): External iterators never provide categorized iterator
+	// stats today because they exist outside the context of a *DB. If the
+	// sstables being read are on the physical filesystem, we may still want to
+	// thread a CategoryStatsCollector through so that we collect their stats.
+
 	if len(it.externalReaders) > cap(mlevels) {
 		mlevels = make([]mergingIterLevel, 0, len(it.externalReaders))
 	}
@@ -159,7 +164,7 @@ func createExternalPointIter(ctx context.Context, it *Iterator) (topLevelIterato
 			pointIter, err = r.NewPointIter(
 				ctx, transforms, it.opts.LowerBound, it.opts.UpperBound, nil, /* BlockPropertiesFilterer */
 				sstable.NeverUseFilterBlock,
-				&it.stats.InternalStats, it.opts.CategoryAndQoS, nil,
+				&it.stats.InternalStats, nil,
 				sstable.MakeTrivialReaderProvider(r))
 			if err != nil {
 				return nil, err
diff --git a/iterator.go b/iterator.go
index 4b21b936..a2aa77e4 100644
--- a/iterator.go
+++ b/iterator.go
@@ -245,6 +245,7 @@ type Iterator struct {
 	// and SetOptions or when re-fragmenting a batch's range keys/range dels.
 	// Non-nil if this Iterator includes a Batch.
 	batch            *Batch
+	tc               *tableCacheContainer
 	newIters         tableNewIters
 	newIterRangeKey  keyspanimpl.TableNewSpanIter
 	lazyCombinedIter lazyCombinedIter
@@ -2866,6 +2867,7 @@ func (i *Iterator) CloneWithContext(ctx context.Context, opts CloneOptions) (*It
 		boundsBuf:           buf.boundsBuf,
 		batch:               i.batch,
 		batchSeqNum:         i.batchSeqNum,
+		tc:                  i.tc,
 		newIters:            i.newIters,
 		newIterRangeKey:     i.newIterRangeKey,
 		seqNum:              i.seqNum,
diff --git a/level_iter.go b/level_iter.go
index 925a7c95..7f4b6f85 100644
--- a/level_iter.go
+++ b/level_iter.go
@@ -22,10 +22,11 @@ type internalIterOpts struct {
 	// if compaction is set, sstable-level iterators will be created using
 	// NewCompactionIter; these iterators have a more constrained interface
 	// and are optimized for the sequential scan of a compaction.
-	compaction         bool
-	bufferPool         *sstable.BufferPool
-	stats              *base.InternalIteratorStats
-	boundLimitedFilter sstable.BoundLimitedBlockPropertyFilter
+	compaction           bool
+	bufferPool           *sstable.BufferPool
+	stats                *base.InternalIteratorStats
+	iterStatsAccumulator sstable.IterStatsAccumulator
+	boundLimitedFilter   sstable.BoundLimitedBlockPropertyFilter
 }
 
 // levelIter provides a merged view of the sstables in a level.
diff --git a/sstable/category_stats.go b/sstable/category_stats.go
index d5b20029..2db32ca6 100644
--- a/sstable/category_stats.go
+++ b/sstable/category_stats.go
@@ -9,6 +9,7 @@ import (
 	"slices"
 	"sync"
 	"time"
+	"unsafe"
 
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/redact"
@@ -96,45 +97,81 @@ type CategoryStatsAggregate struct {
 	CategoryStats CategoryStats
 }
 
+const numCategoryStatsShards = 16
+
 type categoryStatsWithMu struct {
 	mu sync.Mutex
 	// Protected by mu.
-	stats CategoryStatsAggregate
+	stats CategoryStats
+}
+
+// Accumulate implements the IterStatsAccumulator interface.
+func (c *categoryStatsWithMu) Accumulate(stats CategoryStats) {
+	c.mu.Lock()
+	c.stats.aggregate(stats)
+	c.mu.Unlock()
 }
 
 // CategoryStatsCollector collects and aggregates the stats per category.
 type CategoryStatsCollector struct {
 	// mu protects additions to statsMap.
 	mu sync.Mutex
-	// Category => categoryStatsWithMu.
+	// Category => *shardedCategoryStats.
 	statsMap sync.Map
 }
 
-func (c *CategoryStatsCollector) reportStats(
-	category Category, qosLevel QoSLevel, stats CategoryStats,
-) {
-	v, ok := c.statsMap.Load(category)
+// shardedCategoryStats accumulates stats for a category, splitting its stats
+// across multiple shards to prevent mutex contention. In high-read workloads,
+// contention on the category stats mutex has been observed.
+type shardedCategoryStats struct {
+	Category Category
+	QoSLevel QoSLevel
+	shards   [numCategoryStatsShards]struct {
+		categoryStatsWithMu
+		// Pad each shard to 64 bytes so they don't share a cache line.
+		_ [64 - unsafe.Sizeof(categoryStatsWithMu{})]byte
+	}
+}
+
+// getStats retrieves the aggregated stats for the category, summing across all
+// shards.
+func (s *shardedCategoryStats) getStats() CategoryStatsAggregate {
+	agg := CategoryStatsAggregate{
+		Category: s.Category,
+		QoSLevel: s.QoSLevel,
+	}
+	for i := range s.shards {
+		s.shards[i].mu.Lock()
+		agg.CategoryStats.aggregate(s.shards[i].stats)
+		s.shards[i].mu.Unlock()
+	}
+	return agg
+}
+
+// Accumulator returns a stats accumulator for the given category. The provided
+// p is used to detrmine which shard to write stats to.
+func (c *CategoryStatsCollector) Accumulator(p uint64, caq CategoryAndQoS) IterStatsAccumulator {
+	v, ok := c.statsMap.Load(caq.Category)
 	if !ok {
 		c.mu.Lock()
-		v, _ = c.statsMap.LoadOrStore(category, &categoryStatsWithMu{
-			stats: CategoryStatsAggregate{Category: category, QoSLevel: qosLevel},
+		v, _ = c.statsMap.LoadOrStore(caq.Category, &shardedCategoryStats{
+			Category: caq.Category,
+			QoSLevel: caq.QoSLevel,
 		})
 		c.mu.Unlock()
 	}
-	aggStats := v.(*categoryStatsWithMu)
-	aggStats.mu.Lock()
-	aggStats.stats.CategoryStats.aggregate(stats)
-	aggStats.mu.Unlock()
+	s := v.(*shardedCategoryStats)
+	// This equation is taken from:
+	// https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use
+	shard := ((p * 25214903917) >> 32) & (numCategoryStatsShards - 1)
+	return &s.shards[shard].categoryStatsWithMu
 }
 
 // GetStats returns the aggregated stats.
 func (c *CategoryStatsCollector) GetStats() []CategoryStatsAggregate {
 	var stats []CategoryStatsAggregate
 	c.statsMap.Range(func(_, v any) bool {
-		aggStats := v.(*categoryStatsWithMu)
-		aggStats.mu.Lock()
-		s := aggStats.stats
-		aggStats.mu.Unlock()
+		s := v.(*shardedCategoryStats).getStats()
 		if len(s.Category) == 0 {
 			s.Category = "_unknown"
 		}
@@ -147,34 +184,33 @@ func (c *CategoryStatsCollector) GetStats() []CategoryStatsAggregate {
 	return stats
 }
 
-// iterStatsAccumulator is a helper for a sstable iterator to accumulate
-// stats, which are reported to the CategoryStatsCollector when the
-// accumulator is closed.
+type IterStatsAccumulator interface {
+	// Accumulate accumulates the provided stats.
+	Accumulate(cas CategoryStats)
+}
+
+// iterStatsAccumulator is a helper for a sstable iterator to accumulate stats,
+// which are reported to the CategoryStatsCollector when the accumulator is
+// closed.
 type iterStatsAccumulator struct {
-	Category
-	QoSLevel
-	stats     CategoryStats
-	collector *CategoryStatsCollector
+	stats  CategoryStats
+	parent IterStatsAccumulator
 }
 
-func (accum *iterStatsAccumulator) init(
-	categoryAndQoS CategoryAndQoS, collector *CategoryStatsCollector,
-) {
-	accum.Category = categoryAndQoS.Category
-	accum.QoSLevel = categoryAndQoS.QoSLevel
-	accum.collector = collector
+func (a *iterStatsAccumulator) init(parent IterStatsAccumulator) {
+	a.parent = parent
 }
 
-func (accum *iterStatsAccumulator) reportStats(
+func (a *iterStatsAccumulator) reportStats(
 	blockBytes, blockBytesInCache uint64, blockReadDuration time.Duration,
 ) {
-	accum.stats.BlockBytes += blockBytes
-	accum.stats.BlockBytesInCache += blockBytesInCache
-	accum.stats.BlockReadDuration += blockReadDuration
+	a.stats.BlockBytes += blockBytes
+	a.stats.BlockBytesInCache += blockBytesInCache
+	a.stats.BlockReadDuration += blockReadDuration
 }
 
-func (accum *iterStatsAccumulator) close() {
-	if accum.collector != nil {
-		accum.collector.reportStats(accum.Category, accum.QoSLevel, accum.stats)
+func (a *iterStatsAccumulator) close() {
+	if a.parent != nil {
+		a.parent.Accumulate(a.stats)
 	}
 }
diff --git a/sstable/colblk/data_block.go b/sstable/colblk/data_block.go
index 550e600c..aafe7f8b 100644
--- a/sstable/colblk/data_block.go
+++ b/sstable/colblk/data_block.go
@@ -874,9 +874,9 @@ func (d *DataBlockDecoder) PrefixChanged() Bitmap {
 	return d.prefixChanged
 }
 
-// KeySchemaHeader returns the KeySchema-specific header of fixed size.
-func (d *DataBlockDecoder) KeySchemaHeader(schemaHeaderSize uint32) []byte {
-	return d.d.data[:schemaHeaderSize]
+// KeySchemaHeader returns the KeySchema-specific header.
+func (d *DataBlockDecoder) KeySchemaHeader() []byte {
+	return d.d.data[:d.d.customHeaderSize-dataBlockCustomHeaderSize]
 }
 
 // Init initializes the data block reader with the given serialized data block.
@@ -905,6 +905,9 @@ func (d *DataBlockDecoder) Describe(f *binfmt.Formatter, tp treeprinter.Node) {
 	f.SetAnchorOffset()
 
 	n := tp.Child("data block header")
+	if keySchemaHeaderSize := int(d.d.customHeaderSize - 4); keySchemaHeaderSize > 0 {
+		f.HexBytesln(keySchemaHeaderSize, "key schema header")
+	}
 	f.HexBytesln(4, "maximum key length: %d", d.maximumKeyLength)
 	d.d.headerToBinFormatter(f, n)
 	for i := 0; i < int(d.d.header.Columns); i++ {
diff --git a/sstable/reader.go b/sstable/reader.go
index 261d7536..9bb09421 100644
--- a/sstable/reader.go
+++ b/sstable/reader.go
@@ -120,13 +120,12 @@ func (r *Reader) NewPointIter(
 	filterer *BlockPropertiesFilterer,
 	filterBlockSizeLimit FilterBlockSizeLimit,
 	stats *base.InternalIteratorStats,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	rp ReaderProvider,
 ) (Iterator, error) {
 	return r.newPointIter(
 		ctx, transforms, lower, upper, filterer, filterBlockSizeLimit,
-		stats, categoryAndQoS, statsCollector, rp, nil)
+		stats, statsAccum, rp, nil)
 }
 
 // TryAddBlockPropertyFilterForHideObsoletePoints is expected to be called
@@ -152,8 +151,7 @@ func (r *Reader) newPointIter(
 	filterer *BlockPropertiesFilterer,
 	filterBlockSizeLimit FilterBlockSizeLimit,
 	stats *base.InternalIteratorStats,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	rp ReaderProvider,
 	vState *virtualState,
 ) (Iterator, error) {
@@ -166,21 +164,21 @@ func (r *Reader) newPointIter(
 		if r.tableFormat.BlockColumnar() {
 			res, err = newColumnBlockTwoLevelIterator(
 				ctx, r, vState, transforms, lower, upper, filterer, filterBlockSizeLimit,
-				stats, categoryAndQoS, statsCollector, rp, nil /* bufferPool */)
+				stats, statsAccum, rp, nil /* bufferPool */)
 		} else {
 			res, err = newRowBlockTwoLevelIterator(
 				ctx, r, vState, transforms, lower, upper, filterer, filterBlockSizeLimit,
-				stats, categoryAndQoS, statsCollector, rp, nil /* bufferPool */)
+				stats, statsAccum, rp, nil /* bufferPool */)
 		}
 	} else {
 		if r.tableFormat.BlockColumnar() {
 			res, err = newColumnBlockSingleLevelIterator(
 				ctx, r, vState, transforms, lower, upper, filterer, filterBlockSizeLimit,
-				stats, categoryAndQoS, statsCollector, rp, nil /* bufferPool */)
+				stats, statsAccum, rp, nil /* bufferPool */)
 		} else {
 			res, err = newRowBlockSingleLevelIterator(
 				ctx, r, vState, transforms, lower, upper, filterer, filterBlockSizeLimit,
-				stats, categoryAndQoS, statsCollector, rp, nil /* bufferPool */)
+				stats, statsAccum, rp, nil /* bufferPool */)
 		}
 	}
 	if err != nil {
@@ -202,7 +200,7 @@ func (r *Reader) NewIter(transforms IterTransforms, lower, upper []byte) (Iterat
 	// likely isn't a cache set up.
 	return r.NewPointIter(
 		context.TODO(), transforms, lower, upper, nil, AlwaysUseFilterBlock,
-		nil /* stats */, CategoryAndQoS{}, nil /* statsCollector */, MakeTrivialReaderProvider(r))
+		nil /* stats */, nil /* statsAccum */, MakeTrivialReaderProvider(r))
 }
 
 // NewCompactionIter returns an iterator similar to NewIter but it also increments
@@ -210,18 +208,16 @@ func (r *Reader) NewIter(transforms IterTransforms, lower, upper []byte) (Iterat
 // after itself and returns a nil iterator.
 func (r *Reader) NewCompactionIter(
 	transforms IterTransforms,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	rp ReaderProvider,
 	bufferPool *block.BufferPool,
 ) (Iterator, error) {
-	return r.newCompactionIter(transforms, categoryAndQoS, statsCollector, rp, nil, bufferPool)
+	return r.newCompactionIter(transforms, statsAccum, rp, nil, bufferPool)
 }
 
 func (r *Reader) newCompactionIter(
 	transforms IterTransforms,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	rp ReaderProvider,
 	vState *virtualState,
 	bufferPool *block.BufferPool,
@@ -234,7 +230,7 @@ func (r *Reader) newCompactionIter(
 			i, err := newRowBlockTwoLevelIterator(
 				context.Background(),
 				r, vState, transforms, nil /* lower */, nil /* upper */, nil,
-				NeverUseFilterBlock, nil /* stats */, categoryAndQoS, statsCollector, rp, bufferPool)
+				NeverUseFilterBlock, nil /* stats */, statsAccum, rp, bufferPool)
 			if err != nil {
 				return nil, err
 			}
@@ -244,7 +240,7 @@ func (r *Reader) newCompactionIter(
 		i, err := newColumnBlockTwoLevelIterator(
 			context.Background(),
 			r, vState, transforms, nil /* lower */, nil /* upper */, nil,
-			NeverUseFilterBlock, nil /* stats */, categoryAndQoS, statsCollector, rp, bufferPool)
+			NeverUseFilterBlock, nil /* stats */, statsAccum, rp, bufferPool)
 		if err != nil {
 			return nil, err
 		}
@@ -254,7 +250,7 @@ func (r *Reader) newCompactionIter(
 	if !r.tableFormat.BlockColumnar() {
 		i, err := newRowBlockSingleLevelIterator(
 			context.Background(), r, vState, transforms, nil /* lower */, nil, /* upper */
-			nil, NeverUseFilterBlock, nil /* stats */, categoryAndQoS, statsCollector, rp, bufferPool)
+			nil, NeverUseFilterBlock, nil /* stats */, statsAccum, rp, bufferPool)
 		if err != nil {
 			return nil, err
 		}
@@ -263,7 +259,7 @@ func (r *Reader) newCompactionIter(
 	}
 	i, err := newColumnBlockSingleLevelIterator(
 		context.Background(), r, vState, transforms, nil /* lower */, nil, /* upper */
-		nil, NeverUseFilterBlock, nil /* stats */, categoryAndQoS, statsCollector, rp, bufferPool)
+		nil, NeverUseFilterBlock, nil /* stats */, statsAccum, rp, bufferPool)
 	if err != nil {
 		return nil, err
 	}
diff --git a/sstable/reader_common.go b/sstable/reader_common.go
index 20cc3015..f8db2627 100644
--- a/sstable/reader_common.go
+++ b/sstable/reader_common.go
@@ -32,15 +32,13 @@ type CommonReader interface {
 		filterer *BlockPropertiesFilterer,
 		filterBlockSizeLimit FilterBlockSizeLimit,
 		stats *base.InternalIteratorStats,
-		categoryAndQoS CategoryAndQoS,
-		statsCollector *CategoryStatsCollector,
+		statsAccum IterStatsAccumulator,
 		rp ReaderProvider,
 	) (Iterator, error)
 
 	NewCompactionIter(
 		transforms IterTransforms,
-		categoryAndQoS CategoryAndQoS,
-		statsCollector *CategoryStatsCollector,
+		statsAccum IterStatsAccumulator,
 		rp ReaderProvider,
 		bufferPool *block.BufferPool,
 	) (Iterator, error)
diff --git a/sstable/reader_iter_single_lvl.go b/sstable/reader_iter_single_lvl.go
index 2cad9c0a..5dcbbd74 100644
--- a/sstable/reader_iter_single_lvl.go
+++ b/sstable/reader_iter_single_lvl.go
@@ -210,8 +210,7 @@ func newColumnBlockSingleLevelIterator(
 	filterer *BlockPropertiesFilterer,
 	filterBlockSizeLimit FilterBlockSizeLimit,
 	stats *base.InternalIteratorStats,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	rp ReaderProvider,
 	bufferPool *block.BufferPool,
 ) (*singleLevelIteratorColumnBlocks, error) {
@@ -225,7 +224,7 @@ func newColumnBlockSingleLevelIterator(
 	useFilterBlock := shouldUseFilterBlock(r, filterBlockSizeLimit)
 	i.init(
 		ctx, r, v, transforms, lower, upper, filterer, useFilterBlock,
-		stats, categoryAndQoS, statsCollector, bufferPool,
+		stats, statsAccum, bufferPool,
 	)
 	var getLazyValuer block.GetLazyValueForPrefixAndValueHandler
 	if r.Properties.NumValueBlocks > 0 {
@@ -275,8 +274,7 @@ func newRowBlockSingleLevelIterator(
 	filterer *BlockPropertiesFilterer,
 	filterBlockSizeLimit FilterBlockSizeLimit,
 	stats *base.InternalIteratorStats,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	rp ReaderProvider,
 	bufferPool *block.BufferPool,
 ) (*singleLevelIteratorRowBlocks, error) {
@@ -290,7 +288,7 @@ func newRowBlockSingleLevelIterator(
 	useFilterBlock := shouldUseFilterBlock(r, filterBlockSizeLimit)
 	i.init(
 		ctx, r, v, transforms, lower, upper, filterer, useFilterBlock,
-		stats, categoryAndQoS, statsCollector, bufferPool,
+		stats, statsAccum, bufferPool,
 	)
 	if r.tableFormat >= TableFormatPebblev3 {
 		if r.Properties.NumValueBlocks > 0 {
@@ -336,8 +334,7 @@ func (i *singleLevelIterator[I, PI, D, PD]) init(
 	filterer *BlockPropertiesFilterer,
 	useFilterBlock bool,
 	stats *base.InternalIteratorStats,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	bufferPool *block.BufferPool,
 ) {
 	i.inPool = false
@@ -353,8 +350,7 @@ func (i *singleLevelIterator[I, PI, D, PD]) init(
 		i.vState = v
 		i.endKeyInclusive, i.lower, i.upper = v.constrainBounds(lower, upper, false /* endInclusive */)
 	}
-
-	i.iterStats.init(categoryAndQoS, statsCollector)
+	i.iterStats.init(statsAccum)
 	i.readBlockEnv = readBlockEnv{
 		Stats:      stats,
 		IterStats:  &i.iterStats,
diff --git a/sstable/reader_iter_two_lvl.go b/sstable/reader_iter_two_lvl.go
index 605ec636..31e38f43 100644
--- a/sstable/reader_iter_two_lvl.go
+++ b/sstable/reader_iter_two_lvl.go
@@ -161,8 +161,7 @@ func newColumnBlockTwoLevelIterator(
 	filterer *BlockPropertiesFilterer,
 	filterBlockSizeLimit FilterBlockSizeLimit,
 	stats *base.InternalIteratorStats,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	rp ReaderProvider,
 	bufferPool *block.BufferPool,
 ) (*twoLevelIteratorColumnBlocks, error) {
@@ -175,7 +174,7 @@ func newColumnBlockTwoLevelIterator(
 	i := twoLevelIterColumnBlockPool.Get().(*twoLevelIteratorColumnBlocks)
 	i.secondLevel.init(ctx, r, v, transforms, lower, upper, filterer,
 		false, // Disable the use of the filter block in the second level.
-		stats, categoryAndQoS, statsCollector, bufferPool)
+		stats, statsAccum, bufferPool)
 	var getLazyValuer block.GetLazyValueForPrefixAndValueHandler
 	if r.Properties.NumValueBlocks > 0 {
 		// NB: we cannot avoid this ~248 byte allocation, since valueBlockReader
@@ -225,8 +224,7 @@ func newRowBlockTwoLevelIterator(
 	filterer *BlockPropertiesFilterer,
 	filterBlockSizeLimit FilterBlockSizeLimit,
 	stats *base.InternalIteratorStats,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	rp ReaderProvider,
 	bufferPool *block.BufferPool,
 ) (*twoLevelIteratorRowBlocks, error) {
@@ -239,7 +237,7 @@ func newRowBlockTwoLevelIterator(
 	i := twoLevelIterRowBlockPool.Get().(*twoLevelIteratorRowBlocks)
 	i.secondLevel.init(ctx, r, v, transforms, lower, upper, filterer,
 		false, // Disable the use of the filter block in the second level.
-		stats, categoryAndQoS, statsCollector, bufferPool)
+		stats, statsAccum, bufferPool)
 	if r.tableFormat >= TableFormatPebblev3 {
 		if r.Properties.NumValueBlocks > 0 {
 			// NB: we cannot avoid this ~248 byte allocation, since valueBlockReader
diff --git a/sstable/reader_virtual.go b/sstable/reader_virtual.go
index dfa84d2a..c37c8963 100644
--- a/sstable/reader_virtual.go
+++ b/sstable/reader_virtual.go
@@ -94,13 +94,12 @@ func MakeVirtualReader(reader *Reader, p VirtualReaderParams) VirtualReader {
 // NewCompactionIter is the compaction iterator function for virtual readers.
 func (v *VirtualReader) NewCompactionIter(
 	transforms IterTransforms,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	rp ReaderProvider,
 	bufferPool *block.BufferPool,
 ) (Iterator, error) {
 	return v.reader.newCompactionIter(
-		transforms, categoryAndQoS, statsCollector, rp, &v.vState, bufferPool)
+		transforms, statsAccum, rp, &v.vState, bufferPool)
 }
 
 // NewPointIter returns an iterator for the point keys in the table.
@@ -119,13 +118,12 @@ func (v *VirtualReader) NewPointIter(
 	filterer *BlockPropertiesFilterer,
 	filterBlockSizeLimit FilterBlockSizeLimit,
 	stats *base.InternalIteratorStats,
-	categoryAndQoS CategoryAndQoS,
-	statsCollector *CategoryStatsCollector,
+	statsAccum IterStatsAccumulator,
 	rp ReaderProvider,
 ) (Iterator, error) {
 	return v.reader.newPointIter(
 		ctx, transforms, lower, upper, filterer, filterBlockSizeLimit,
-		stats, categoryAndQoS, statsCollector, rp, &v.vState)
+		stats, statsAccum, rp, &v.vState)
 }
 
 // ValidateBlockChecksumsOnBacking will call ValidateBlockChecksumsOnBacking on the underlying reader.
diff --git a/table_cache.go b/table_cache.go
index c95203d8..d0d4e361 100644
--- a/table_cache.go
+++ b/table_cache.go
@@ -571,18 +571,17 @@ func (c *tableCacheShard) newPointIter(
 	}
 	transforms := file.IterTransforms()
 	transforms.HideObsoletePoints = hideObsoletePoints
-	var categoryAndQoS sstable.CategoryAndQoS
-	if opts != nil {
-		categoryAndQoS = opts.CategoryAndQoS
+	iterStatsAccum := internalOpts.iterStatsAccumulator
+	if iterStatsAccum == nil && opts != nil && dbOpts.sstStatsCollector != nil {
+		iterStatsAccum = dbOpts.sstStatsCollector.Accumulator(
+			uint64(uintptr(unsafe.Pointer(v.reader))), opts.CategoryAndQoS)
 	}
 	if internalOpts.compaction {
-		iter, err = cr.NewCompactionIter(
-			transforms, categoryAndQoS, dbOpts.sstStatsCollector, rp,
-			internalOpts.bufferPool)
+		iter, err = cr.NewCompactionIter(transforms, iterStatsAccum, rp, internalOpts.bufferPool)
 	} else {
 		iter, err = cr.NewPointIter(
 			ctx, transforms, opts.GetLowerBound(), opts.GetUpperBound(), filterer, filterBlockSizeLimit,
-			internalOpts.stats, categoryAndQoS, dbOpts.sstStatsCollector, rp)
+			internalOpts.stats, iterStatsAccum, rp)
 	}
 	if err != nil {
 		return nil, err


Test patch:
diff --git a/internal/crdbtest/crdb.go b/internal/crdbtest/crdb.go
index 2f594fe7..8dd99020 100644
--- a/internal/crdbtest/crdb.go
+++ b/internal/crdbtest/crdb.go
@@ -423,7 +423,7 @@ const (
 	// hasEmptySuffixes is set if there is at least one key with no suffix in the block.
 	hasEmptySuffixes
 	// hasNonMVCCSuffixes is set if there is at least one key with a non-empty,
-	// non-MVCC sufffix.
+	// non-MVCC suffix.
 	hasNonMVCCSuffixes
 )
 
@@ -435,7 +435,7 @@ func (s suffixTypes) String() string {
 	if s&hasEmptySuffixes != 0 {
 		suffixes = append(suffixes, "empty")
 	}
-	if s&hasEmptySuffixes != 0 {
+	if s&hasNonMVCCSuffixes != 0 {
 		suffixes = append(suffixes, "non-mvcc")
 	}
 	if len(suffixes) == 0 {
@@ -527,7 +527,7 @@ func (kw *cockroachKeyWriter) WriteKey(
 	switch versionLen {
 	case 0:
 		// No-op.
-		kw.suffixTypes |= hasNonMVCCSuffixes
+		kw.suffixTypes |= hasEmptySuffixes
 	case 9:
 		kw.suffixTypes |= hasMVCCSuffixes
 		wallTime = binary.BigEndian.Uint64(key[keyPrefixLen : keyPrefixLen+8])
@@ -539,7 +539,7 @@ func (kw *cockroachKeyWriter) WriteKey(
 		// longer consulted and can be ignored during decoding.
 	default:
 		// Not a MVCC timestamp.
-		kw.suffixTypes |= hasEmptySuffixes
+		kw.suffixTypes |= hasNonMVCCSuffixes
 		untypedVersion = key[keyPrefixLen:]
 	}
 	kw.wallTimes.Set(row, wallTime)
@@ -633,7 +633,11 @@ func (ks *cockroachKeySeeker) init(d *colblk.DataBlockDecoder) {
 	ks.mvccWallTimes = bd.Uints(cockroachColMVCCWallTime)
 	ks.mvccLogical = bd.Uints(cockroachColMVCCLogical)
 	ks.untypedVersions = bd.RawBytes(cockroachColUntypedVersion)
-	ks.suffixTypes = suffixTypes(d.KeySchemaHeader(1)[0])
+	header := d.KeySchemaHeader()
+	if len(header) != 1 {
+		panic(errors.AssertionFailedf("invalid key schema-specific header %x", header))
+	}
+	ks.suffixTypes = suffixTypes(header[0])
 }
 
 // IsLowerBound is part of the KeySeeker interface.
@@ -727,7 +731,7 @@ func (ks *cockroachKeySeeker) seekGEOnSuffix(index int, seekSuffix []byte) (row
 		for l < u {
 			h := int(uint(l+u) >> 1) // avoid overflow when computing h
 			// l ≤ h < u
-			if bytes.Compare(ks.untypedVersions.At(h), seekSuffix) >= 0 {
+			if bytes.Compare(ks.untypedVersions.At(h), seekSuffix) <= 0 {
 				u = h // preserves f(u) == true
 			} else {
 				l = h + 1 // preserves f(l-1) == false
@@ -770,6 +774,9 @@ func (ks *cockroachKeySeeker) seekGEOnSuffix(index int, seekSuffix []byte) (row
 func (ks *cockroachKeySeeker) MaterializeUserKey(
 	ki *colblk.PrefixBytesIter, prevRow, row int,
 ) []byte {
+	if invariants.Enabled && (row < 0 || row >= ks.roachKeys.Rows()) {
+		panic(errors.AssertionFailedf("invalid row number %d", row))
+	}
 	if prevRow+1 == row && prevRow >= 0 {
 		ks.roachKeys.SetNext(ki)
 	} else {
diff --git a/internal/crdbtest/crdb_bench_test.go b/internal/crdbtest/crdb_bench_test.go
index cac71892..d20b2fb3 100644
--- a/internal/crdbtest/crdb_bench_test.go
+++ b/internal/crdbtest/crdb_bench_test.go
@@ -117,7 +117,7 @@ func benchmarkRandSeekInSST(
 	rp := sstable.MakeTrivialReaderProvider(reader)
 	iter, err := reader.NewPointIter(
 		ctx, sstable.NoTransforms, nil, nil, nil, sstable.NeverUseFilterBlock,
-		&stats, sstable.CategoryAndQoS{}, nil, rp)
+		&stats, nil, rp)
 	require.NoError(b, err)
 	n := 0
 	for kv := iter.First(); kv != nil; kv = iter.Next() {
@@ -133,7 +133,7 @@ func benchmarkRandSeekInSST(
 		key := queryKeys[i%numQueryKeys]
 		iter, err := reader.NewPointIter(
 			ctx, sstable.NoTransforms, nil, nil, nil, sstable.NeverUseFilterBlock,
-			&stats, sstable.CategoryAndQoS{}, nil, rp)
+			&stats, nil, rp)
 		if err != nil {
 			b.Fatal(err)
 		}
diff --git a/internal/crdbtest/crdb_test.go b/internal/crdbtest/crdb_test.go
index 61bd8cea..a6f35b14 100644
--- a/internal/crdbtest/crdb_test.go
+++ b/internal/crdbtest/crdb_test.go
@@ -320,3 +320,103 @@ func (g *cockroachKeyGen) randTimestamp() (wallTime uint64, logicalTime uint32)
 	}
 	return wallTime, logicalTime
 }
+
+// formatUserKey formats a user key in the format:
+//
+//	<roach-key> [ @ <version-hex> ]
+func formatUserKey(key []byte) string {
+	n := Split(key)
+	if key[n-1] != 0 {
+		panic("expected sentinel byte")
+	}
+	prefix := key[:n-1]
+	if n == len(key) {
+		return string(prefix)
+	}
+	suffix := key[n : len(key)-1]
+	if key[len(key)-1] != byte(len(suffix)+1) {
+		panic("invalid suffix length byte")
+	}
+	return fmt.Sprintf("%s @ %X", prefix, suffix)
+}
+
+// formatKey formats an internal key in the format:
+//
+//	<roach-key> [ @ <version-hex> ] #<seq-num>,<kind>
+func formatKey(key base.InternalKey) string {
+	return fmt.Sprintf("%s #%d,%s", formatUserKey(key.UserKey), key.SeqNum(), key.Kind())
+}
+
+// formatKV formats an internal key in the format:
+//
+//	<roach-key> [ @ <version-hex> ] #<seq-num>,<kind> = value
+//
+// For example:
+//
+//	foo @ 0001020304050607 #1,SET
+func formatKV(kv base.InternalKV) string {
+	val, _, err := kv.V.Value(nil)
+	if err != nil {
+		panic(err)
+	}
+	if len(val) == 0 {
+		return formatKey(kv.K)
+	}
+	return fmt.Sprintf("%s = %s", formatKey(kv.K), val)
+}
+
+// parseKey parses a cockroach user key in the following format:
+//
+//	<roach-key> [@ <version-hex>]
+//
+// For example:
+//
+//	foo @ 0001020304050607
+func parseUserKey(userKeyStr string) []byte {
+	roachKey, versionStr := splitStringAt(userKeyStr, " @ ")
+	// Append sentinel byte.
+	userKey := append([]byte(roachKey), 0)
+	if versionStr != "" {
+		var version []byte
+		if _, err := fmt.Sscanf(versionStr, "%X", &version); err != nil {
+			panic(fmt.Sprintf("invalid user key string %q: cannot parse version %X", userKeyStr, version))
+		}
+		userKey = append(userKey, version...)
+		userKey = append(userKey, byte(len(version)+1))
+	}
+	return userKey
+}
+
+// parseKey parses a cockroach key in the following format:
+//
+//	<roach-key> [@ <version-hex>] #<seq-num>,<kind>
+//
+// For example:
+//
+//	foo @ 0001020304050607 #1,SET
+func parseKey(keyStr string) base.InternalKey {
+	userKeyStr, trailerStr := splitStringAt(keyStr, " #")
+	return base.InternalKey{
+		UserKey: parseUserKey(userKeyStr),
+		Trailer: base.ParseInternalKey(fmt.Sprintf("foo#%s", trailerStr)).Trailer,
+	}
+}
+
+// parseKey parses a cockroach KV in the following format:
+//
+//	<roach-key> [@ <version-hex>] #<seq-num>,<kind> = value
+//
+// For example:
+//
+//	foo @ 0001020304050607 #1,SET = bar
+func parseKV(input string) (key base.InternalKey, value []byte) {
+	keyStr, valStr := splitStringAt(input, " = ")
+	return parseKey(keyStr), []byte(valStr)
+}
+
+func splitStringAt(str string, sep string) (before, after string) {
+	if s := strings.SplitN(str, sep, 2); len(s) == 2 {
+		return s[0], s[1]
+	}
+	return str, ""
+}
diff --git a/internal/crdbtest/key_schema_test.go b/internal/crdbtest/key_schema_test.go
new file mode 100644
index 00000000..a3b05e12
--- /dev/null
+++ b/internal/crdbtest/key_schema_test.go
@@ -0,0 +1,105 @@
+package crdbtest
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/cockroachdb/crlib/crstrings"
+	"github.com/cockroachdb/datadriven"
+	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/binfmt"
+	"github.com/cockroachdb/pebble/internal/treeprinter"
+	"github.com/cockroachdb/pebble/sstable/block"
+	"github.com/cockroachdb/pebble/sstable/colblk"
+	"github.com/stretchr/testify/require"
+)
+
+// TestKeySchema tests the cockroachKeyWriter and cockroachKeySeeker.
+func TestKeySchema(t *testing.T) {
+	for _, file := range []string{"suffix_types", "block_encoding", "seek"} {
+		t.Run(file, func(t *testing.T) {
+			runDataDrivenTest(t, fmt.Sprintf("testdata/%s", file))
+		})
+	}
+}
+
+func runDataDrivenTest(t *testing.T, path string) {
+	var blockData []byte
+	var e colblk.DataBlockEncoder
+	e.Init(&KeySchema)
+	var iter colblk.DataBlockIter
+	iter.InitOnce(&KeySchema, Comparer.Compare, Comparer.Split, nil)
+
+	datadriven.RunTest(t, path, func(t *testing.T, td *datadriven.TestData) string {
+		switch td.Cmd {
+		case "init":
+			e.Reset()
+			for _, l := range crstrings.Lines(td.Input) {
+				key, value := parseKV(l)
+				kcmp := e.KeyWriter.ComparePrev(key.UserKey)
+				e.Add(key, value, 0, kcmp, false /* isObsolete */)
+			}
+			numRows := e.Rows()
+			size := e.Size()
+			blockData, _ = e.Finish(numRows, size)
+			require.Equal(t, size, len(blockData))
+			return fmt.Sprintf("%d rows, total size %dB", numRows, size)
+
+		case "describe":
+			var d colblk.DataBlockDecoder
+			d.Init(&KeySchema, blockData)
+			f := binfmt.New(blockData)
+			tp := treeprinter.New()
+			d.Describe(f, tp)
+			return tp.String()
+
+		case "suffix-types":
+			var d colblk.DataBlockDecoder
+			d.Init(&KeySchema, blockData)
+			var ks cockroachKeySeeker
+			ks.init(&d)
+			return fmt.Sprintf("suffix-types: %s", ks.suffixTypes)
+
+		case "keys":
+			var d colblk.DataBlockDecoder
+			d.Init(&KeySchema, blockData)
+			require.NoError(t, iter.Init(&d, block.IterTransforms{}))
+			defer iter.Close()
+			var buf bytes.Buffer
+			var prevKey base.InternalKey
+			for kv := iter.First(); kv != nil; kv = iter.Next() {
+				fmt.Fprintf(&buf, "%s", formatKV(*kv))
+				if prevKey.UserKey != nil && base.InternalCompare(Comparer.Compare, prevKey, kv.K) != -1 {
+					buf.WriteString(" !!! OUT OF ORDER KEY !!!")
+				}
+				buf.WriteString("\n")
+				prevKey = kv.K.Clone()
+			}
+			return buf.String()
+
+		case "seek":
+			var d colblk.DataBlockDecoder
+			d.Init(&KeySchema, blockData)
+			require.NoError(t, iter.Init(&d, block.IterTransforms{}))
+			defer iter.Close()
+			var buf strings.Builder
+			for _, l := range crstrings.Lines(td.Input) {
+				key := parseUserKey(l)
+				fmt.Fprintf(&buf, "%s: ", formatUserKey(key))
+				kv := iter.SeekGE(key, base.SeekGEFlagsNone)
+				require.NoError(t, iter.Error())
+				if kv == nil {
+					buf.WriteString(".\n")
+				} else {
+					fmt.Fprintf(&buf, "%s\n", formatKV(*kv))
+				}
+			}
+			return buf.String()
+
+		default:
+			return fmt.Sprintf("unknown command: %s", td.Cmd)
+		}
+	})
+}
diff --git a/internal/crdbtest/testdata/block_encoding b/internal/crdbtest/testdata/block_encoding
new file mode 100644
index 00000000..95323170
--- /dev/null
+++ b/internal/crdbtest/testdata/block_encoding
@@ -0,0 +1,208 @@
+# Note that these versions are the same MVCC timestamp:
+#   0102030405060708
+#   0102030405060708000000000 (zero logical)
+#   01020304050607080000000001 (zero logical, synthetic bit)
+init
+foo #3,SET = nosuffix3
+foo #2,SET = nosuffix2
+foo #1,SET = nosuffix1
+foo @ 0102030405060708091011121314151617 #1,SET = lockkey
+foo @ 010203040506070801020304 #2,SET = mvcclogical2
+foo @ 01020304050607080102030401 #1,SET = mvcclogical1
+foo @ 010203040506070800000000 #4,SET = mvcc4
+foo @ 0102030405060708 #3,SET = mvcc3
+foo @ 01020304050607080000000001 #2,SET = mvcc2
+foo @ 0102030405060708 #1,SET = mvcc1
+fop #1,SET = foo
+x #1,SET = x
+----
+12 rows, total size 411B
+
+keys
+----
+foo #3,SET = nosuffix3
+foo #2,SET = nosuffix2
+foo #1,SET = nosuffix1
+foo @ 0102030405060708091011121314151617 #1,SET = lockkey
+foo @ 010203040506070801020304 #2,SET = mvcclogical2
+foo @ 010203040506070801020304 #1,SET = mvcclogical1
+foo @ 0102030405060708 #4,SET = mvcc4
+foo @ 0102030405060708 #3,SET = mvcc3
+foo @ 0102030405060708 #2,SET = mvcc2
+foo @ 0102030405060708 #1,SET = mvcc1
+fop #1,SET = foo
+x #1,SET = x
+
+describe
+----
+data block header
+ ├── columnar block header
+ │    ├── 000-001: x 07       # key schema header
+ │    ├── 001-005: x 16000000 # maximum key length: 22
+ │    ├── 005-006: x 01       # version 1
+ │    ├── 006-008: x 0900     # 9 columns
+ │    ├── 008-012: x 0c000000 # 12 rows
+ │    ├── 012-013: b 00000100 # col 0: prefixbytes
+ │    ├── 013-017: x 39000000 # col 0: page start 57
+ │    ├── 017-018: b 00000010 # col 1: uint
+ │    ├── 018-022: x 50000000 # col 1: page start 80
+ │    ├── 022-023: b 00000010 # col 2: uint
+ │    ├── 023-027: x b8000000 # col 2: page start 184
+ │    ├── 027-028: b 00000011 # col 3: bytes
+ │    ├── 028-032: x ec000000 # col 3: page start 236
+ │    ├── 032-033: b 00000010 # col 4: uint
+ │    ├── 033-037: x 0c010000 # col 4: page start 268
+ │    ├── 037-038: b 00000001 # col 5: bool
+ │    ├── 038-042: x 26010000 # col 5: page start 294
+ │    ├── 042-043: b 00000011 # col 6: bytes
+ │    ├── 043-047: x 38010000 # col 6: page start 312
+ │    ├── 047-048: b 00000001 # col 7: bool
+ │    ├── 048-052: x 98010000 # col 7: page start 408
+ │    ├── 052-053: b 00000001 # col 8: bool
+ │    └── 053-057: x 99010000 # col 8: page start 409
+ ├── data for column 0 (prefixbytes)
+ │    ├── 057-058: x 04 # bundle size: 16
+ │    ├── offsets table
+ │    │    ├── 058-059: x 01 # encoding: 1b
+ │    │    ├── 059-060: x 00 # data[0] = 0 [73 overall]
+ │    │    ├── 060-061: x 00 # data[1] = 0 [73 overall]
+ │    │    ├── 061-062: x 03 # data[2] = 3 [76 overall]
+ │    │    ├── 062-063: x 03 # data[3] = 3 [76 overall]
+ │    │    ├── 063-064: x 03 # data[4] = 3 [76 overall]
+ │    │    ├── 064-065: x 03 # data[5] = 3 [76 overall]
+ │    │    ├── 065-066: x 03 # data[6] = 3 [76 overall]
+ │    │    ├── 066-067: x 03 # data[7] = 3 [76 overall]
+ │    │    ├── 067-068: x 03 # data[8] = 3 [76 overall]
+ │    │    ├── 068-069: x 03 # data[9] = 3 [76 overall]
+ │    │    ├── 069-070: x 03 # data[10] = 3 [76 overall]
+ │    │    ├── 070-071: x 03 # data[11] = 3 [76 overall]
+ │    │    ├── 071-072: x 06 # data[12] = 6 [79 overall]
+ │    │    └── 072-073: x 07 # data[13] = 7 [80 overall]
+ │    └── data
+ │         ├── 073-073: x        # data[00]:  (block prefix)
+ │         ├── 073-073: x        # data[01]:  (bundle prefix)
+ │         ├── 073-076: x 666f6f # data[02]: foo
+ │         ├── 076-076: x        # data[03]: ...
+ │         ├── 076-076: x        # data[04]: ...
+ │         ├── 076-076: x        # data[05]: ...
+ │         ├── 076-076: x        # data[06]: ...
+ │         ├── 076-076: x        # data[07]: ...
+ │         ├── 076-076: x        # data[08]: ...
+ │         ├── 076-076: x        # data[09]: ...
+ │         ├── 076-076: x        # data[10]: ...
+ │         ├── 076-076: x        # data[11]: ...
+ │         ├── 076-079: x 666f70 # data[12]: fop
+ │         └── 079-080: x 78     # data[13]: x
+ ├── data for column 1 (uint)
+ │    ├── 080-081: x 08               # encoding: 8b
+ │    ├── 081-088: x 00000000000000   # padding (aligning to 64-bit boundary)
+ │    ├── 088-096: x 0000000000000000 # data[0] = 0
+ │    ├── 096-104: x 0000000000000000 # data[1] = 0
+ │    ├── 104-112: x 0000000000000000 # data[2] = 0
+ │    ├── 112-120: x 0000000000000000 # data[3] = 0
+ │    ├── 120-128: x 0807060504030201 # data[4] = 72623859790382856
+ │    ├── 128-136: x 0807060504030201 # data[5] = 72623859790382856
+ │    ├── 136-144: x 0807060504030201 # data[6] = 72623859790382856
+ │    ├── 144-152: x 0807060504030201 # data[7] = 72623859790382856
+ │    ├── 152-160: x 0807060504030201 # data[8] = 72623859790382856
+ │    ├── 160-168: x 0807060504030201 # data[9] = 72623859790382856
+ │    ├── 168-176: x 0000000000000000 # data[10] = 0
+ │    └── 176-184: x 0000000000000000 # data[11] = 0
+ ├── data for column 2 (uint)
+ │    ├── 184-185: x 04       # encoding: 4b
+ │    ├── 185-188: x 000000   # padding (aligning to 32-bit boundary)
+ │    ├── 188-192: x 00000000 # data[0] = 0
+ │    ├── 192-196: x 00000000 # data[1] = 0
+ │    ├── 196-200: x 00000000 # data[2] = 0
+ │    ├── 200-204: x 00000000 # data[3] = 0
+ │    ├── 204-208: x 04030201 # data[4] = 16909060
+ │    ├── 208-212: x 04030201 # data[5] = 16909060
+ │    ├── 212-216: x 00000000 # data[6] = 0
+ │    ├── 216-220: x 00000000 # data[7] = 0
+ │    ├── 220-224: x 00000000 # data[8] = 0
+ │    ├── 224-228: x 00000000 # data[9] = 0
+ │    ├── 228-232: x 00000000 # data[10] = 0
+ │    └── 232-236: x 00000000 # data[11] = 0
+ ├── data for column 3 (bytes)
+ │    ├── offsets table
+ │    │    ├── 236-237: x 01 # encoding: 1b
+ │    │    ├── 237-238: x 00 # data[0] = 0 [250 overall]
+ │    │    ├── 238-239: x 00 # data[1] = 0 [250 overall]
+ │    │    ├── 239-240: x 00 # data[2] = 0 [250 overall]
+ │    │    ├── 240-241: x 00 # data[3] = 0 [250 overall]
+ │    │    ├── 241-242: x 12 # data[4] = 18 [268 overall]
+ │    │    ├── 242-243: x 12 # data[5] = 18 [268 overall]
+ │    │    ├── 243-244: x 12 # data[6] = 18 [268 overall]
+ │    │    ├── 244-245: x 12 # data[7] = 18 [268 overall]
+ │    │    ├── 245-246: x 12 # data[8] = 18 [268 overall]
+ │    │    ├── 246-247: x 12 # data[9] = 18 [268 overall]
+ │    │    ├── 247-248: x 12 # data[10] = 18 [268 overall]
+ │    │    ├── 248-249: x 12 # data[11] = 18 [268 overall]
+ │    │    └── 249-250: x 12 # data[12] = 18 [268 overall]
+ │    └── data
+ │         ├── 250-250: x                                      # data[0]:
+ │         ├── 250-250: x                                      # data[1]:
+ │         ├── 250-250: x                                      # data[2]:
+ │         ├── 250-268: x 010203040506070809101112131415161712 # data[3]: "\x01\x02\x03\x04\x05\x06\a\b\t\x10\x11\x12\x13\x14\x15\x16\x17\x12"
+ │         ├── 268-268: x                                      # data[4]:
+ │         ├── 268-268: x                                      # data[5]:
+ │         ├── 268-268: x                                      # data[6]:
+ │         ├── 268-268: x                                      # data[7]:
+ │         ├── 268-268: x                                      # data[8]:
+ │         ├── 268-268: x                                      # data[9]:
+ │         ├── 268-268: x                                      # data[10]:
+ │         └── 268-268: x                                      # data[11]:
+ ├── data for column 4 (uint)
+ │    ├── 268-269: x 02   # encoding: 2b
+ │    ├── 269-270: x 00   # padding (aligning to 16-bit boundary)
+ │    ├── 270-272: x 0103 # data[0] = 769
+ │    ├── 272-274: x 0102 # data[1] = 513
+ │    ├── 274-276: x 0101 # data[2] = 257
+ │    ├── 276-278: x 0101 # data[3] = 257
+ │    ├── 278-280: x 0102 # data[4] = 513
+ │    ├── 280-282: x 0101 # data[5] = 257
+ │    ├── 282-284: x 0104 # data[6] = 1025
+ │    ├── 284-286: x 0103 # data[7] = 769
+ │    ├── 286-288: x 0102 # data[8] = 513
+ │    ├── 288-290: x 0101 # data[9] = 257
+ │    ├── 290-292: x 0101 # data[10] = 257
+ │    └── 292-294: x 0101 # data[11] = 257
+ ├── data for column 5 (bool)
+ │    ├── 294-295: x 00                                                               # default bitmap encoding
+ │    ├── 295-296: x 00                                                               # padding to align to 64-bit boundary
+ │    ├── 296-304: b 0000000100001100000000000000000000000000000000000000000000000000 # bitmap word 0
+ │    └── 304-312: b 0000000100000000000000000000000000000000000000000000000000000000 # bitmap summary word 0-63
+ ├── data for column 6 (bytes)
+ │    ├── offsets table
+ │    │    ├── 312-313: x 01 # encoding: 1b
+ │    │    ├── 313-314: x 00 # data[0] = 0 [326 overall]
+ │    │    ├── 314-315: x 09 # data[1] = 9 [335 overall]
+ │    │    ├── 315-316: x 12 # data[2] = 18 [344 overall]
+ │    │    ├── 316-317: x 1b # data[3] = 27 [353 overall]
+ │    │    ├── 317-318: x 22 # data[4] = 34 [360 overall]
+ │    │    ├── 318-319: x 2e # data[5] = 46 [372 overall]
+ │    │    ├── 319-320: x 3a # data[6] = 58 [384 overall]
+ │    │    ├── 320-321: x 3f # data[7] = 63 [389 overall]
+ │    │    ├── 321-322: x 44 # data[8] = 68 [394 overall]
+ │    │    ├── 322-323: x 49 # data[9] = 73 [399 overall]
+ │    │    ├── 323-324: x 4e # data[10] = 78 [404 overall]
+ │    │    ├── 324-325: x 51 # data[11] = 81 [407 overall]
+ │    │    └── 325-326: x 52 # data[12] = 82 [408 overall]
+ │    └── data
+ │         ├── 326-335: x 6e6f73756666697833       # data[0]: nosuffix3
+ │         ├── 335-344: x 6e6f73756666697832       # data[1]: nosuffix2
+ │         ├── 344-353: x 6e6f73756666697831       # data[2]: nosuffix1
+ │         ├── 353-360: x 6c6f636b6b6579           # data[3]: lockkey
+ │         ├── 360-372: x 6d7663636c6f676963616c32 # data[4]: mvcclogical2
+ │         ├── 372-384: x 6d7663636c6f676963616c31 # data[5]: mvcclogical1
+ │         ├── 384-389: x 6d76636334               # data[6]: mvcc4
+ │         ├── 389-394: x 6d76636333               # data[7]: mvcc3
+ │         ├── 394-399: x 6d76636332               # data[8]: mvcc2
+ │         ├── 399-404: x 6d76636331               # data[9]: mvcc1
+ │         ├── 404-407: x 666f6f                   # data[10]: foo
+ │         └── 407-408: x 78                       # data[11]: x
+ ├── data for column 7 (bool)
+ │    └── 408-409: x 01 # zero bitmap encoding
+ ├── data for column 8 (bool)
+ │    └── 409-410: x 01 # zero bitmap encoding
+ └── 410-411: x 00 # block padding byte
diff --git a/internal/crdbtest/testdata/seek b/internal/crdbtest/testdata/seek
new file mode 100644
index 00000000..eb0f0e8b
--- /dev/null
+++ b/internal/crdbtest/testdata/seek
@@ -0,0 +1,133 @@
+# Test seeking with MVCC versions.
+init
+fo @ 0102030405060708 #1,SET = row0
+foo @ 0102030405060708 #1,SET = row1
+foo @ 010203040506070701020304 #2,SET = row2
+foo @ 010203040506070701020304 #1,SET = row3
+foo @ 0102030405060707 #1,SET = row4
+foop @ 0102030405060708 #1,SET = row5
+fop @ 0102030405060708 #1,SET = row6
+----
+7 rows, total size 200B
+
+seek
+fo
+fo @ 05
+fo @ 05
+fo @ 0102030405060709 #1,SET = v0
+fo @ 0102030405060708 #1,SET = v0
+fo @ 0102030405060707 #1,SET = v0
+----
+fo: fo @ 0102030405060708 #1,SET = row0
+fo @ 05: fo @ 0102030405060708 #1,SET = row0
+fo @ 05: fo @ 0102030405060708 #1,SET = row0
+fo @ 0102030405060709: fo @ 0102030405060708 #1,SET = row0
+fo @ 0102030405060708: fo @ 0102030405060708 #1,SET = row0
+fo @ 0102030405060707: foo @ 0102030405060708 #1,SET = row1
+
+seek
+foo
+foo @ 0102030405060708
+foo @ 010203040506070701020305
+foo @ 010203040506070701020304
+foo @ 010203040506070701020303
+foo @ 0102030405060707
+----
+foo: foo @ 0102030405060708 #1,SET = row1
+foo @ 0102030405060708: foo @ 0102030405060708 #1,SET = row1
+foo @ 010203040506070701020305: foo @ 010203040506070701020304 #2,SET = row2
+foo @ 010203040506070701020304: foo @ 010203040506070701020304 #2,SET = row2
+foo @ 010203040506070701020303: foo @ 0102030405060707 #1,SET = row4
+foo @ 0102030405060707: foo @ 0102030405060707 #1,SET = row4
+
+seek
+foop
+foop @ 0102030405060708
+foop @ 0102030405060707
+fop
+foq
+----
+foop: foop @ 0102030405060708 #1,SET = row5
+foop @ 0102030405060708: foop @ 0102030405060708 #1,SET = row5
+foop @ 0102030405060707: fop @ 0102030405060708 #1,SET = row6
+fop: fop @ 0102030405060708 #1,SET = row6
+foq: .
+
+# Test seeking with no versions.
+init
+fo #2,SET = row0
+fo #1,SET = row1
+foo #1,DEL
+fop #1,DEL
+foq #1,SET = row4
+fz #1,SET = row5
+----
+6 rows, total size 139B
+
+seek
+a
+fo
+foo
+foo1
+fop
+fop1
+foq
+fu
+g
+----
+a: fo #2,SET = row0
+fo: fo #2,SET = row0
+foo: foo #1,DEL
+foo1: fop #1,DEL
+fop: fop #1,DEL
+fop1: foq #1,SET = row4
+foq: foq #1,SET = row4
+fu: fz #1,SET = row5
+g: .
+
+# Test seeking among lock keys.
+init
+fo @ 0000000000000000000000000000000002 #1,SET = row0
+foo @ 0000000000000000000000000000000010 #1,SET = row1
+foo @ 0000000000000000000000000000000008 #1,SET = row2
+foo @ 0000000000000000000000000000000002 #1,SET = row3
+foo @ 0000000000000000000000000000000001 #1,SET = row4
+z @ 0000000000000000000000000000000001 #1,SET = row5
+----
+6 rows, total size 259B
+
+seek
+a
+fo
+fo @ 0000000000000000000000000000000003
+fo @ 0000000000000000000000000000000002
+fo @ 0000000000000000000000000000000001
+----
+a: fo @ 0000000000000000000000000000000002 #1,SET = row0
+fo: fo @ 0000000000000000000000000000000002 #1,SET = row0
+fo @ 0000000000000000000000000000000003: fo @ 0000000000000000000000000000000002 #1,SET = row0
+fo @ 0000000000000000000000000000000002: fo @ 0000000000000000000000000000000002 #1,SET = row0
+fo @ 0000000000000000000000000000000001: foo @ 0000000000000000000000000000000010 #1,SET = row1
+
+seek
+foo
+foo @ 0000000000000000000000000000000015
+foo @ 0000000000000000000000000000000010
+foo @ 0000000000000000000000000000000009
+foo @ 0000000000000000000000000000000008
+foo @ 0000000000000000000000000000000007
+foo @ 0000000000000000000000000000000002
+foo @ 0000000000000000000000000000000001
+foo @ 0000000000000000000000000000000000
+----
+foo: foo @ 0000000000000000000000000000000010 #1,SET = row1
+foo @ 0000000000000000000000000000000015: foo @ 0000000000000000000000000000000010 #1,SET = row1
+foo @ 0000000000000000000000000000000010: foo @ 0000000000000000000000000000000010 #1,SET = row1
+foo @ 0000000000000000000000000000000009: foo @ 0000000000000000000000000000000008 #1,SET = row2
+foo @ 0000000000000000000000000000000008: foo @ 0000000000000000000000000000000008 #1,SET = row2
+foo @ 0000000000000000000000000000000007: foo @ 0000000000000000000000000000000002 #1,SET = row3
+foo @ 0000000000000000000000000000000002: foo @ 0000000000000000000000000000000002 #1,SET = row3
+foo @ 0000000000000000000000000000000001: foo @ 0000000000000000000000000000000001 #1,SET = row4
+foo @ 0000000000000000000000000000000000: z @ 0000000000000000000000000000000001 #1,SET = row5
+
+# TODO(radu): tests with mixed types of versions.
diff --git a/internal/crdbtest/testdata/suffix_types b/internal/crdbtest/testdata/suffix_types
new file mode 100644
index 00000000..004ea03f
--- /dev/null
+++ b/internal/crdbtest/testdata/suffix_types
@@ -0,0 +1,71 @@
+# This file contains tests that verifies the suffixTypes indicator encoded in
+# the key schema prefix.
+
+init
+foo #1,SET = bar
+----
+1 rows, total size 105B
+
+suffix-types
+----
+suffix-types: empty
+
+init
+foo @ 0102030405060708 #1,SET = bar
+foo @ 010203040506070801020304 #1,SET = bar
+----
+2 rows, total size 125B
+
+suffix-types
+----
+suffix-types: mvcc
+
+init
+foo @ 0102030405060708091011121314151617 #1,SET = bar
+----
+1 rows, total size 121B
+
+suffix-types
+----
+suffix-types: non-mvcc
+
+init
+foo #1,SET = bar
+foo @ 0102030405060708 #1,SET = bar
+----
+2 rows, total size 133B
+
+suffix-types
+----
+suffix-types: mvcc,empty
+
+init
+foo @ 0102030405060708 #1,SET = bar
+foo @ 0102030405060708091011121314151617 #1,SET = bar
+----
+2 rows, total size 149B
+
+suffix-types
+----
+suffix-types: mvcc,non-mvcc
+
+init
+foo #1,SET = bar
+foo @ 0102030405060708091011121314151617 #1,SET = bar
+----
+2 rows, total size 133B
+
+suffix-types
+----
+suffix-types: empty,non-mvcc
+
+init
+foo #1,SET = bar
+foo @ 0102030405060708 #1,SET = bar
+foo @ 0102030405060708091011121314151617 #1,SET = bar
+----
+3 rows, total size 169B
+
+suffix-types
+----
+suffix-types: mvcc,empty,non-mvcc
diff --git a/level_iter_test.go b/level_iter_test.go
index c52efaa8..8a7f51d7 100644
--- a/level_iter_test.go
+++ b/level_iter_test.go
@@ -184,7 +184,7 @@ func (lt *levelIterTest) newIters(
 	if kinds.Point() {
 		iter, err := lt.readers[file.FileNum].NewPointIter(
 			ctx, transforms,
-			opts.LowerBound, opts.UpperBound, nil, sstable.AlwaysUseFilterBlock, iio.stats, sstable.CategoryAndQoS{},
+			opts.LowerBound, opts.UpperBound, nil, sstable.AlwaysUseFilterBlock, iio.stats,
 			nil, sstable.MakeTrivialReaderProvider(lt.readers[file.FileNum]))
 		if err != nil {
 			return iterSet{}, errors.CombineErrors(err, set.CloseAll())
diff --git a/merging_iter_test.go b/merging_iter_test.go
index 2e719a39..d8378ee5 100644
--- a/merging_iter_test.go
+++ b/merging_iter_test.go
@@ -179,7 +179,7 @@ func TestMergingIterDataDriven(t *testing.T) {
 					context.Background(),
 					sstable.NoTransforms,
 					opts.GetLowerBound(), opts.GetUpperBound(), nil, sstable.AlwaysUseFilterBlock, iio.stats,
-					sstable.CategoryAndQoS{}, nil, sstable.MakeTrivialReaderProvider(r))
+					nil, sstable.MakeTrivialReaderProvider(r))
 				if err != nil {
 					return iterSet{}, errors.CombineErrors(err, set.CloseAll())
 				}
diff --git a/sstable/block_property_test.go b/sstable/block_property_test.go
index 17ca33b1..5abada5c 100644
--- a/sstable/block_property_test.go
+++ b/sstable/block_property_test.go
@@ -988,7 +988,7 @@ func TestBlockProperties(t *testing.T) {
 			iter, err := r.NewPointIter(
 				context.Background(),
 				NoTransforms, lower, upper, filterer, NeverUseFilterBlock, &stats,
-				CategoryAndQoS{}, nil, MakeTrivialReaderProvider(r))
+				nil, MakeTrivialReaderProvider(r))
 			if err != nil {
 				return err.Error()
 			}
@@ -1072,7 +1072,7 @@ func TestBlockProperties_BoundLimited(t *testing.T) {
 			iter, err := r.NewPointIter(
 				context.Background(),
 				NoTransforms, lower, upper, filterer, NeverUseFilterBlock, &stats,
-				CategoryAndQoS{}, nil, MakeTrivialReaderProvider(r))
+				nil, MakeTrivialReaderProvider(r))
 			if err != nil {
 				return err.Error()
 			}
diff --git a/sstable/random_test.go b/sstable/random_test.go
index fd983d4e..36bc9ae0 100644
--- a/sstable/random_test.go
+++ b/sstable/random_test.go
@@ -111,8 +111,7 @@ func runErrorInjectionTest(t *testing.T, seed int64) {
 		filterer,
 		filterBlockSizeLimit,
 		&stats,
-		CategoryAndQoS{},
-		nil, /* CategoryStatsCollector */
+		nil, /* IterStatsAccumulator */
 		MakeTrivialReaderProvider(r),
 	)
 	require.NoError(t, err)
diff --git a/sstable/reader_iter_test.go b/sstable/reader_iter_test.go
index a16793ce..28fae7eb 100644
--- a/sstable/reader_iter_test.go
+++ b/sstable/reader_iter_test.go
@@ -64,8 +64,7 @@ func TestIteratorErrorOnInit(t *testing.T) {
 				nil /* lower */, nil, /* upper */
 				nil /* filterer */, NeverUseFilterBlock,
 				&stats,
-				CategoryAndQoS{},
-				nil, /* statsCollector */
+				nil, /* statsAccum */
 				MakeTrivialReaderProvider(r),
 				&pool,
 			)
@@ -79,8 +78,7 @@ func TestIteratorErrorOnInit(t *testing.T) {
 				nil /* lower */, nil, /* upper */
 				nil /* filterer */, NeverUseFilterBlock,
 				&stats,
-				CategoryAndQoS{},
-				nil, /* statsCollector */
+				nil, /* statsAccum */
 				MakeTrivialReaderProvider(r),
 				&pool,
 			)
diff --git a/sstable/reader_test.go b/sstable/reader_test.go
index f05f4828..c244a833 100644
--- a/sstable/reader_test.go
+++ b/sstable/reader_test.go
@@ -234,7 +234,7 @@ func runVirtualReaderTest(t *testing.T, path string, blockSize, indexBlockSize i
 			transforms := IterTransforms{
 				SyntheticPrefixAndSuffix: block.MakeSyntheticPrefixAndSuffix(nil, syntheticSuffix),
 			}
-			iter, err := v.NewCompactionIter(transforms, CategoryAndQoS{}, nil, rp, &bp)
+			iter, err := v.NewCompactionIter(transforms, nil, rp, &bp)
 			if err != nil {
 				return err.Error()
 			}
@@ -361,7 +361,7 @@ func runVirtualReaderTest(t *testing.T, path string, blockSize, indexBlockSize i
 			}
 			iter, err := v.NewPointIter(
 				context.Background(), transforms, lower, upper, filterer, NeverUseFilterBlock,
-				&stats, CategoryAndQoS{}, nil, MakeTrivialReaderProvider(r))
+				&stats, nil, MakeTrivialReaderProvider(r))
 			if err != nil {
 				return err.Error()
 			}
@@ -755,7 +755,6 @@ func runTestReader(t *testing.T, o WriterOptions, dir string, r *Reader, printVa
 					filterer,
 					AlwaysUseFilterBlock,
 					&stats,
-					CategoryAndQoS{},
 					nil,
 					MakeTrivialReaderProvider(r),
 				)
@@ -899,7 +898,7 @@ func TestCompactionIteratorSetupForCompaction(t *testing.T) {
 				var pool block.BufferPool
 				pool.Init(5)
 				citer, err := r.NewCompactionIter(
-					NoTransforms, CategoryAndQoS{}, nil, MakeTrivialReaderProvider(r), &pool)
+					NoTransforms, nil, MakeTrivialReaderProvider(r), &pool)
 				require.NoError(t, err)
 				switch i := citer.(type) {
 				case *singleLevelIteratorRowBlocks:
@@ -957,7 +956,7 @@ func TestReadaheadSetupForV3TablesWithMultipleVersions(t *testing.T) {
 		pool.Init(5)
 		defer pool.Release()
 		citer, err := r.NewCompactionIter(
-			NoTransforms, CategoryAndQoS{}, nil, MakeTrivialReaderProvider(r), &pool)
+			NoTransforms, nil, MakeTrivialReaderProvider(r), &pool)
 		require.NoError(t, err)
 		defer citer.Close()
 		i := citer.(*singleLevelIteratorRowBlocks)
@@ -1254,7 +1253,7 @@ func TestRandomizedPrefixSuffixRewriter(t *testing.T) {
 				SyntheticPrefixAndSuffix: block.MakeSyntheticPrefixAndSuffix(syntheticPrefix, syntheticSuffix),
 			},
 			nil, nil, nil,
-			AlwaysUseFilterBlock, nil, CategoryAndQoS{}, nil,
+			AlwaysUseFilterBlock, nil, nil,
 			MakeTrivialReaderProvider(eReader), &virtualState{
 				lower: base.MakeInternalKey([]byte("_"), base.SeqNumMax, base.InternalKeyKindSet),
 				upper: base.MakeRangeDeleteSentinelKey([]byte("~~~~~~~~~~~~~~~~")),
@@ -2401,7 +2400,7 @@ func BenchmarkIteratorScanObsolete(b *testing.B) {
 								transforms := IterTransforms{HideObsoletePoints: hideObsoletePoints}
 								iter, err := r.NewPointIter(
 									context.Background(), transforms, nil, nil, filterer,
-									AlwaysUseFilterBlock, nil, CategoryAndQoS{}, nil,
+									AlwaysUseFilterBlock, nil, nil,
 									MakeTrivialReaderProvider(r))
 								require.NoError(b, err)
 								b.ResetTimer()
diff --git a/testdata/metrics b/testdata/metrics
index cd66bfe2..360a8a56 100644
--- a/testdata/metrics
+++ b/testdata/metrics
@@ -83,6 +83,9 @@ Table iters: 1
 Filter utility: 0.0%
 Ingestions: 0  as flushable: 0 (0B in 0 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
+Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
+                   b,     latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
 
 disk-usage
 ----
@@ -141,6 +144,9 @@ Filter utility: 0.0%
 Ingestions: 0  as flushable: 0 (0B in 0 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
+                   b,     latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
+                   c, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:88 BlockBytesInCache:44 BlockReadDuration:10ms}
 
 disk-usage
@@ -185,6 +191,9 @@ Filter utility: 0.0%
 Ingestions: 0  as flushable: 0 (0B in 0 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
+                   b,     latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
+                   c, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:88 BlockBytesInCache:44 BlockReadDuration:10ms}
 
 # Closing iter c will release one of the zombie sstables. The other
@@ -226,6 +235,8 @@ Filter utility: 0.0%
 Ingestions: 0  as flushable: 0 (0B in 0 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
+                   b,     latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:88 BlockBytesInCache:44 BlockReadDuration:10ms}
 
@@ -271,6 +282,7 @@ Filter utility: 0.0%
 Ingestions: 0  as flushable: 0 (0B in 0 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:88 BlockBytesInCache:44 BlockReadDuration:10ms}
@@ -343,6 +355,7 @@ Filter utility: 0.0%
 Ingestions: 0  as flushable: 0 (0B in 0 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:88 BlockBytesInCache:44 BlockReadDuration:10ms}
@@ -399,6 +412,7 @@ Filter utility: 0.0%
 Ingestions: 0  as flushable: 0 (0B in 0 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:301 BlockBytesInCache:44 BlockReadDuration:60ms}
@@ -504,6 +518,7 @@ Filter utility: 0.0%
 Ingestions: 2  as flushable: 2 (1.7KB in 3 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:301 BlockBytesInCache:44 BlockReadDuration:60ms}
@@ -568,6 +583,7 @@ Filter utility: 0.0%
 Ingestions: 2  as flushable: 2 (1.7KB in 3 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:301 BlockBytesInCache:44 BlockReadDuration:60ms}
@@ -646,6 +662,7 @@ Filter utility: 0.0%
 Ingestions: 3  as flushable: 2 (1.7KB in 3 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:301 BlockBytesInCache:44 BlockReadDuration:60ms}
@@ -749,6 +766,7 @@ Filter utility: 0.0%
 Ingestions: 4  as flushable: 2 (1.7KB in 3 tables)
 Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtables: 0B
 Iter category stats:
+                   a, non-latency: {BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s}
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:677 BlockBytesInCache:376 BlockReadDuration:70ms}