Repository: cockroachdb/pebble

Issue:
db: use flushable ingest despite high backlog of memtables due to WAL failover
See the discussion in https://cockroachlabs.slack.com/archives/CAC6K3SLU/p1750663898215949?thread_ts=1750420169.044609&cid=CAC6K3SLU relating to the investigation of the roachtest failure https://github.com/cockroachdb/cockroach/issues/148562 for details.

We observed an ingest taking 16s:
```
I250619 12:32:41.179246 128870 3@pebble/event.go:981 â‹® [n12,s24,pebble] 66659  [JOB 5548] ingesting: sstable created 013714
I250619 12:32:41.179291 128870 3@pebble/event.go:981 â‹® [n12,s24,pebble] 66660  [JOB 5548] ingesting: sstable created 013721
I250619 12:32:41.179314 128870 3@pebble/event.go:981 â‹® [n12,s24,pebble] 66661  [JOB 5548] ingesting: sstable created 013715
I250619 12:32:41.179339 128870 3@pebble/event.go:981 â‹® [n12,s24,pebble] 66662  [JOB 5548] ingesting: sstable created 013716
I250619 12:32:41.179358 128870 3@pebble/event.go:981 â‹® [n12,s24,pebble] 66663  [JOB 5548] ingesting: sstable created 013717
I250619 12:32:41.179374 128870 3@pebble/event.go:981 â‹® [n12,s24,pebble] 66664  [JOB 5548] ingesting: sstable created 013718
I250619 12:32:41.179393 128870 3@pebble/event.go:981 â‹® [n12,s24,pebble] 66665  [JOB 5548] ingesting: sstable created 013719
I250619 12:32:41.179410 128870 3@pebble/event.go:981 â‹® [n12,s24,pebble] 66666  [JOB 5548] ingesting: sstable created 013720

... lots of other activity ...

I250619 12:32:57.628957 138580 3@pebble/event.go:957 ⋮ [T1,n12,s24,pebble] 73725  [JOB 5428] flushed 30 memtables (1.8GB) to L0 [013646 013647 013650 013651 013675 013676 013677 013679 013680 013681 013683 013684 013686 013687 01
...] (1.6GB), in 17.0s (17.0s total), output rate 99MB/s

I250619 12:32:57.940369 128870 3@pebble/event.go:989 â‹® [n12,s24,pebble] 73983  [JOB 5548] ingested L5:013714 (1.4KB), L0:013721 (1.0KB), L5:013715 (1.6KB), L5:013716 (1.0KB), L6:013717 (1016B), L6:013718 (128MB), L6:013719 (128MB), L6:013720 (122MB), L0:014993 (550KB), L0:014994 (1.5MB), L3:014995 (1.8MB), L3:014996 (2.2MB), L4:014997 (2.7MB), L4:014998 (1.6MB), L5:014999 (13MB), L5:015000 (8.6MB), L6:015001 (14MB), L6:015002 (21MB), L5:015003 (60KB), L5:015004 (30KB), L5:015005 (31KB), L5:015006 (16MB)
```

The flush backlog developed due to WAL failover, during which we allow unlimited memtable backlog. 
The ingest must have overlapped with some data in the memtables. The waiting in the ingest happens after the ingest grabs a `seqNum` so it prevents later regular writes (with a later seqNum) from becoming visible:
```
1371:I250619 12:32:57.940540 707 kv/kvserver/replica_raft.go:1826 ⋮ [T1,Vsystem,n12,s24,r108/1:‹/Table/106/1/-2{79496…-67105…}›,raft] 1259  slow non-blocking raft commit: commit-wait 16.748319254s sem 262ns
```

Normally, we would expect this ingest to be a flushable-ingest, because it excises the data below it, and this is one of those:
```
I250619 12:32:57.941611 128870 kv/kvserver/replica_raftstorage.go:527 â‹® [T1,Vsystem,n12,s24,r245/1:â€¹/Table/106/1/-{11187â€¦-83848â€¦}â€º] 1284  applied snapshot 90d17928 from (n3,s5):4 at applied index 12701 (total=16780ms data=377 MiB excise=true ingestion=8@16762ms)
```

It was not a flushable-ingest since the flushable queue is considered full due to this condition: https://github.com/cockroachdb/pebble/blob/f4e9b0c046e3de98a58d3f734ed2b79dc64be3f5/ingest.go#L1594

But since we are not imposing a memtable threshold for write stalls due to WAL failover, we should not be preventing the flushable-ingest either.

Hints from issue comments:

Solution patch:
diff --git a/.github/workflows/instrumented.yaml b/.github/workflows/instrumented.yaml
index 6ca08197..083d27eb 100644
--- a/.github/workflows/instrumented.yaml
+++ b/.github/workflows/instrumented.yaml
@@ -49,7 +49,7 @@ jobs:
       - run: GOTRACEBACK=all make testrace TAGS=
 
       - name: Post issue on failure
-        if: failure() && inputs.file_issue
+        if: failure() && inputs.file_issue_branch != ''
         id: create-or-update-unique-issue
         uses: ./.github/actions/post-issue
         with:
@@ -73,7 +73,7 @@ jobs:
       - run: make testasan
 
       - name: Post issue on failure
-        if: failure() && inputs.file_issue
+        if: failure() && inputs.file_issue_branch != ''
         id: create-or-update-unique-issue
         uses: ./.github/actions/post-issue
         with:
@@ -97,7 +97,7 @@ jobs:
       - run: make testmsan
 
       - name: Post issue on failure
-        if: failure() && inputs.file_issue
+        if: failure() && inputs.file_issue_branch != ''
         id: create-or-update-unique-issue
         uses: ./.github/actions/post-issue
         with:
diff --git a/.github/workflows/stress.yaml b/.github/workflows/stress.yaml
index f71b35b9..bd1c8946 100644
--- a/.github/workflows/stress.yaml
+++ b/.github/workflows/stress.yaml
@@ -52,7 +52,7 @@ jobs:
           scripts/stress.sh
 
       - name: Post issue on failure
-        if: failure() && inputs.file_issue
+        if: failure() && inputs.file_issue_branch != ''
         id: create-or-update-unique-issue
         uses: ./.github/actions/post-issue
         with:
diff --git a/blob_rewrite.go b/blob_rewrite.go
new file mode 100644
index 00000000..8987df5e
--- /dev/null
+++ b/blob_rewrite.go
@@ -0,0 +1,530 @@
+// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use
+// of this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+
+package pebble
+
+import (
+	"container/heap"
+	"context"
+	"iter"
+	"runtime/pprof"
+	"slices"
+	"sync/atomic"
+	"time"
+
+	"github.com/cockroachdb/errors"
+	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/manifest"
+	"github.com/cockroachdb/pebble/internal/problemspans"
+	"github.com/cockroachdb/pebble/objstorage"
+	"github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing"
+	"github.com/cockroachdb/pebble/sstable"
+	"github.com/cockroachdb/pebble/sstable/blob"
+	"github.com/cockroachdb/pebble/sstable/block"
+	"github.com/cockroachdb/pebble/sstable/colblk"
+)
+
+// A pickedBlobFileCompaction is a blob file rewrite compaction that has been
+// picked by the compaction picker.
+type pickedBlobFileCompaction struct {
+	vers              *manifest.Version
+	file              manifest.BlobFileMetadata
+	referencingTables []*manifest.TableMetadata
+}
+
+// Assert that *pickedBlobFileCompaction implements the pickedCompaction
+// interface.
+var _ pickedCompaction = (*pickedBlobFileCompaction)(nil)
+
+func (c *pickedBlobFileCompaction) ManualID() uint64 { return 0 }
+
+func (c *pickedBlobFileCompaction) WaitingCompaction() WaitingCompaction {
+	entry := scheduledCompactionMap[compactionKindBlobFileRewrite]
+	return WaitingCompaction{
+		Optional: entry.optional,
+		Priority: entry.priority,
+	}
+}
+
+func (c *pickedBlobFileCompaction) ConstructCompaction(
+	d *DB, grantHandle CompactionGrantHandle,
+) compaction {
+	return &blobFileRewriteCompaction{
+		beganAt:           d.timeNow(),
+		grantHandle:       grantHandle,
+		version:           c.vers,
+		input:             c.file,
+		referencingTables: c.referencingTables,
+		objCreateOpts: objstorage.CreateOptions{
+			// TODO(jackson): Enable shared storage for blob files.
+			PreferSharedStorage: false,
+			WriteCategory:       getDiskWriteCategoryForCompaction(d.opts, compactionKindBlobFileRewrite),
+		},
+	}
+}
+
+// A blobFileRewriteCompaction is a special variant of a compaction that
+// rewrites a blob file without rewriting sstables. When the compaction
+// completes, the Version's mapping of blob file ID to disk file number is
+// updated to point to the new blob file. The blob file is rewritten without
+// copying over values that are no longer referenced by any tables, reclaiming
+// disk space.
+type blobFileRewriteCompaction struct {
+	// cancel is a bool that can be used by other goroutines to signal a compaction
+	// to cancel, such as if a conflicting excise operation raced it to manifest
+	// application. Only holders of the manifest lock will write to this atomic.
+	cancel atomic.Bool
+	// beganAt is the time when the compaction began.
+	beganAt time.Time
+	// grantHandle is a handle to the compaction that can be used to track
+	// progress.
+	grantHandle CompactionGrantHandle
+	// version is a referenced version obtained when the compaction was picked.
+	// This version must be unreferenced when the compaction is complete.
+	version *manifest.Version
+	// versionEditApplied is set to true when a compaction has completed and the
+	// resulting version has been installed (if successful), but the compaction
+	// goroutine is still cleaning up (eg, deleting obsolete files).
+	versionEditApplied bool
+	// input is the blob file that is being rewritten.
+	input manifest.BlobFileMetadata
+	// referencingTables is the set of sstables that reference the input blob
+	// file in version.
+	referencingTables     []*manifest.TableMetadata
+	objCreateOpts         objstorage.CreateOptions
+	internalIteratorStats base.InternalIteratorStats
+}
+
+// Assert that *blobFileRewriteCompaction implements the Compaction interface.
+var _ compaction = (*blobFileRewriteCompaction)(nil)
+
+func (c *blobFileRewriteCompaction) AddInProgressLocked(d *DB) {
+	d.mu.compact.inProgress[c] = struct{}{}
+	// TODO(jackson): Currently the compaction picker iterates through all
+	// ongoing compactions in order to limit the number of concurrent blob
+	// rewrite compactions to 1.
+	//
+	// Consider instead tracking which blob files are being rewritten, and we
+	// can allow multiple concurrent blob rewrite compactions as long as they
+	// compact different blob files.
+}
+
+func (c *blobFileRewriteCompaction) BeganAt() time.Time                 { return c.beganAt }
+func (c *blobFileRewriteCompaction) Bounds() *base.UserKeyBounds        { return nil }
+func (c *blobFileRewriteCompaction) Cancel()                            { c.cancel.Store(true) }
+func (c *blobFileRewriteCompaction) IsDownload() bool                   { return false }
+func (c *blobFileRewriteCompaction) IsFlush() bool                      { return false }
+func (c *blobFileRewriteCompaction) GrantHandle() CompactionGrantHandle { return c.grantHandle }
+func (c *blobFileRewriteCompaction) Tables() iter.Seq2[int, *manifest.TableMetadata] {
+	// No tables; return an empty iterator.
+	return func(yield func(int, *manifest.TableMetadata) bool) {}
+}
+
+func (c *blobFileRewriteCompaction) ObjioTracingContext(ctx context.Context) context.Context {
+	if objiotracing.Enabled {
+		ctx = objiotracing.WithReason(ctx, objiotracing.ForCompaction)
+	}
+	return ctx
+}
+
+func (c *blobFileRewriteCompaction) PprofLabels(UserKeyCategories) pprof.LabelSet {
+	return pprof.Labels("pebble", "blob-rewrite")
+}
+
+func (c *blobFileRewriteCompaction) VersionEditApplied() bool {
+	return c.versionEditApplied
+}
+
+func (c *blobFileRewriteCompaction) Execute(jobID JobID, d *DB) error {
+	ctx := context.TODO()
+	if objiotracing.Enabled {
+		ctx = objiotracing.WithReason(ctx, objiotracing.ForCompaction)
+	}
+	c.grantHandle.Started()
+	// The version stored in the compaction is ref'd when the compaction is
+	// created. We're responsible for un-refing it when the compaction is
+	// complete.
+	defer c.version.UnrefLocked()
+
+	// Notify the event listener that the compaction has begun.
+	info := BlobFileRewriteInfo{
+		JobID: int(jobID),
+		Input: BlobFileInfo{
+			BlobFileID:  c.input.FileID,
+			DiskFileNum: c.input.Physical.FileNum,
+			Size:        c.input.Physical.Size,
+			ValueSize:   c.input.Physical.ValueSize,
+		},
+	}
+	d.opts.EventListener.BlobFileRewriteBegin(info)
+	startTime := d.timeNow()
+
+	// Run the blob file rewrite.
+	objMeta, ve, err := d.runBlobFileRewriteLocked(ctx, jobID, c)
+
+	info.Duration = d.timeNow().Sub(startTime)
+
+	// Update the version with the remapped blob file.
+	if err == nil {
+		info.Output.BlobFileID = ve.NewBlobFiles[0].FileID
+		info.Output.DiskFileNum = ve.NewBlobFiles[0].Physical.FileNum
+		info.Output.Size = ve.NewBlobFiles[0].Physical.Size
+		info.Output.ValueSize = ve.NewBlobFiles[0].Physical.ValueSize
+		err = d.mu.versions.UpdateVersionLocked(func() (versionUpdate, error) {
+			// It's possible that concurrent compactions removed references to
+			// the blob file while the blob file rewrite compaction was running.
+			// Now that we have the manifest lock, check if the blob file is
+			// still current. If not, we bubble up ErrCancelledCompaction.
+			v := d.mu.versions.currentVersion()
+			currentDiskFileNum, ok := v.BlobFiles.Lookup(c.input.FileID)
+			if !ok {
+				return versionUpdate{}, errors.Wrapf(ErrCancelledCompaction,
+					"blob file %s became unreferenced", c.input.FileID)
+			}
+			// Assert that the current version's disk file number for the blob
+			// matches the one we rewrote. This compaction should be the only
+			// rewrite compaction running for this blob file.
+			if currentDiskFileNum != c.input.Physical.FileNum {
+				return versionUpdate{}, base.AssertionFailedf(
+					"blob file %s was rewritten to %s during rewrite compaction of %s",
+					c.input.FileID, currentDiskFileNum, c.input.Physical.FileNum)
+			}
+			return versionUpdate{
+				VE:    ve,
+				JobID: jobID,
+				InProgressCompactionsFn: func() []compactionInfo {
+					return d.getInProgressCompactionInfoLocked(c)
+				},
+			}, nil
+		})
+	}
+
+	// Update the read state to publish the new version.
+	if err == nil {
+		d.updateReadStateLocked(d.opts.DebugCheck)
+	}
+
+	// Ensure we clean up the blob file we created on failure.
+	if err != nil {
+		if objMeta.DiskFileNum != 0 {
+			d.mu.versions.obsoleteBlobs = append(d.mu.versions.obsoleteBlobs, obsoleteFile{
+				fileType: base.FileTypeBlob,
+				fs:       d.opts.FS,
+				path:     d.objProvider.Path(objMeta),
+				fileNum:  objMeta.DiskFileNum,
+				// We don't know the size of the output blob file--it may have
+				// been half-written. We use the input blob file size as an
+				// approximation for deletion pacing.
+				fileSize: c.input.Physical.Size,
+				isLocal:  true,
+			})
+		}
+	}
+
+	// Notify the event listener that the compaction has ended.
+	now := d.timeNow()
+	info.TotalDuration = now.Sub(c.beganAt)
+	info.Done = true
+	info.Err = err
+	d.opts.EventListener.BlobFileRewriteEnd(info)
+	return nil
+}
+
+func (c *blobFileRewriteCompaction) Info() compactionInfo {
+	return compactionInfo{
+		kind:               compactionKindBlobFileRewrite,
+		versionEditApplied: c.versionEditApplied,
+		outputLevel:        -1,
+	}
+}
+
+func (c *blobFileRewriteCompaction) RecordError(*problemspans.ByLevel, error) {
+	// TODO(jackson): Track problematic blob files and avoid re-picking the same
+	// blob file compaction.
+}
+
+// runBlobFileRewriteLocked runs a blob file rewrite. d.mu must be held when
+// calling this, although it may be dropped and re-acquired during the course of
+// the method.
+func (d *DB) runBlobFileRewriteLocked(
+	ctx context.Context, jobID JobID, c *blobFileRewriteCompaction,
+) (objstorage.ObjectMetadata, *manifest.VersionEdit, error) {
+	// Drop the database mutex while we perform the rewrite, and re-acquire it
+	// before returning.
+	d.mu.Unlock()
+	defer d.mu.Lock()
+
+	// Construct the block.ReadEnv configured with a buffer pool. Setting the
+	// buffer pool ensures we won't cache blocks in the block cache. As soon as
+	// the compaction finishes new iterators will read the new blob file, so it
+	// would be unlikely the cached blocks would be reused.
+	var bufferPool block.BufferPool
+	bufferPool.Init(4)
+	defer bufferPool.Release()
+	env := block.ReadEnv{
+		Stats:              &c.internalIteratorStats,
+		BufferPool:         &bufferPool,
+		ReportCorruptionFn: d.reportCorruption,
+	}
+
+	// Create a new file for the rewritten blob file.
+	newDiskFileNum := d.mu.versions.getNextDiskFileNum()
+	writable, objMeta, err := d.objProvider.Create(ctx, base.FileTypeBlob, newDiskFileNum, c.objCreateOpts)
+	if err != nil {
+		return objstorage.ObjectMetadata{}, nil, err
+	}
+	d.opts.EventListener.BlobFileCreated(BlobFileCreateInfo{
+		JobID:   int(jobID),
+		Reason:  "compaction",
+		Path:    d.objProvider.Path(objMeta),
+		FileNum: objMeta.DiskFileNum,
+	})
+	// Initialize a blob file writer. We pass L6 to MakeBlobWriterOptions.
+	// There's no single associated level with a blob file. A long-lived blob
+	// file that gets rewritten is likely to mostly be referenced from L6.
+	// TODO(jackson): Consider refactoring to remove the level association.
+	writer := blob.NewFileWriter(newDiskFileNum, writable, d.opts.MakeBlobWriterOptions(6))
+
+	// Perform the rewrite.
+	rewriter := newBlobFileRewriter(d.fileCache, env, writer, c.referencingTables, c.input)
+	stats, err := rewriter.Rewrite(ctx)
+	if err != nil {
+		return objstorage.ObjectMetadata{}, nil, err
+	}
+
+	// Sync the object provider to ensure the metadata for the blob file is
+	// persisted.
+	if err := d.objProvider.Sync(); err != nil {
+		return objstorage.ObjectMetadata{}, nil, err
+	}
+
+	ve := &manifest.VersionEdit{
+		DeletedBlobFiles: map[manifest.DeletedBlobFileEntry]*manifest.PhysicalBlobFile{
+			{
+				FileID:  c.input.FileID,
+				FileNum: c.input.Physical.FileNum,
+			}: c.input.Physical,
+		},
+		NewBlobFiles: []manifest.BlobFileMetadata{
+			{
+				FileID: c.input.FileID,
+				Physical: &manifest.PhysicalBlobFile{
+					FileNum:      newDiskFileNum,
+					Size:         stats.FileLen,
+					ValueSize:    stats.UncompressedValueBytes,
+					CreationTime: uint64(d.timeNow().Unix()),
+				},
+			},
+		},
+	}
+	return objMeta, ve, nil
+}
+
+// blockHeap is a min-heap of blob reference liveness encodings, ordered by
+// blockID. We use this to help us determine the overall liveness of values in
+// each blob block by combining the blob reference liveness encodings of all
+// referencing sstables for a particular blockID.
+type blockHeap []*sstable.BlobRefLivenessEncoding
+
+// Len implements sort.Interface.
+func (h blockHeap) Len() int { return len(h) }
+
+// Less implements sort.Interface.
+func (h blockHeap) Less(i, j int) bool { return h[i].BlockID < h[j].BlockID }
+
+// Swap implements sort.Interface.
+func (h blockHeap) Swap(i, j int) {
+	h[i], h[j] = h[j], h[i]
+}
+
+// Push implements heap.Interface.
+func (h *blockHeap) Push(x any) {
+	blobEnc := x.(*sstable.BlobRefLivenessEncoding)
+	*h = append(*h, blobEnc)
+}
+
+// Pop implements heap.Interface.
+func (h *blockHeap) Pop() any {
+	old := *h
+	n := len(old)
+	item := old[n-1]
+	old[n-1] = nil
+	*h = old[0 : n-1]
+	return item
+}
+
+// blockValues holds the accumulated liveness data for blockID.
+type blockValues struct {
+	blockID      blob.BlockID
+	valuesSize   int
+	liveValueIDs []int
+}
+
+// blobFileMapping implements blob.FileMapping to always map to the input blob
+// file.
+type blobFileMapping struct {
+	fileNum base.DiskFileNum
+}
+
+// Assert that (*blobFileMapping) implements blob.FileMapping.
+var _ blob.FileMapping = (*blobFileMapping)(nil)
+
+func (m *blobFileMapping) Lookup(fileID base.BlobFileID) (base.DiskFileNum, bool) {
+	return m.fileNum, true
+}
+
+// blobFileRewriter is responsible for rewriting blob files by combining and
+// processing blob reference liveness encodings from multiple SSTables. It
+// maintains state for writing to an output blob file.
+type blobFileRewriter struct {
+	fc       *fileCacheHandle
+	env      block.ReadEnv
+	sstables []*manifest.TableMetadata
+
+	inputBlob    manifest.BlobFileMetadata
+	valueFetcher blob.ValueFetcher
+	fileMapping  blobFileMapping
+	blkHeap      blockHeap
+
+	// Current blob writer state.
+	writer *blob.FileWriter
+}
+
+func newBlobFileRewriter(
+	fc *fileCacheHandle,
+	env block.ReadEnv,
+	writer *blob.FileWriter,
+	sstables []*manifest.TableMetadata,
+	inputBlob manifest.BlobFileMetadata,
+) *blobFileRewriter {
+	return &blobFileRewriter{
+		fc:          fc,
+		env:         env,
+		writer:      writer,
+		sstables:    sstables,
+		inputBlob:   inputBlob,
+		fileMapping: blobFileMapping{fileNum: inputBlob.Physical.FileNum},
+		blkHeap:     blockHeap{},
+	}
+}
+
+// generateHeap populates rw.blkHeap with the blob reference liveness encodings
+// for each referencing sstable, rw.sstables.
+func (rw *blobFileRewriter) generateHeap() error {
+	ctx := context.TODO()
+	heap.Init(&rw.blkHeap)
+
+	var decoder colblk.ReferenceLivenessBlockDecoder
+	// For each sstable that references the input blob file, push its
+	// sstable.BlobLivenessEncoding on to the heap.
+	for _, sst := range rw.sstables {
+		// Validate that the sstable contains a reference to the input blob
+		// file.
+		refID, ok := sst.BlobReferences.IDByBlobFileID(rw.inputBlob.FileID)
+		if !ok {
+			return errors.AssertionFailedf("table %s doesn't contain a reference to blob file %s",
+				sst.TableNum, rw.inputBlob.FileID)
+		}
+		err := rw.fc.withReader(ctx, rw.env, sst, func(r *sstable.Reader, readEnv sstable.ReadEnv) error {
+			h, err := r.ReadBlobRefIndexBlock(ctx, readEnv.Block)
+			if err != nil {
+				return err
+			}
+			defer h.Release()
+			decoder.Init(h.BlockData())
+			bitmapEncodings := slices.Clone(decoder.LivenessAtReference(int(refID)))
+			// TODO(annie): We should instead maintain 1 heap item per sstable
+			// instead of 1 heap item per sstable block ref to reduce the heap
+			// comparisons to O(sstables).
+			for _, enc := range sstable.DecodeBlobRefLivenessEncoding(bitmapEncodings) {
+				heap.Push(&rw.blkHeap, &enc)
+			}
+			return nil
+		})
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// copyBlockValues copies the live values from the given block to the output
+// blob file, flushing the current block before if necessary.
+func (rw *blobFileRewriter) copyBlockValues(ctx context.Context, finishedBlock blockValues) error {
+	shouldFlush := rw.writer.ShouldFlushBefore(finishedBlock.valuesSize)
+	if shouldFlush {
+		rw.writer.ForceFlush()
+	}
+	slices.Sort(finishedBlock.liveValueIDs)
+	for i, valueID := range finishedBlock.liveValueIDs {
+		if i > 0 && finishedBlock.liveValueIDs[i-1]+1 != valueID {
+			// There's a gap in the referenced value IDs.
+			for missing := finishedBlock.liveValueIDs[i-1] + 1; missing < valueID; missing++ {
+				rw.writer.AddValue(nil)
+			}
+		}
+
+		value, _, err := rw.valueFetcher.Fetch(ctx, rw.inputBlob.FileID, finishedBlock.blockID, blob.BlockValueID(valueID))
+		if err != nil {
+			return err
+		}
+		rw.writer.AddValue(value)
+	}
+	return nil
+}
+
+func (rw *blobFileRewriter) Rewrite(ctx context.Context) (blob.FileWriterStats, error) {
+	rw.valueFetcher.Init(&rw.fileMapping, rw.fc, rw.env)
+	defer func() { _ = rw.valueFetcher.Close() }()
+
+	err := rw.generateHeap()
+	if err != nil {
+		return blob.FileWriterStats{}, err
+	}
+	if rw.blkHeap.Len() == 0 {
+		return blob.FileWriterStats{}, errors.AssertionFailedf("heap empty")
+	}
+
+	// Begin constructing our output blob file. We maintain a map of blockID
+	// to accumulated liveness data across all referencing sstables.
+	firstBlock := heap.Pop(&rw.blkHeap).(*sstable.BlobRefLivenessEncoding)
+
+	// Add virtual block mappings for all blocks from 0 to the first block.
+	for blockID := blob.BlockID(0); blockID < firstBlock.BlockID; blockID++ {
+		rw.writer.BeginNewVirtualBlock(blockID)
+	}
+	pendingBlock := blockValues{
+		blockID:      firstBlock.BlockID,
+		valuesSize:   firstBlock.ValuesSize,
+		liveValueIDs: slices.Collect(sstable.IterSetBitsInRunLengthBitmap(firstBlock.Bitmap)),
+	}
+	for rw.blkHeap.Len() > 0 {
+		nextBlock := heap.Pop(&rw.blkHeap).(*sstable.BlobRefLivenessEncoding)
+
+		// If we are encountering a new block, write the last accumulated block
+		// to the blob file.
+		if pendingBlock.blockID != nextBlock.BlockID {
+			// Add virtual block mappings for all blocks between the last block
+			// we encountered and the current block.
+			for blockID := pendingBlock.blockID; blockID < nextBlock.BlockID; blockID++ {
+				rw.writer.BeginNewVirtualBlock(blockID)
+			}
+			// Write the last accumulated block's values to the blob file.
+			if err := rw.copyBlockValues(ctx, pendingBlock); err != nil {
+				return blob.FileWriterStats{}, err
+			}
+		}
+		// Update the accumulated encoding for this block.
+		pendingBlock.valuesSize += nextBlock.ValuesSize
+		pendingBlock.liveValueIDs = slices.AppendSeq(pendingBlock.liveValueIDs,
+			sstable.IterSetBitsInRunLengthBitmap(nextBlock.Bitmap))
+	}
+
+	// Copy the last accumulated block.
+	rw.writer.BeginNewVirtualBlock(pendingBlock.blockID)
+	if err := rw.copyBlockValues(ctx, pendingBlock); err != nil {
+		return blob.FileWriterStats{}, err
+	}
+	return rw.writer.Close()
+}
diff --git a/compaction.go b/compaction.go
index fbc4c032..83d5b0dd 100644
--- a/compaction.go
+++ b/compaction.go
@@ -151,6 +151,7 @@ const (
 	compactionKindTombstoneDensity
 	compactionKindRewrite
 	compactionKindIngestedFlushable
+	compactionKindBlobFileRewrite
 )
 
 func (k compactionKind) String() string {
@@ -175,6 +176,8 @@ func (k compactionKind) String() string {
 		return "ingested-flushable"
 	case compactionKindCopy:
 		return "copy"
+	case compactionKindBlobFileRewrite:
+		return "blob-file-rewrite"
 	}
 	return "?"
 }
@@ -410,8 +413,9 @@ func (c *tableCompaction) IsFlush() bool                      { return len(c.flu
 func (c *tableCompaction) Info() compactionInfo {
 	info := compactionInfo{
 		versionEditApplied: c.versionEditApplied,
+		kind:               c.kind,
 		inputs:             c.inputs,
-		bounds:             c.bounds,
+		bounds:             &c.bounds,
 		outputLevel:        -1,
 	}
 	if c.outputLevel != nil {
@@ -1774,7 +1778,8 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) {
 				// doing a [c,d) excise at the same time as this compaction, we will have
 				// to error out the whole compaction as we can't guarantee it hasn't/won't
 				// write a file overlapping with the excise span.
-				if c2.Bounds().Overlaps(d.cmp, &exciseBounds) {
+				bounds := c2.Bounds()
+				if bounds != nil && bounds.Overlaps(d.cmp, &exciseBounds) {
 					c2.Cancel()
 				}
 			}
@@ -3623,6 +3628,8 @@ func getDiskWriteCategoryForCompaction(opts *Options, kind compactionKind) vfs.D
 		return "sql-row-spill"
 	} else if kind == compactionKindFlush {
 		return "pebble-memtable-flush"
+	} else if kind == compactionKindBlobFileRewrite {
+		return "pebble-blob-file-rewrite"
 	} else {
 		return "pebble-compaction"
 	}
diff --git a/compaction_picker.go b/compaction_picker.go
index 05d7e6b7..4bddc227 100644
--- a/compaction_picker.go
+++ b/compaction_picker.go
@@ -93,9 +93,13 @@ type compactionInfo struct {
 	// been committed. The compaction may still be in-progress deleting newly
 	// obsolete files.
 	versionEditApplied bool
-	inputs             []compactionLevel
-	outputLevel        int
-	bounds             base.UserKeyBounds
+	// kind indicates the kind of compaction.
+	kind        compactionKind
+	inputs      []compactionLevel
+	outputLevel int
+	// bounds may be nil if the compaction does not involve sstables
+	// (specifically, a blob file rewrite).
+	bounds *base.UserKeyBounds
 }
 
 func (info compactionInfo) String() string {
@@ -1448,6 +1452,12 @@ func (p *compactionPickerByScore) pickAutoNonScore(env compactionEnv) (pc picked
 		return pc
 	}
 
+	// Check for blob file rewrites. These are low-priority compactions because
+	// they don't help us keep up with writes, just reclaim disk space.
+	if pc := p.pickBlobFileRewriteCompaction(env); pc != nil {
+		return pc
+	}
+
 	if pc := p.pickReadTriggeredCompaction(env); pc != nil {
 		return pc
 	}
@@ -1662,6 +1672,51 @@ func (p *compactionPickerByScore) pickRewriteCompaction(
 	return nil
 }
 
+// pickBlobFileRewriteCompaction looks for compactions of blob files that
+// can be rewritten to reclaim disk space.
+func (p *compactionPickerByScore) pickBlobFileRewriteCompaction(
+	env compactionEnv,
+) (pc *pickedBlobFileCompaction) {
+	aggregateStats, heuristicStats := p.latestVersionState.blobFiles.Stats()
+	if heuristicStats.CountFilesEligible == 0 && heuristicStats.CountFilesTooRecent == 0 {
+		// No blob files with any garbage to rewrite.
+		return nil
+	}
+	policy := p.opts.Experimental.ValueSeparationPolicy()
+	if policy.TargetGarbageRatio >= 1.0 {
+		// Blob file rewrite compactions are disabled.
+		return nil
+	}
+	garbagePct := float64(aggregateStats.ValueSize-aggregateStats.ReferencedValueSize) /
+		float64(aggregateStats.ValueSize)
+	if garbagePct <= policy.TargetGarbageRatio {
+		// Not enough garbage to warrant a rewrite compaction.
+		return nil
+	}
+
+	// Check if there is an ongoing blob file rewrite compaction. If there is,
+	// don't schedule a new one.
+	for _, c := range env.inProgressCompactions {
+		if c.kind == compactionKindBlobFileRewrite {
+			return nil
+		}
+	}
+
+	candidate, ok := p.latestVersionState.blobFiles.ReplacementCandidate()
+	if !ok {
+		// None meet the heuristic.
+		return nil
+	}
+	// Add a reference to the version. The compaction will release the reference
+	// when it completes.
+	p.vers.Ref()
+	return &pickedBlobFileCompaction{
+		vers:              p.vers,
+		file:              candidate,
+		referencingTables: p.latestVersionState.blobFiles.ReferencingTables(candidate.FileID),
+	}
+}
+
 // pickTombstoneDensityCompaction looks for a compaction that eliminates
 // regions of extremely high point tombstone density. For each level, it picks
 // a file where the ratio of tombstone-dense blocks is at least
diff --git a/compaction_scheduler.go b/compaction_scheduler.go
index 5f423ce7..fc7ba92a 100644
--- a/compaction_scheduler.go
+++ b/compaction_scheduler.go
@@ -142,10 +142,12 @@ func init() {
 		compactionOptionalAndPriority{optional: true, priority: 60}
 	scheduledCompactionMap[compactionKindElisionOnly] =
 		compactionOptionalAndPriority{optional: true, priority: 50}
-	scheduledCompactionMap[compactionKindRead] =
+	scheduledCompactionMap[compactionKindBlobFileRewrite] =
 		compactionOptionalAndPriority{optional: true, priority: 40}
-	scheduledCompactionMap[compactionKindRewrite] =
+	scheduledCompactionMap[compactionKindRead] =
 		compactionOptionalAndPriority{optional: true, priority: 30}
+	scheduledCompactionMap[compactionKindRewrite] =
+		compactionOptionalAndPriority{optional: true, priority: 20}
 }
 
 // noopGrantHandle is used in cases that don't interact with a CompactionScheduler.
diff --git a/db.go b/db.go
index 42bfe337..463e7ab1 100644
--- a/db.go
+++ b/db.go
@@ -2630,15 +2630,6 @@ func (d *DB) maybeInduceWriteStall(b *Batch) {
 		for i := range d.mu.mem.queue {
 			size += d.mu.mem.queue[i].totalBytes()
 		}
-		// If ElevateWriteStallThresholdForFailover is true, we give an
-		// unlimited memory budget for memtables. This is simpler than trying to
-		// configure an explicit value, given that memory resources can vary.
-		// When using WAL failover in CockroachDB, an OOM risk is worth
-		// tolerating for workloads that have a strict latency SLO. Also, an
-		// unlimited budget here does not mean that the disk stall in the
-		// primary will go unnoticed until the OOM -- CockroachDB is monitoring
-		// disk stalls, and we expect it to fail the node after ~60s if the
-		// primary is stalled.
 		if size >= uint64(d.opts.MemTableStopWritesThreshold)*d.opts.MemTableSize &&
 			!d.mu.log.manager.ElevateWriteStallThresholdForFailover() {
 			// We have filled up the current memtable, but already queued memtables
@@ -2860,7 +2851,7 @@ func (d *DB) getEarliestUnflushedSeqNumLocked() base.SeqNum {
 	return seqNum
 }
 
-func (d *DB) getInProgressCompactionInfoLocked(finishing *tableCompaction) (rv []compactionInfo) {
+func (d *DB) getInProgressCompactionInfoLocked(finishing compaction) (rv []compactionInfo) {
 	for c := range d.mu.compact.inProgress {
 		if !c.IsFlush() && (finishing == nil || c != finishing) {
 			rv = append(rv, c.Info())
@@ -2888,7 +2879,7 @@ func inProgressL0Compactions(inProgress []compactionInfo) []manifest.L0Compactio
 			continue
 		}
 		compactions = append(compactions, manifest.L0Compaction{
-			Bounds:    info.bounds,
+			Bounds:    *info.bounds,
 			IsIntraL0: info.outputLevel == 0,
 		})
 	}
diff --git a/event.go b/event.go
index b1f59846..8c1c85c7 100644
--- a/event.go
+++ b/event.go
@@ -139,6 +139,66 @@ func (i BlobFileDeleteInfo) SafeFormat(w redact.SafePrinter, _ rune) {
 	w.Printf("[JOB %d] blob file deleted %s", redact.Safe(i.JobID), i.FileNum)
 }
 
+// BlobFileRewriteInfo contains the info for a blob file rewrite event.
+type BlobFileRewriteInfo struct {
+	// JobID is the ID of the job.
+	JobID int
+	// Input contains the input tables for the compaction organized by level.
+	Input BlobFileInfo
+	// Output contains the output tables generated by the compaction. The output
+	// info is empty for the compaction begin event.
+	Output BlobFileInfo
+	// Duration is the time spent compacting, including reading and writing
+	// files.
+	Duration time.Duration
+	// TotalDuration is the total wall-time duration of the compaction,
+	// including applying the compaction to the database. TotalDuration is
+	// always ≥ Duration.
+	TotalDuration time.Duration
+	Done          bool
+	// Err is set only if Done is true. If non-nil, indicates that the compaction
+	// failed. Note that err can be ErrCancelledCompaction, which can happen
+	// during normal operation.
+	Err error
+}
+
+func (i BlobFileRewriteInfo) String() string {
+	return redact.StringWithoutMarkers(i)
+}
+
+// SafeFormat implements redact.SafeFormatter.
+func (i BlobFileRewriteInfo) SafeFormat(w redact.SafePrinter, _ rune) {
+	if i.Err != nil {
+		w.Printf("[JOB %d] blob file (%s, %s) rewrite error: %s",
+			redact.Safe(i.JobID), i.Input.BlobFileID, i.Input.DiskFileNum, i.Err)
+		return
+	}
+
+	if !i.Done {
+		w.Printf("[JOB %d] rewriting blob file %s (physical file %s)",
+			redact.Safe(i.JobID), i.Input.BlobFileID, i.Input.DiskFileNum)
+		return
+	}
+	w.Printf("[JOB %d] rewrote blob file (%s, %s) -> (%s, %s), in %.1fs (%.1fs total)",
+		redact.Safe(i.JobID), i.Input.BlobFileID, i.Input.DiskFileNum,
+		i.Output.BlobFileID, i.Output.DiskFileNum,
+		redact.Safe(i.Duration.Seconds()),
+		redact.Safe(i.TotalDuration.Seconds()))
+}
+
+// BlobFileInfo describes a blob file.
+type BlobFileInfo struct {
+	// BlobFileID is the logical ID of the blob file.
+	BlobFileID base.BlobFileID
+	// DiskFileNum is the file number of the blob file on disk.
+	DiskFileNum base.DiskFileNum
+	// Size is the physical size of the file in bytes.
+	Size uint64
+	// ValueSize is the pre-compressed size of the values in the blob file in
+	// bytes.
+	ValueSize uint64
+}
+
 // CompactionInfo contains the info for a compaction event.
 type CompactionInfo struct {
 	// JobID is the ID of the compaction job.
@@ -802,6 +862,12 @@ type EventListener struct {
 	// BlobFileDeleted is invoked after a blob file has been deleted.
 	BlobFileDeleted func(BlobFileDeleteInfo)
 
+	// BlobFileRewriteBegin is invoked when a blob file rewrite compaction begins.
+	BlobFileRewriteBegin func(BlobFileRewriteInfo)
+
+	// BlobFileRewriteEnd is invoked when a blob file rewrite compaction ends.
+	BlobFileRewriteEnd func(BlobFileRewriteInfo)
+
 	// DataCorruption is invoked when an on-disk corruption is detected. It should
 	// not block, as it is called synchronously in read paths.
 	DataCorruption func(DataCorruptionInfo)
@@ -907,6 +973,12 @@ func (l *EventListener) EnsureDefaults(logger Logger) {
 	if l.BlobFileDeleted == nil {
 		l.BlobFileDeleted = func(info BlobFileDeleteInfo) {}
 	}
+	if l.BlobFileRewriteBegin == nil {
+		l.BlobFileRewriteBegin = func(info BlobFileRewriteInfo) {}
+	}
+	if l.BlobFileRewriteEnd == nil {
+		l.BlobFileRewriteEnd = func(info BlobFileRewriteInfo) {}
+	}
 	if l.DataCorruption == nil {
 		if logger != nil {
 			l.DataCorruption = func(info DataCorruptionInfo) {
@@ -998,6 +1070,12 @@ func MakeLoggingEventListener(logger Logger) EventListener {
 		BlobFileDeleted: func(info BlobFileDeleteInfo) {
 			logger.Infof("%s", info)
 		},
+		BlobFileRewriteBegin: func(info BlobFileRewriteInfo) {
+			logger.Infof("%s", info)
+		},
+		BlobFileRewriteEnd: func(info BlobFileRewriteInfo) {
+			logger.Infof("%s", info)
+		},
 		DataCorruption: func(info DataCorruptionInfo) {
 			logger.Errorf("%s", info)
 		},
@@ -1084,6 +1162,14 @@ func TeeEventListener(a, b EventListener) EventListener {
 			a.BlobFileDeleted(info)
 			b.BlobFileDeleted(info)
 		},
+		BlobFileRewriteBegin: func(info BlobFileRewriteInfo) {
+			a.BlobFileRewriteBegin(info)
+			b.BlobFileRewriteBegin(info)
+		},
+		BlobFileRewriteEnd: func(info BlobFileRewriteInfo) {
+			a.BlobFileRewriteEnd(info)
+			b.BlobFileRewriteEnd(info)
+		},
 		DataCorruption: func(info DataCorruptionInfo) {
 			a.DataCorruption(info)
 			b.DataCorruption(info)
diff --git a/go.mod b/go.mod
index cb25ac56..7bbe6b8a 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/cockroachdb/errors v1.11.3
 	github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895
 	github.com/cockroachdb/redact v1.1.5
-	github.com/cockroachdb/swiss v0.0.0-20250304010804-34a2c6a59016
+	github.com/cockroachdb/swiss v0.0.0-20250624142022-d6e517c1d961
 	github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06
 	github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9
 	github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e
diff --git a/go.sum b/go.sum
index d099cf86..067b1337 100644
--- a/go.sum
+++ b/go.sum
@@ -46,8 +46,8 @@ github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895 h1:XANOgPY
 github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895/go.mod h1:aPd7gM9ov9M8v32Yy5NJrDyOcD8z642dqs+F0CeNXfA=
 github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
 github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
-github.com/cockroachdb/swiss v0.0.0-20250304010804-34a2c6a59016 h1:d0dxi6Q8yGtRBupybhtBC4GJk8WegtDAFr+Xk+/3sSE=
-github.com/cockroachdb/swiss v0.0.0-20250304010804-34a2c6a59016/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg=
+github.com/cockroachdb/swiss v0.0.0-20250624142022-d6e517c1d961 h1:Nua446ru3juLHLZd4AwKNzClZgL1co3pUPGv3o8FlcA=
+github.com/cockroachdb/swiss v0.0.0-20250624142022-d6e517c1d961/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg=
 github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
 github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
diff --git a/ingest.go b/ingest.go
index fe58e1dd..ed92fa86 100644
--- a/ingest.go
+++ b/ingest.go
@@ -1034,7 +1034,8 @@ func ingestTargetLevel(
 			if tblCompaction.outputLevel == nil || level != tblCompaction.outputLevel.level {
 				continue
 			}
-			if metaBounds.Overlaps(cmp, tblCompaction.Bounds()) {
+			bounds := tblCompaction.Bounds()
+			if bounds != nil && metaBounds.Overlaps(cmp, bounds) {
 				overlaps = true
 				break
 			}
@@ -1591,7 +1592,19 @@ func (d *DB) ingest(ctx context.Context, args ingestArgs) (IngestOperationStats,
 		// files.
 		hasRemoteFiles := len(shared) > 0 || len(external) > 0
 		canIngestFlushable := d.FormatMajorVersion() >= FormatFlushableIngest &&
-			(len(d.mu.mem.queue) < d.opts.MemTableStopWritesThreshold) &&
+			// We require that either the queue of flushables is below the
+			// stop-writes threshold (note that this is typically a conservative
+			// check, since not every element of this queue will contribute the full
+			// memtable memory size that could result in a write stall), or WAL
+			// failover is permitting an unlimited queue without causing a write
+			// stall. The latter condition is important to avoid delays in
+			// visibility of concurrent writes that happen to get a sequence number
+			// after this ingest and then must wait for this ingest that is itself
+			// waiting on a large flush. See
+			// https://github.com/cockroachdb/pebble/issues/4944 for an illustration
+			// of this problem.
+			(len(d.mu.mem.queue) < d.opts.MemTableStopWritesThreshold ||
+				d.mu.log.manager.ElevateWriteStallThresholdForFailover()) &&
 			!d.opts.Experimental.DisableIngestAsFlushable() && !hasRemoteFiles &&
 			(!args.ExciseSpan.Valid() || d.FormatMajorVersion() >= FormatFlushableIngestExcises)
 
@@ -2115,7 +2128,8 @@ func (d *DB) ingestApply(
 				// doing a [c,d) excise at the same time as this compaction, we will have
 				// to error out the whole compaction as we can't guarantee it hasn't/won't
 				// write a file overlapping with the excise span.
-				if c.Bounds().Overlaps(d.cmp, &exciseBounds) {
+				bounds := c.Bounds()
+				if bounds != nil && bounds.Overlaps(d.cmp, &exciseBounds) {
 					c.Cancel()
 				}
 				// Check if this compaction's inputs have been replaced due to an
diff --git a/internal/base/lazy_value.go b/internal/base/lazy_value.go
index f82b41da..d784d54a 100644
--- a/internal/base/lazy_value.go
+++ b/internal/base/lazy_value.go
@@ -170,8 +170,8 @@ type LazyFetcher struct {
 
 // ValueFetcher is an interface for fetching a value.
 type ValueFetcher interface {
-	// Fetch returns the value, given the handle. It is acceptable to call the
-	// ValueFetcher.Fetch as long as the DB is open. However, one should assume
+	// FetchHandle returns the value, given the handle. It is acceptable to call the
+	// ValueFetcher.FetchHandle as long as the DB is open. However, one should assume
 	// there is a fast-path when the iterator tree has not moved off the sstable
 	// iterator that initially provided this LazyValue. Hence, to utilize this
 	// fast-path the caller should try to decide whether it needs the value or
@@ -181,7 +181,7 @@ type ValueFetcher interface {
 	// If the fetcher attempted to use buf *and* len(buf) was insufficient, it
 	// will allocate a new slice for the value. In either case it will set
 	// callerOwned to true.
-	Fetch(
+	FetchHandle(
 		ctx context.Context, handle []byte, blobFileID BlobFileID, valLen uint32, buf []byte,
 	) (val []byte, callerOwned bool, err error)
 }
@@ -192,7 +192,7 @@ func (lv *LazyValue) Value(buf []byte) (val []byte, callerOwned bool, err error)
 	if f == nil {
 		return lv.ValueOrHandle, false, nil
 	}
-	return f.Fetcher.Fetch(context.TODO(),
+	return f.Fetcher.FetchHandle(context.TODO(),
 		lv.ValueOrHandle, f.BlobFileID, f.Attribute.ValueLen, buf)
 }
 
@@ -264,8 +264,8 @@ type errValueFetcher struct{}
 
 var _ ValueFetcher = errValueFetcher{}
 
-// Fetch implements base.ValueFetcher.
-func (e errValueFetcher) Fetch(
+// FetchHandle implements base.ValueFetcher.
+func (e errValueFetcher) FetchHandle(
 	_ context.Context, _ []byte, blobFileID BlobFileID, valLen uint32, _ []byte,
 ) (val []byte, callerOwned bool, err error) {
 	err = AssertionFailedf("unexpected blob value: %d-byte from %s", valLen, blobFileID)
diff --git a/internal/manifest/blob_metadata.go b/internal/manifest/blob_metadata.go
index e6092063..903df0cd 100644
--- a/internal/manifest/blob_metadata.go
+++ b/internal/manifest/blob_metadata.go
@@ -9,6 +9,7 @@ import (
 	"container/heap"
 	"fmt"
 	"iter"
+	"maps"
 	"slices"
 	"strings"
 	"sync/atomic"
@@ -490,7 +491,7 @@ type currentBlobFile struct {
 	// referencing table number. This would likely be more memory efficient,
 	// reduce overall number of pointers to chase and suffer fewer allocations
 	// (and we can pool the B-Tree nodes to further reduce allocs)
-	references map[base.TableNum]struct{}
+	references map[*TableMetadata]struct{}
 	// referencedValueSize is the sum of the length of uncompressed values in
 	// this blob file that are still live.
 	referencedValueSize uint64
@@ -576,7 +577,7 @@ func (s *CurrentBlobFileSet) Init(bve *BulkVersionEdit, h BlobRewriteHeuristic)
 	for blobFileID, pbf := range bve.BlobFiles.Added {
 		s.files[blobFileID] = &currentBlobFile{
 			metadata:   BlobFileMetadata{FileID: blobFileID, Physical: pbf},
-			references: make(map[base.TableNum]struct{}),
+			references: make(map[*TableMetadata]struct{}),
 		}
 		s.stats.Count++
 		s.stats.PhysicalSize += pbf.Size
@@ -591,7 +592,7 @@ func (s *CurrentBlobFileSet) Init(bve *BulkVersionEdit, h BlobRewriteHeuristic)
 				if !ok {
 					panic(errors.AssertionFailedf("pebble: referenced blob file %d not found", ref.FileID))
 				}
-				cbf.references[table.TableNum] = struct{}{}
+				cbf.references[table] = struct{}{}
 				cbf.referencedValueSize += ref.ValueSize
 				s.stats.ReferencedValueSize += ref.ValueSize
 				s.stats.ReferencesCount++
@@ -724,7 +725,7 @@ func (s *CurrentBlobFileSet) ApplyAndUpdateVersionEdit(ve *VersionEdit) error {
 		}
 
 		blobFileID := m.FileID
-		cbf := &currentBlobFile{references: make(map[base.TableNum]struct{})}
+		cbf := &currentBlobFile{references: make(map[*TableMetadata]struct{})}
 		cbf.metadata = BlobFileMetadata{FileID: blobFileID, Physical: m.Physical}
 		s.files[blobFileID] = cbf
 		s.stats.Count++
@@ -742,7 +743,7 @@ func (s *CurrentBlobFileSet) ApplyAndUpdateVersionEdit(ve *VersionEdit) error {
 			if !ok {
 				return errors.AssertionFailedf("pebble: referenced blob file %d not found", ref.FileID)
 			}
-			cbf.references[e.Meta.TableNum] = struct{}{}
+			cbf.references[e.Meta] = struct{}{}
 			cbf.referencedValueSize += ref.ValueSize
 			s.stats.ReferencedValueSize += ref.ValueSize
 			s.stats.ReferencesCount++
@@ -765,7 +766,7 @@ func (s *CurrentBlobFileSet) ApplyAndUpdateVersionEdit(ve *VersionEdit) error {
 					return errors.AssertionFailedf("pebble: referenced value size %d for blob file %s is greater than the referenced value size %d",
 						ref.ValueSize, cbf.metadata.FileID, cbf.referencedValueSize)
 				}
-				if _, ok := cbf.references[meta.TableNum]; !ok {
+				if _, ok := cbf.references[meta]; !ok {
 					return errors.AssertionFailedf("pebble: deleted table %s's reference to blob file %s not known",
 						meta.TableNum, ref.FileID)
 				}
@@ -786,7 +787,7 @@ func (s *CurrentBlobFileSet) ApplyAndUpdateVersionEdit(ve *VersionEdit) error {
 				continue
 			}
 			// Remove the reference of this table to this blob file.
-			delete(cbf.references, meta.TableNum)
+			delete(cbf.references, meta)
 
 			// If there are no more references to the blob file, remove it from
 			// the set and add the removal of the blob file to the version edit.
@@ -868,7 +869,7 @@ func (s *CurrentBlobFileSet) ApplyAndUpdateVersionEdit(ve *VersionEdit) error {
 // ReplacementCandidate returns the next blob file that should be rewritten. If
 // there are no candidates, the second return value is false.  Successive calls
 // to ReplacementCandidate may (but are not guaranteed to) return the same blob
-// file until the blob file is replaced..
+// file until the blob file is replaced.
 func (s *CurrentBlobFileSet) ReplacementCandidate() (BlobFileMetadata, bool) {
 	s.moveAgedBlobFilesToCandidatesHeap(s.rewrite.heuristic.CurrentTime())
 	if len(s.rewrite.candidates.items) == 0 {
@@ -877,6 +878,21 @@ func (s *CurrentBlobFileSet) ReplacementCandidate() (BlobFileMetadata, bool) {
 	return s.rewrite.candidates.items[0].metadata, true
 }
 
+// ReferencingTables returns a slice containing the set of tables that reference
+// the blob file with the provided file ID. The returned slice is sorted by
+// table number.
+func (s *CurrentBlobFileSet) ReferencingTables(fileID base.BlobFileID) []*TableMetadata {
+	cbf, ok := s.files[fileID]
+	if !ok {
+		return nil
+	}
+	tables := slices.Collect(maps.Keys(cbf.references))
+	slices.SortFunc(tables, func(a, b *TableMetadata) int {
+		return stdcmp.Compare(a.TableNum, b.TableNum)
+	})
+	return tables
+}
+
 // moveAgedBlobFilesToCandidatesHeap moves blob files from the recentlyCreated
 // heap to the candidates heap if at the provided timestamp they are considered
 // old enough to be eligible for rewriting.
diff --git a/metamorphic/options.go b/metamorphic/options.go
index 3e98bd15..9f63112d 100644
--- a/metamorphic/options.go
+++ b/metamorphic/options.go
@@ -916,16 +916,16 @@ func RandomOptions(rng *rand.Rand, kf KeyFormat, cfg RandomOptionsCfg) *TestOpti
 		if testOpts.Opts.FormatMajorVersion < pebble.FormatValueSeparation {
 			testOpts.Opts.FormatMajorVersion = pebble.FormatValueSeparation
 		}
-		minSize := 1 + rng.IntN(maxValueSize)
-		maxBlobReferenceDepth := 2 + rng.IntN(9)                       // 2-10
-		rewriteMinimumAge := time.Duration(rng.IntN(10)) * time.Second // [0, 10s)
+		policy := pebble.ValueSeparationPolicy{
+			Enabled:               true,
+			MinimumSize:           1 + rng.IntN(maxValueSize),
+			MaxBlobReferenceDepth: 2 + rng.IntN(9),                           // 2-10
+			RewriteMinimumAge:     time.Duration(rng.IntN(10)) * time.Second, // [0, 10s)
+			// TODO(jackson): Begin enabling blob file rewrite compactions.
+			TargetGarbageRatio: 1.0,
+		}
 		opts.Experimental.ValueSeparationPolicy = func() pebble.ValueSeparationPolicy {
-			return pebble.ValueSeparationPolicy{
-				Enabled:               true,
-				MinimumSize:           minSize,
-				MaxBlobReferenceDepth: maxBlobReferenceDepth,
-				RewriteMinimumAge:     rewriteMinimumAge,
-			}
+			return policy
 		}
 	}
 
diff --git a/options.go b/options.go
index 22650c80..a83774ac 100644
--- a/options.go
+++ b/options.go
@@ -1169,6 +1169,23 @@ type ValueSeparationPolicy struct {
 	// to be eligible for a rewrite that reclaims disk space. Lower values
 	// reduce space amplification at the cost of write amplification
 	RewriteMinimumAge time.Duration
+	// TargetGarbageRatio is a value in the range [0, 1.0] and configures how
+	// aggressively blob files should be written in order to reduce space
+	// amplification induced by value separation. As compactions rewrite blob
+	// files, data may be duplicated.  Older blob files containing the
+	// duplicated data may need to remain because other sstables are referencing
+	// other values contained in the same file.
+	//
+	// The DB can rewrite these blob files in place in order to reduce this
+	// space amplification, but this incurs write amplification. This option
+	// configures how much garbage may accrue before the DB will attempt to
+	// rewrite blob files to reduce it. A value of 0.20 indicates that once 20%
+	// of values in blob files are unreferenced, the DB should attempt to
+	// rewrite blob files to reclaim disk space.
+	//
+	// A value of 1.0 indicates that the DB should never attempt to rewrite blob
+	// files.
+	TargetGarbageRatio float64
 }
 
 // SpanPolicy contains policies that can vary by key range. The zero value is
@@ -1673,6 +1690,7 @@ func (o *Options) String() string {
 			fmt.Fprintf(&buf, "  minimum_size=%d\n", policy.MinimumSize)
 			fmt.Fprintf(&buf, "  max_blob_reference_depth=%d\n", policy.MaxBlobReferenceDepth)
 			fmt.Fprintf(&buf, "  rewrite_minimum_age=%s\n", policy.RewriteMinimumAge)
+			fmt.Fprintf(&buf, "  target_garbage_ratio=%.2f\n", policy.TargetGarbageRatio)
 		}
 	}
 
@@ -2107,6 +2125,8 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error {
 				valSepPolicy.MaxBlobReferenceDepth, err = strconv.Atoi(value)
 			case "rewrite_minimum_age":
 				valSepPolicy.RewriteMinimumAge, err = time.ParseDuration(value)
+			case "target_garbage_ratio":
+				valSepPolicy.TargetGarbageRatio, err = strconv.ParseFloat(value, 64)
 			default:
 				if hooks != nil && hooks.SkipUnknown != nil && hooks.SkipUnknown(section+"."+key, value) {
 					return nil
diff --git a/sstable/blob/blob.go b/sstable/blob/blob.go
index 6d7c19fd..897950a8 100644
--- a/sstable/blob/blob.go
+++ b/sstable/blob/blob.go
@@ -195,19 +195,29 @@ func (w *FileWriter) EstimatedSize() uint64 {
 	return sz
 }
 
-// FlushForTesting flushes the current block to the write queue. Writers should
-// generally not call FlushForTesting, and instead let the heuristics configured
+// ForceFlush flushes the current block to the write queue. Writers should
+// generally not call ForceFlush, and instead let the heuristics configured
 // through FileWriterOptions handle flushing.
 //
-// It's exposed so that tests can force flushes to construct blob files with
-// arbitrary structures.
-func (w *FileWriter) FlushForTesting() {
+// For blob file rewriting, ForceFlush needs to be called to ensure that there
+// is a 1:1 remapping of virtual blocks to physical blocks.
+//
+// Otherwise, it's exposed so that tests can force flushes to construct blob
+// files with arbitrary structures.
+func (w *FileWriter) ForceFlush() {
 	if w.valuesEncoder.Count() == 0 {
 		return
 	}
 	w.flush()
 }
 
+// ShouldFlush returns true if the current block should be flushed before adding
+// newDataBytes of data.
+func (w *FileWriter) ShouldFlushBefore(newDataBytes int) bool {
+	size := w.valuesEncoder.size() + block.TrailerLen
+	return w.flushGov.ShouldFlush(size, size+newDataBytes)
+}
+
 // flush flushes the current block to the write queue.
 func (w *FileWriter) flush() {
 	if w.valuesEncoder.Count() == 0 {
@@ -283,7 +293,7 @@ func (w *FileWriter) Close() (FileWriterStats, error) {
 	{
 		indexBlock := w.indexEncoder.Finish()
 		var compressedBuf []byte
-		pb := block.CompressAndChecksum(&compressedBuf, indexBlock, blockkind.Metadata, block.NoopCompressor, &w.checksummer)
+		pb := block.CopyAndChecksum(&compressedBuf, indexBlock, blockkind.Metadata, &w.compressor, &w.checksummer)
 		if _, w.err = pb.WriteTo(w.w); w.err != nil {
 			err = w.err
 			if w.w != nil {
diff --git a/sstable/blob/fetcher.go b/sstable/blob/fetcher.go
index effade78..a579567f 100644
--- a/sstable/blob/fetcher.go
+++ b/sstable/blob/fetcher.go
@@ -88,9 +88,9 @@ func (r *ValueFetcher) Init(fm FileMapping, rp ReaderProvider, env block.ReadEnv
 	}
 }
 
-// Fetch returns the value, given the handle. Fetch must not be called after
-// Close.
-func (r *ValueFetcher) Fetch(
+// FetchHandle returns the value, given the handle. FetchHandle must not be
+// called after Close.
+func (r *ValueFetcher) FetchHandle(
 	ctx context.Context, handle []byte, blobFileID base.BlobFileID, valLen uint32, buf []byte,
 ) (val []byte, callerOwned bool, err error) {
 	handleSuffix := DecodeHandleSuffix(handle)
@@ -101,6 +101,27 @@ func (r *ValueFetcher) Fetch(
 		ValueID:    handleSuffix.ValueID,
 	}
 	v, err := r.retrieve(ctx, vh)
+	if err == nil && len(v) != int(vh.ValueLen) {
+		return nil, false,
+			errors.AssertionFailedf("value length mismatch: %d != %d", len(v), vh.ValueLen)
+	}
+	if invariants.Enabled {
+		v = r.bufMangler.MaybeMangleLater(v)
+	}
+	return v, false, err
+}
+
+// Fetch is like FetchHandle, but it constructs handle and does not
+// validate the value length. Fetch must not be called after Close.
+func (r *ValueFetcher) Fetch(
+	ctx context.Context, blobFileID base.BlobFileID, blockID BlockID, valueID BlockValueID,
+) (val []byte, callerOwned bool, err error) {
+	vh := Handle{
+		BlobFileID: blobFileID,
+		BlockID:    blockID,
+		ValueID:    valueID,
+	}
+	v, err := r.retrieve(ctx, vh)
 	if invariants.Enabled {
 		v = r.bufMangler.MaybeMangleLater(v)
 	}
@@ -268,9 +289,6 @@ func (cr *cachedReader) GetUnsafeValue(
 
 	invariants.CheckBounds(int(valueID), cr.currentValueBlock.dec.bd.Rows())
 	v := cr.currentValueBlock.dec.values.Slice(cr.currentValueBlock.dec.values.Offsets(int(valueID)))
-	if len(v) != int(vh.ValueLen) {
-		return nil, errors.AssertionFailedf("value length mismatch: %d != %d", len(v), vh.ValueLen)
-	}
 	return v, nil
 }
 
diff --git a/sstable/block/compression.go b/sstable/block/compression.go
index c57e4c6e..26eac4e4 100644
--- a/sstable/block/compression.go
+++ b/sstable/block/compression.go
@@ -340,10 +340,6 @@ func (b *PhysicalBlock) WriteTo(w objstorage.Writable) (n int, err error) {
 // CompressAndChecksum compresses and checksums the provided block, returning
 // the compressed block and its trailer. The result is appended to the dst
 // argument.
-//
-// If the compressed block is not sufficiently smaller than the original block,
-// the compressed payload is discarded and the original, uncompressed block data
-// is used to avoid unnecessary decompression overhead at read time.
 func CompressAndChecksum(
 	dst *[]byte, blockData []byte, blockKind Kind, compressor *Compressor, checksummer *Checksummer,
 ) PhysicalBlock {
@@ -358,6 +354,27 @@ func CompressAndChecksum(
 	return pb
 }
 
+// CopyAndChecksum copies the provided block (without compressing it) and
+// checksums it, returning the physical block. The result is appended to the dst
+// argument.
+//
+// Note that we still need to provide a Compressor so we can inform it of the
+// uncompressed block (for statistics).
+func CopyAndChecksum(
+	dst *[]byte, blockData []byte, blockKind Kind, compressor *Compressor, checksummer *Checksummer,
+) PhysicalBlock {
+	buf := *dst
+	buf = append(buf[:0], blockData...)
+	*dst = buf
+
+	// Calculate the checksum.
+	pb := PhysicalBlock{data: buf}
+	checksum := checksummer.Checksum(buf, byte(NoCompressionIndicator))
+	pb.trailer = MakeTrailer(byte(NoCompressionIndicator), checksum)
+	compressor.UncompressedBlock(len(blockData), blockKind)
+	return pb
+}
+
 // CompressAndChecksumToTempBuffer compresses and checksums the provided block
 // into a TempBuffer. The caller should Release() the TempBuffer once it is no
 // longer necessary.
@@ -370,6 +387,18 @@ func CompressAndChecksumToTempBuffer(
 	return pb, compressedBuf
 }
 
+// CopyAndChecksumToTempBuffer copies (without compressing) and checksums
+// the provided block into a TempBuffer. The caller should Release() the
+// TempBuffer once it is no longer necessary.
+func CopyAndChecksumToTempBuffer(
+	blockData []byte, blockKind Kind, compressor *Compressor, checksummer *Checksummer,
+) (PhysicalBlock, *TempBuffer) {
+	// Grab a buffer to use as the destination for compression.
+	compressedBuf := NewTempBuffer()
+	pb := CopyAndChecksum(&compressedBuf.b, blockData, blockKind, compressor, checksummer)
+	return pb, compressedBuf
+}
+
 // TempBuffer is a buffer that is used temporarily and is released back to a
 // pool for reuse.
 type TempBuffer struct {
diff --git a/sstable/block/compressor.go b/sstable/block/compressor.go
index ab5b8c91..f4f838a5 100644
--- a/sstable/block/compressor.go
+++ b/sstable/block/compressor.go
@@ -1,7 +1,10 @@
 package block
 
 import (
+	"fmt"
+	"iter"
 	"math/rand"
+	"strings"
 
 	"github.com/cockroachdb/pebble/internal/compression"
 	"github.com/cockroachdb/pebble/sstable/block/blockkind"
@@ -14,17 +17,19 @@ import (
 //	.. = c.Compress(..)
 //	c.Close()
 type Compressor struct {
-	profile               CompressionProfile
+	minReductionPercent   uint8
 	dataBlocksCompressor  compression.Compressor
 	valueBlocksCompressor compression.Compressor
 	otherBlocksCompressor compression.Compressor
+
+	stats CompressionStats
 }
 
 // MakeCompressor returns a Compressor that applies the given compression
 // profile. Close must be called when the compressor is no longer needed.
 func MakeCompressor(profile *CompressionProfile) Compressor {
 	c := Compressor{
-		profile: *profile,
+		minReductionPercent: profile.MinReductionPercent,
 	}
 
 	c.dataBlocksCompressor = maybeAdaptiveCompressor(profile, profile.DataBlocks)
@@ -82,17 +87,96 @@ func (c *Compressor) Compress(dst, src []byte, kind Kind) (CompressionIndicator,
 	//   -----------  >  100 - MinReductionPercent
 	//      before
 	if setting.Algorithm != compression.NoCompression &&
-		int64(len(out))*100 > int64(len(src))*int64(100-c.profile.MinReductionPercent) {
+		int64(len(out))*100 > int64(len(src))*int64(100-c.minReductionPercent) {
+		c.stats.add(compression.None, uint64(len(src)), uint64(len(src)))
 		return NoCompressionIndicator, append(out[:0], src...)
 	}
+	c.stats.add(setting, uint64(len(src)), uint64(len(out)))
 	return compressionIndicatorFromAlgorithm(setting.Algorithm), out
 }
 
-// NoopCompressor is a Compressor that does not compress data. It does not have
-// any state and can be used in parallel.
-var NoopCompressor = &noopCompressor
+// UncompressedBlock informs the compressor that a block of the given size and
+// kind was written uncompressed. This is used so that the final statistics are
+// complete.
+func (c *Compressor) UncompressedBlock(size int, kind Kind) {
+	c.stats.add(compression.None, uint64(size), uint64(size))
+}
+
+// Stats returns the compression stats. The result can only be used until the
+// next call to the Compressor.
+func (c *Compressor) Stats() *CompressionStats {
+	return &c.stats
+}
+
+// CompressionStats collects compression statistics for a single file - the
+// total compressed and uncompressed sizes for each distinct compression.Setting
+// used.
+type CompressionStats struct {
+	n int
+	// Compression profiles have three settings (data, value, other) and
+	// NoCompression can also be used for data that didn't compress.
+	buf [4]CompressionStatsForSetting
+}
+
+type CompressionStatsForSetting struct {
+	Setting           compression.Setting
+	UncompressedBytes uint64
+	CompressedBytes   uint64
+}
+
+// add updates the stats to reflect a block that was compressed with the given setting.
+func (c *CompressionStats) add(
+	setting compression.Setting, sizeUncompressed, sizeCompressed uint64,
+) {
+	for i := 0; i < c.n; i++ {
+		if c.buf[i].Setting == setting {
+			c.buf[i].UncompressedBytes += sizeUncompressed
+			c.buf[i].CompressedBytes += sizeCompressed
+			return
+		}
+	}
+	if c.n >= len(c.buf)-1 {
+		panic("too many compression settings")
+	}
+	c.buf[c.n] = CompressionStatsForSetting{
+		Setting:           setting,
+		UncompressedBytes: sizeUncompressed,
+		CompressedBytes:   sizeCompressed,
+	}
+	c.n++
+}
+
+// MergeWith updates the receiver stats to include the other stats.
+func (c *CompressionStats) MergeWith(other *CompressionStats) {
+	for i := 0; i < other.n; i++ {
+		c.add(other.buf[i].Setting, other.buf[i].UncompressedBytes, other.buf[i].CompressedBytes)
+	}
+}
+
+// All returns an iterator over the collected stats, in arbitrary order.
+func (c CompressionStats) All() iter.Seq[CompressionStatsForSetting] {
+	return func(yield func(cs CompressionStatsForSetting) bool) {
+		for i := 0; i < c.n; i++ {
+			if !yield(c.buf[i]) {
+				return
+			}
+		}
+	}
+}
 
-var noopCompressor = MakeCompressor(NoCompression)
+// String returns a string representation of the stats, in the format:
+// "<setting1>:<compressed1>/<uncompressed1>,<setting2>:<compressed2>/<uncompressed2>,..."
+func (c CompressionStats) String() string {
+	var buf strings.Builder
+	buf.Grow(c.n * 64)
+	for i := 0; i < c.n; i++ {
+		if i > 0 {
+			buf.WriteString(",")
+		}
+		fmt.Fprintf(&buf, "%s:%d/%d", c.buf[i].Setting.String(), c.buf[i].CompressedBytes, c.buf[i].UncompressedBytes)
+	}
+	return buf.String()
+}
 
 type Decompressor = compression.Decompressor
 
diff --git a/sstable/colblk_writer.go b/sstable/colblk_writer.go
index fc8d059b..0dcc9e6f 100644
--- a/sstable/colblk_writer.go
+++ b/sstable/colblk_writer.go
@@ -1047,6 +1047,7 @@ func (w *RawColumnWriter) Close() (err error) {
 			}
 		}
 
+		w.props.CompressionStats = w.compressor.Stats().String()
 		var toWrite []byte
 		w.props.CompressionOptions = rocksDBCompressionOptions
 		if w.opts.TableFormat >= TableFormatPebblev7 {
@@ -1100,7 +1101,7 @@ func (w *RawColumnWriter) rewriteSuffixes(
 		return errors.Wrap(err, "reading layout")
 	}
 	// Copy data blocks in parallel, rewriting suffixes as we go.
-	blocks, err := rewriteDataBlocksInParallel(r, sstBytes, wo, l.Data, from, to, concurrency, func() blockRewriter {
+	blocks, err := rewriteDataBlocksInParallel(r, sstBytes, wo, l.Data, from, to, concurrency, w.compressor.Stats(), func() blockRewriter {
 		return colblk.NewDataBlockRewriter(wo.KeySchema, w.comparer)
 	})
 	if err != nil {
diff --git a/sstable/layout.go b/sstable/layout.go
index cfee62b9..40f8203e 100644
--- a/sstable/layout.go
+++ b/sstable/layout.go
@@ -369,7 +369,7 @@ func (l *Layout) Describe(
 				// We have already read the value-index to construct the list of
 				// value-blocks, so no need to do it again.
 			case "blob-reference-index":
-				h, err = r.blockReader.Read(ctx, block.NoReadEnv, noReadHandle, b.Handle, blockkind.BlobReferenceValueLivenessIndex, noInitBlockMetadataFn)
+				h, err = r.readBlobRefIndexBlock(ctx, block.NoReadEnv, noReadHandle)
 				if err != nil {
 					return err
 				}
@@ -858,7 +858,7 @@ func (w *layoutWriter) Abort() {
 // WriteDataBlock constructs a trailer for the provided data block and writes
 // the block and trailer to the writer. It returns the block's handle.
 func (w *layoutWriter) WriteDataBlock(b []byte, buf *blockBuf) (block.Handle, error) {
-	return w.writeBlock(b, blockkind.SSTableData, &w.compressor, buf)
+	return w.writeBlock(b, blockkind.SSTableData, buf)
 }
 
 // WritePrecompressedDataBlock writes a pre-compressed data block and its
@@ -873,7 +873,7 @@ func (w *layoutWriter) WritePrecompressedDataBlock(blk block.PhysicalBlock) (blo
 // the last-written index block's handle and adds it to the file's meta index
 // when the writer is finished.
 func (w *layoutWriter) WriteIndexBlock(b []byte) (block.Handle, error) {
-	h, err := w.writeBlock(b, blockkind.SSTableIndex, &w.compressor, &w.buf)
+	h, err := w.writeBlock(b, blockkind.SSTableIndex, &w.buf)
 	if err == nil {
 		w.lastIndexBlockHandle = h
 	}
@@ -888,29 +888,33 @@ func (w *layoutWriter) WriteFilterBlock(f filterWriter) (bh block.Handle, err er
 	if err != nil {
 		return block.Handle{}, err
 	}
-	return w.writeNamedBlock(b, blockkind.Filter, block.NoopCompressor, f.metaName())
+	return w.writeNamedBlockUncompressed(b, blockkind.Filter, f.metaName())
 }
 
 // WritePropertiesBlock constructs a trailer for the provided properties block
 // and writes the block and trailer to the writer. It automatically adds the
 // properties block to the file's meta index when the writer is finished.
-func (w *layoutWriter) WritePropertiesBlock(b []byte) (block.Handle, error) {
-	compressor := &w.compressor
+func (w *layoutWriter) WritePropertiesBlock(b []byte) (bh block.Handle, err error) {
 	// In v6 and earlier, we use a row oriented block with an infinite restart
 	// interval, which provides very good prefix compression. Since v7, we use the
 	// columnar format without prefix compression for this block; we enable block
 	// compression to compensate.
 	if w.tableFormat < TableFormatPebblev7 {
-		compressor = block.NoopCompressor
+		bh, err = w.writeBlockUncompressed(b, blockkind.Metadata, &w.buf)
+	} else {
+		bh, err = w.writeBlock(b, blockkind.Metadata, &w.buf)
+	}
+	if err == nil {
+		w.recordToMetaindex(metaPropertiesName, bh)
 	}
-	return w.writeNamedBlock(b, blockkind.Metadata, compressor, metaPropertiesName)
+	return bh, err
 }
 
 // WriteRangeKeyBlock constructs a trailer for the provided range key block and
 // writes the block and trailer to the writer. It automatically adds the range
 // key block to the file's meta index when the writer is finished.
 func (w *layoutWriter) WriteRangeKeyBlock(b []byte) (block.Handle, error) {
-	return w.writeNamedBlock(b, blockkind.RangeKey, block.NoopCompressor, metaRangeKeyName)
+	return w.writeNamedBlockUncompressed(b, blockkind.RangeKey, metaRangeKeyName)
 }
 
 // WriteBlobRefIndexBlock constructs a trailer for the provided blob reference
@@ -918,7 +922,7 @@ func (w *layoutWriter) WriteRangeKeyBlock(b []byte) (block.Handle, error) {
 // adds the blob reference index block to the file's meta index when the writer
 // is finished.
 func (w *layoutWriter) WriteBlobRefIndexBlock(b []byte) (block.Handle, error) {
-	return w.writeNamedBlock(b, blockkind.BlobReferenceValueLivenessIndex, &w.compressor, metaBlobRefIndexName)
+	return w.writeNamedBlockUncompressed(b, blockkind.BlobReferenceValueLivenessIndex, metaBlobRefIndexName)
 }
 
 // WriteRangeDeletionBlock constructs a trailer for the provided range deletion
@@ -926,13 +930,14 @@ func (w *layoutWriter) WriteBlobRefIndexBlock(b []byte) (block.Handle, error) {
 // the range deletion block to the file's meta index when the writer is
 // finished.
 func (w *layoutWriter) WriteRangeDeletionBlock(b []byte) (block.Handle, error) {
-	return w.writeNamedBlock(b, blockkind.RangeDel, block.NoopCompressor, metaRangeDelV2Name)
+	return w.writeNamedBlockUncompressed(b, blockkind.RangeDel, metaRangeDelV2Name)
 }
 
-func (w *layoutWriter) writeNamedBlock(
-	b []byte, kind block.Kind, compressor *block.Compressor, name string,
+// writeNamedBlockUncompressed writes a block without compressing it and adds it to the metaindex.
+func (w *layoutWriter) writeNamedBlockUncompressed(
+	b []byte, kind block.Kind, name string,
 ) (bh block.Handle, err error) {
-	bh, err = w.writeBlock(b, kind, compressor, &w.buf)
+	bh, err = w.writeBlockUncompressed(b, kind, &w.buf)
 	if err == nil {
 		w.recordToMetaindex(name, bh)
 	}
@@ -960,10 +965,17 @@ func (w *layoutWriter) WriteValueIndexBlock(
 }
 
 // writeBlock checksums, compresses, and writes out a block.
-func (w *layoutWriter) writeBlock(
-	b []byte, kind block.Kind, compressor *block.Compressor, buf *blockBuf,
+func (w *layoutWriter) writeBlock(b []byte, kind block.Kind, buf *blockBuf) (block.Handle, error) {
+	pb := block.CompressAndChecksum(&buf.dataBuf, b, kind, &w.compressor, &buf.checksummer)
+	h, err := w.writePrecompressedBlock(pb)
+	return h, err
+}
+
+// writeBlock checksums and writes out a block.
+func (w *layoutWriter) writeBlockUncompressed(
+	b []byte, kind block.Kind, buf *blockBuf,
 ) (block.Handle, error) {
-	pb := block.CompressAndChecksum(&buf.dataBuf, b, kind, compressor, &buf.checksummer)
+	pb := block.CopyAndChecksum(&buf.dataBuf, b, kind, &w.compressor, &buf.checksummer)
 	h, err := w.writePrecompressedBlock(pb)
 	return h, err
 }
@@ -1047,7 +1059,7 @@ func (w *layoutWriter) Finish() (size uint64, err error) {
 		}
 		b = bw.Finish()
 	}
-	metaIndexHandle, err := w.writeBlock(b, blockkind.Metadata, block.NoopCompressor, &w.buf)
+	metaIndexHandle, err := w.writeBlockUncompressed(b, blockkind.Metadata, &w.buf)
 	if err != nil {
 		return 0, err
 	}
diff --git a/sstable/properties.go b/sstable/properties.go
index a6394691..4f1165a1 100644
--- a/sstable/properties.go
+++ b/sstable/properties.go
@@ -193,6 +193,9 @@ type Properties struct {
 	SnapshotPinnedValueSize uint64 `prop:"pebble.raw.snapshot-pinned-values.size"`
 	// Size (uncompressed) of the top-level index if kTwoLevelIndexSearch is used.
 	TopLevelIndexSize uint64 `prop:"rocksdb.top-level.index.size"`
+	// The compression statistics encoded as a string. The format is:
+	// "<setting1>:<compressed1>/<uncompressed1>,<setting2>:<compressed2>/<uncompressed2>,..."
+	CompressionStats string `prop:"pebble.compression_stats"`
 	// User collected properties. Currently, we only use them to store block
 	// properties aggregated at the table level.
 	UserProperties map[string]string
@@ -468,6 +471,9 @@ func (p *Properties) accumulateProps(tblFormat TableFormat) ([]string, map[strin
 	if p.NumTombstoneDenseBlocks != 0 {
 		p.saveUvarint(m, unsafe.Offsetof(p.NumTombstoneDenseBlocks), p.NumTombstoneDenseBlocks)
 	}
+	if p.CompressionStats != "" {
+		p.saveString(m, unsafe.Offsetof(p.CompressionStats), p.CompressionStats)
+	}
 
 	if tblFormat < TableFormatPebblev1 {
 		m["rocksdb.column.family.id"] = maxInt32Slice
diff --git a/sstable/reader.go b/sstable/reader.go
index 064aef7b..d0be42b5 100644
--- a/sstable/reader.go
+++ b/sstable/reader.go
@@ -422,6 +422,18 @@ func (r *Reader) readValueBlock(
 	return r.blockReader.Read(ctx, env, readHandle, bh, blockkind.SSTableValue, noInitBlockMetadataFn)
 }
 
+func (r *Reader) ReadBlobRefIndexBlock(
+	ctx context.Context, env block.ReadEnv,
+) (block.BufferHandle, error) {
+	return r.readBlobRefIndexBlock(ctx, env, noReadHandle)
+}
+
+func (r *Reader) readBlobRefIndexBlock(
+	ctx context.Context, env block.ReadEnv, readHandle objstorage.ReadHandle,
+) (block.BufferHandle, error) {
+	return r.blockReader.Read(ctx, env, readHandle, r.blobRefIndexBH, blockkind.BlobReferenceValueLivenessIndex, noInitBlockMetadataFn)
+}
+
 // metaBufferPools is a sync pool of BufferPools used exclusively when opening a
 // table and loading its meta blocks.
 var metaBufferPools = sync.Pool{
diff --git a/sstable/rowblk_writer.go b/sstable/rowblk_writer.go
index 94ff6fe8..791f0e12 100644
--- a/sstable/rowblk_writer.go
+++ b/sstable/rowblk_writer.go
@@ -1803,7 +1803,7 @@ func (w *RawRowWriter) rewriteSuffixes(
 	}
 
 	// Copy data blocks in parallel, rewriting suffixes as we go.
-	blocks, err := rewriteDataBlocksInParallel(r, sst, wo, l.Data, from, to, concurrency, func() blockRewriter {
+	blocks, err := rewriteDataBlocksInParallel(r, sst, wo, l.Data, from, to, concurrency, w.layout.compressor.Stats(), func() blockRewriter {
 		return rowblk.NewRewriter(r.Comparer, wo.BlockRestartInterval)
 	})
 	if err != nil {
diff --git a/sstable/runlength_bitmap.go b/sstable/runlength_bitmap.go
index 6c964d4e..d5cc2cc4 100644
--- a/sstable/runlength_bitmap.go
+++ b/sstable/runlength_bitmap.go
@@ -156,9 +156,9 @@ func (bb *BitmapRunLengthEncoder) Size() int {
 	}
 }
 
-// iterSetBitsInRunLengthBitmap returns an iterator over the indices of set bits
+// IterSetBitsInRunLengthBitmap returns an iterator over the indices of set bits
 // within the provided run-length encoded bitmap.
-func iterSetBitsInRunLengthBitmap(bitmap []byte) iter.Seq[int] {
+func IterSetBitsInRunLengthBitmap(bitmap []byte) iter.Seq[int] {
 	return func(yield func(int) bool) {
 		// i is the index of the current byte in the bitmap.
 		i := 0
diff --git a/sstable/suffix_rewriter.go b/sstable/suffix_rewriter.go
index 1c6d105e..9ac1f28d 100644
--- a/sstable/suffix_rewriter.go
+++ b/sstable/suffix_rewriter.go
@@ -144,6 +144,7 @@ func rewriteDataBlocksInParallel(
 	input []block.HandleWithProperties,
 	from, to []byte,
 	concurrency int,
+	compressionStats *block.CompressionStats,
 	newDataBlockRewriter func() blockRewriter,
 ) ([]blockWithSpan, error) {
 	if !r.Attributes.Has(AttributePointKeys) {
@@ -152,6 +153,8 @@ func rewriteDataBlocksInParallel(
 	}
 	output := make([]blockWithSpan, len(input))
 
+	var compressionStatsMu sync.Mutex
+
 	g := &sync.WaitGroup{}
 	g.Add(concurrency)
 	type workerErr struct {
@@ -168,11 +171,11 @@ func rewriteDataBlocksInParallel(
 			var compressedBuf []byte
 			var inputBlock, inputBlockBuf []byte
 			checksummer := block.Checksummer{Type: opts.Checksum}
+			compressor := block.MakeCompressor(opts.Compression)
+			defer compressor.Close()
 			// We'll assume all blocks are _roughly_ equal so round-robin static partition
 			// of each worker doing every ith block is probably enough.
 			err := func() error {
-				compressor := block.MakeCompressor(opts.Compression)
-				defer compressor.Close()
 				for i := worker; i < len(input); i += concurrency {
 					bh := input[i]
 					var err error
@@ -201,6 +204,9 @@ func rewriteDataBlocksInParallel(
 			if err != nil {
 				errCh <- workerErr{worker: worker, err: err}
 			}
+			compressionStatsMu.Lock()
+			compressionStats.MergeWith(compressor.Stats())
+			defer compressionStatsMu.Unlock()
 		}()
 	}
 	g.Wait()
diff --git a/sstable/valblk/reader.go b/sstable/valblk/reader.go
index b6ae5a35..c484d70c 100644
--- a/sstable/valblk/reader.go
+++ b/sstable/valblk/reader.go
@@ -187,8 +187,8 @@ func newValueBlockFetcher(
 	}
 }
 
-// Fetch implements base.ValueFetcher.
-func (f *valueBlockFetcher) Fetch(
+// FetchHandle implements base.ValueFetcher.
+func (f *valueBlockFetcher) FetchHandle(
 	ctx context.Context, handle []byte, _ base.BlobFileID, valLen uint32, buf []byte,
 ) (val []byte, callerOwned bool, err error) {
 	if !f.closed {
diff --git a/sstable/valblk/writer.go b/sstable/valblk/writer.go
index 710a3e96..739c64fe 100644
--- a/sstable/valblk/writer.go
+++ b/sstable/valblk/writer.go
@@ -170,7 +170,7 @@ func (w *Writer) writeValueBlocksIndex(layout LayoutWriter, h IndexHandle) (Inde
 	if len(b) != 0 {
 		panic("incorrect length calculation")
 	}
-	pb, bufHandle := block.CompressAndChecksumToTempBuffer(w.buf.Data(), blockkind.Metadata, block.NoopCompressor, &w.checksummer)
+	pb, bufHandle := block.CopyAndChecksumToTempBuffer(w.buf.Data(), blockkind.Metadata, w.compressor, &w.checksummer)
 	if _, err := layout.WriteValueIndexBlock(pb, h); err != nil {
 		return IndexHandle{}, err
 	}
diff --git a/wal/wal.go b/wal/wal.go
index e53132f1..3749a961 100644
--- a/wal/wal.go
+++ b/wal/wal.go
@@ -349,6 +349,15 @@ type Manager interface {
 	// ElevateWriteStallThresholdForFailover returns true if the caller should
 	// use a high write stall threshold because the WALs are being written to
 	// the secondary dir.
+	//
+	// In practice, if this value is true, we give an unlimited memory budget
+	// for memtables. This is simpler than trying to configure an explicit
+	// value, given that memory resources can vary. When using WAL failover in
+	// CockroachDB, an OOM risk is worth tolerating for workloads that have a
+	// strict latency SLO. Also, an unlimited budget here does not mean that the
+	// disk stall in the primary will go unnoticed until the OOM -- CockroachDB
+	// is monitoring disk stalls, and we expect it to fail the node after ~60s
+	// if the primary is stalled.
 	ElevateWriteStallThresholdForFailover() bool
 	// Stats returns the latest Stats.
 	Stats() Stats



Test patch:
diff --git a/blob_rewrite_test.go b/blob_rewrite_test.go
new file mode 100644
index 00000000..b558b54f
--- /dev/null
+++ b/blob_rewrite_test.go
@@ -0,0 +1,214 @@
+// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use
+// of this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+
+package pebble
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/cockroachdb/crlib/crstrings"
+	"github.com/cockroachdb/datadriven"
+	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/blobtest"
+	"github.com/cockroachdb/pebble/internal/compact"
+	"github.com/cockroachdb/pebble/internal/manifest"
+	"github.com/cockroachdb/pebble/internal/testkeys"
+	"github.com/cockroachdb/pebble/objstorage"
+	"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
+	"github.com/cockroachdb/pebble/sstable"
+	"github.com/cockroachdb/pebble/sstable/blob"
+	"github.com/cockroachdb/pebble/sstable/block"
+	"github.com/cockroachdb/pebble/vfs"
+	"github.com/stretchr/testify/require"
+)
+
+func TestBlobRewrite(t *testing.T) {
+	var (
+		bv  blobtest.Values
+		vs  compact.ValueSeparation
+		tw  sstable.RawWriter
+		fn  base.DiskFileNum
+		buf bytes.Buffer
+		// Set up a logging FS to capture the filesystem operations throughout
+		// the test. When testing a value separation policy that writes new blob
+		// files, this demonstrates the creation of new blob files and that
+		// they're created lazily, the first time a value is actually separated.
+		fs = vfs.WithLogging(vfs.NewMem(), func(format string, args ...interface{}) {
+			fmt.Fprint(&buf, "# ")
+			fmt.Fprintf(&buf, format, args...)
+			fmt.Fprintln(&buf)
+		})
+	)
+	ctx := context.Background()
+	objStore, err := objstorageprovider.Open(objstorageprovider.Settings{
+		FS: fs,
+	})
+	require.NoError(t, err)
+
+	initRawWriter := func() {
+		if tw != nil {
+			require.NoError(t, tw.Close())
+		}
+		fn++
+		w, _, err := objStore.Create(ctx, base.FileTypeTable, fn, objstorage.CreateOptions{})
+		require.NoError(t, err)
+		tw = sstable.NewRawWriter(w, sstable.WriterOptions{
+			TableFormat: sstable.TableFormatMax,
+		})
+		tw = &loggingRawWriter{w: &buf, RawWriter: tw}
+	}
+
+	datadriven.RunTest(t, "testdata/blob_rewrite",
+		func(t *testing.T, d *datadriven.TestData) string {
+			buf.Reset()
+			switch d.Cmd {
+			case "init":
+				if tw != nil {
+					require.NoError(t, tw.Close())
+				}
+				bv = blobtest.Values{}
+				switch x := d.CmdArgs[0].String(); x {
+				case "preserve-blob-references":
+					pbr := &preserveBlobReferences{}
+					lines := crstrings.Lines(d.Input)
+					pbr.inputBlobPhysicalFiles = make(map[base.BlobFileID]*manifest.PhysicalBlobFile, len(lines))
+					for _, line := range lines {
+						bfm, err := manifest.ParseBlobFileMetadataDebug(line)
+						require.NoError(t, err)
+						fn = max(fn, bfm.Physical.FileNum)
+						pbr.inputBlobPhysicalFiles[bfm.FileID] = bfm.Physical
+					}
+					vs = pbr
+				case "write-new-blob-files":
+					newSep := &writeNewBlobFiles{
+						comparer: testkeys.Comparer,
+						newBlobObject: func() (objstorage.Writable, objstorage.ObjectMetadata, error) {
+							fn++
+							return objStore.Create(ctx, base.FileTypeBlob, fn, objstorage.CreateOptions{})
+						},
+					}
+					d.MaybeScanArgs(t, "minimum-size", &newSep.minimumSize)
+					vs = newSep
+				default:
+					t.Fatalf("unknown value separation policy: %s", x)
+				}
+				return buf.String()
+			case "add":
+				if tw == nil {
+					initRawWriter()
+				}
+				var ikv base.InternalKV
+				for _, line := range crstrings.Lines(d.Input) {
+					parts := strings.SplitN(line, ":", 2)
+					ik := base.ParseInternalKey(parts[0])
+					ikv.K = ik
+					if strings.HasPrefix(parts[1], "blob") {
+						ikv.V, err = bv.ParseInternalValue(parts[1])
+						require.NoError(t, err)
+					} else {
+						ikv.V = base.MakeInPlaceValue([]byte(parts[1]))
+					}
+					require.NoError(t, vs.Add(tw, &ikv, false /* forceObsolete */))
+				}
+				return buf.String()
+			case "close-output":
+				if tw != nil {
+					require.NoError(t, tw.Close())
+					tw = nil
+				}
+
+				meta, err := vs.FinishOutput()
+				require.NoError(t, err)
+				if meta.BlobFileObject.DiskFileNum == 0 {
+					fmt.Fprintln(&buf, "no blob file created")
+				} else {
+					fmt.Fprintf(&buf, "Blob file created: %s\n", meta.BlobFileMetadata)
+					fmt.Fprintln(&buf, meta.BlobFileStats)
+				}
+				if len(meta.BlobReferences) == 0 {
+					fmt.Fprintln(&buf, "blobrefs:[]")
+				} else {
+					fmt.Fprintln(&buf, "blobrefs:[")
+					for i, ref := range meta.BlobReferences {
+						fmt.Fprintf(&buf, " %d: %s %d\n", i, ref.FileID, ref.ValueSize)
+					}
+					fmt.Fprintln(&buf, "]")
+				}
+				return buf.String()
+			case "rewrite-blob":
+				// rewrite-blob <target-blob-file> <sstable1> <sstable2> ...
+				if len(d.CmdArgs) < 2 {
+					t.Fatalf("rewrite-blob requires at least a target blob file and one SSTable")
+				}
+				targetBlobFileStr := d.CmdArgs[0].String()
+				targetBlobFileNum, err := strconv.ParseUint(targetBlobFileStr, 10, 64)
+				if err != nil {
+					t.Fatalf("invalid blob file number %s: %v", targetBlobFileStr, err)
+				}
+				var sstableFileNums []base.DiskFileNum
+				for i := 1; i < len(d.CmdArgs); i++ {
+					sstFileNumStr := d.CmdArgs[i].String()
+					sstFileNum, err := strconv.ParseUint(sstFileNumStr, 10, 64)
+					if err != nil {
+						t.Fatalf("invalid SSTable file number %s: %v", sstFileNumStr, err)
+					}
+					sstableFileNums = append(sstableFileNums, base.DiskFileNum(sstFileNum))
+				}
+
+				fileCache := NewFileCache(1, 100)
+				mockFC := fileCache.newHandle(
+					nil,
+					objStore,
+					&base.LoggerWithNoopTracer{Logger: base.DefaultLogger},
+					sstable.ReaderOptions{},
+					func(any, error) error { return nil },
+				)
+				var sstables []*manifest.TableMetadata
+				for _, sstFileNum := range sstableFileNums {
+					sst := &manifest.TableMetadata{
+						TableBacking: &manifest.TableBacking{
+							DiskFileNum: sstFileNum,
+						},
+						BlobReferences: []manifest.BlobReference{{FileID: base.BlobFileID(targetBlobFileNum)}},
+					}
+					sstables = append(sstables, sst)
+				}
+
+				inputBlob := manifest.BlobFileMetadata{
+					FileID: base.BlobFileID(targetBlobFileNum),
+					Physical: &manifest.PhysicalBlobFile{
+						FileNum:      base.DiskFileNum(targetBlobFileNum),
+						CreationTime: uint64(time.Now().Unix()),
+					},
+				}
+				fn++
+				outputWritable, _, err := objStore.Create(ctx, base.FileTypeBlob, fn, objstorage.CreateOptions{})
+				require.NoError(t, err)
+
+				blobWriter := blob.NewFileWriter(fn, outputWritable, blob.FileWriterOptions{})
+				rewriter := newBlobFileRewriter(mockFC, block.ReadEnv{}, blobWriter, sstables, inputBlob)
+				stats, err := rewriter.Rewrite(context.Background())
+				if err != nil {
+					fmt.Fprintf(&buf, "rewrite error: %v\n", err)
+				} else {
+					fmt.Fprintf(&buf, "Successfully rewrote blob file %s to %s\n",
+						targetBlobFileStr, fn.String())
+					fmt.Fprintf(&buf, "Input SSTables: %v\n", sstableFileNums)
+					fmt.Fprintf(&buf, "SSTables with blob references: %d\n", len(sstables))
+					fmt.Fprintln(&buf, stats)
+				}
+
+				return buf.String()
+			default:
+				t.Fatalf("unknown command: %s", d.Cmd)
+			}
+			panic("unreachable")
+		})
+}
diff --git a/compaction_picker_test.go b/compaction_picker_test.go
index 55c4f4e6..2de6e803 100644
--- a/compaction_picker_test.go
+++ b/compaction_picker_test.go
@@ -267,11 +267,13 @@ func TestCompactionPickerTargetLevel(t *testing.T) {
 						break
 					}
 					fmt.Fprintf(&b, "L%d->L%d: %.1f\n", pc.startLevel.level, pc.outputLevel.level, pc.score)
+					bounds := pc.bounds.Clone()
 					inProgress = append(inProgress, compactionInfo{
 						inputs:      pc.inputs,
 						outputLevel: pc.outputLevel.level,
-						bounds:      pc.bounds.Clone(),
+						bounds:      &bounds,
 					})
+
 					if pc.outputLevel.level == 0 {
 						// Once we pick one L0->L0 compaction, we'll keep on doing so
 						// because the test isn't marking files as Compacting.
@@ -520,7 +522,13 @@ func TestCompactionPickerL0(t *testing.T) {
 							return fmt.Sprintf("cannot find compaction file %s", tableNum)
 						}
 						compactFile.CompactionState = manifest.CompactionStateCompacting
-						info.bounds = info.bounds.Union(DefaultComparer.Compare, compactFile.UserKeyBounds())
+						var bounds base.UserKeyBounds
+						if info.bounds != nil {
+							bounds = info.bounds.Union(DefaultComparer.Compare, compactFile.UserKeyBounds())
+						} else {
+							bounds = compactFile.UserKeyBounds()
+						}
+						info.bounds = &bounds
 						compactionFiles[level] = append(compactionFiles[level], compactFile)
 					}
 				}
@@ -745,7 +753,13 @@ func TestCompactionPickerConcurrency(t *testing.T) {
 							return fmt.Sprintf("cannot find compaction file %s", tableNum)
 						}
 						compactFile.CompactionState = manifest.CompactionStateCompacting
-						info.bounds = info.bounds.Union(DefaultComparer.Compare, compactFile.UserKeyBounds())
+						var bounds base.UserKeyBounds
+						if info.bounds != nil {
+							bounds = info.bounds.Union(DefaultComparer.Compare, compactFile.UserKeyBounds())
+						} else {
+							bounds = compactFile.UserKeyBounds()
+						}
+						info.bounds = &bounds
 						compactionFiles[level] = append(compactionFiles[level], compactFile)
 					}
 				}
diff --git a/compaction_test.go b/compaction_test.go
index c5caabe1..423cb6e0 100644
--- a/compaction_test.go
+++ b/compaction_test.go
@@ -1068,6 +1068,9 @@ func TestCompaction(t *testing.T) {
 				return runLSMCmd(td, d)
 
 			case "metrics":
+				d.mu.Lock()
+				d.waitTableStatsInitialLoad()
+				d.mu.Unlock()
 				m := d.Metrics()
 				return m.StringForTests()
 
diff --git a/data_test.go b/data_test.go
index 7879f8af..8ecb9a71 100644
--- a/data_test.go
+++ b/data_test.go
@@ -1640,13 +1640,6 @@ func describeLSM(d *DB, verbose bool) string {
 	} else {
 		buf.WriteString(d.mu.versions.currentVersion().String())
 	}
-	if blobFileMetas := d.mu.versions.latest.blobFiles.Metadatas(); len(blobFileMetas) > 0 {
-		buf.WriteString("Blob files:\n")
-		for _, meta := range blobFileMetas {
-			fmt.Fprintf(&buf, "  %s: [%s] %d physical bytes, %d value bytes\n",
-				meta.FileID, meta.Physical.FileNum, meta.Physical.Size, meta.Physical.ValueSize)
-		}
-	}
 	return buf.String()
 }
 
@@ -1788,8 +1781,8 @@ func parseDBOptionsArgs(opts *Options, args []datadriven.CmdArg) error {
 				opts.TargetFileSizes[i] = opts.TargetFileSizes[i-1] * 2
 			}
 		case "value-separation":
-			if len(cmdArg.Vals) != 3 {
-				return errors.New("value-separation-policy expects 3 arguments: (enabled, minimum-size, max-blob-reference-depth)")
+			if len(cmdArg.Vals) != 5 {
+				return errors.New("value-separation expects 5 arguments: (enabled, minimum-size, max-blob-reference-depth, rewrite-minimum-age, target-garbage-ratio)")
 			}
 			var policy ValueSeparationPolicy
 			var err error
@@ -1805,6 +1798,14 @@ func parseDBOptionsArgs(opts *Options, args []datadriven.CmdArg) error {
 			if err != nil {
 				return err
 			}
+			policy.RewriteMinimumAge, err = time.ParseDuration(cmdArg.Vals[3])
+			if err != nil {
+				return err
+			}
+			policy.TargetGarbageRatio, err = strconv.ParseFloat(cmdArg.Vals[4], 64)
+			if err != nil {
+				return err
+			}
 			opts.Experimental.ValueSeparationPolicy = func() ValueSeparationPolicy {
 				return policy
 			}
diff --git a/internal/base/lazy_value_test.go b/internal/base/lazy_value_test.go
index 75ace8d9..11cb0894 100644
--- a/internal/base/lazy_value_test.go
+++ b/internal/base/lazy_value_test.go
@@ -16,7 +16,7 @@ import (
 type valueFetcherFunc func(
 	handle []byte, blobFileID BlobFileID, valLen uint32, buf []byte) (val []byte, callerOwned bool, err error)
 
-func (v valueFetcherFunc) Fetch(
+func (v valueFetcherFunc) FetchHandle(
 	ctx context.Context, handle []byte, blobFileID BlobFileID, valLen uint32, buf []byte,
 ) (val []byte, callerOwned bool, err error) {
 	return v(handle, blobFileID, valLen, buf)
diff --git a/internal/blobtest/handles.go b/internal/blobtest/handles.go
index 8b2566a1..9b3a2379 100644
--- a/internal/blobtest/handles.go
+++ b/internal/blobtest/handles.go
@@ -34,8 +34,8 @@ type Values struct {
 	trackedHandles map[blob.Handle]string
 }
 
-// Fetch returns the value corresponding to the given handle.
-func (bv *Values) Fetch(
+// FetchHandle returns the value corresponding to the given handle.
+func (bv *Values) FetchHandle(
 	ctx context.Context, handleSuffix []byte, blobFileID base.BlobFileID, valLen uint32, _ []byte,
 ) (val []byte, callerOwned bool, err error) {
 	if bv.trackedHandles == nil {
@@ -243,7 +243,7 @@ func (bv *Values) WriteFiles(
 		prevID := -1
 		for i, handle := range handles {
 			if i > 0 && handles[i-1].BlockID != handle.BlockID {
-				writer.FlushForTesting()
+				writer.ForceFlush()
 				prevID = -1
 			}
 			// The user of a blobtest.Values may specify a value ID for a handle. If
diff --git a/internal/compact/iterator_test.go b/internal/compact/iterator_test.go
index f46592f7..b0586a1c 100644
--- a/internal/compact/iterator_test.go
+++ b/internal/compact/iterator_test.go
@@ -296,7 +296,7 @@ type mockBlobValueFetcher struct{}
 
 var _ base.ValueFetcher = mockBlobValueFetcher{}
 
-func (mockBlobValueFetcher) Fetch(
+func (mockBlobValueFetcher) FetchHandle(
 	ctx context.Context, handle []byte, blobFileID base.BlobFileID, valLen uint32, buf []byte,
 ) (val []byte, callerOwned bool, err error) {
 	s := fmt.Sprintf("<fetched value from blobref(%s, encodedHandle=%x, valLen=%d)>",
diff --git a/options_test.go b/options_test.go
index bd22d6dd..989cf20f 100644
--- a/options_test.go
+++ b/options_test.go
@@ -47,7 +47,8 @@ func (o *Options) testingRandomized(t testing.TB) *Options {
 			MinimumSize:           1 << rand.IntN(10), // [1, 512]
 			MaxBlobReferenceDepth: rand.IntN(10) + 1,  // [1, 10)
 			// Constrain the rewrite minimum age to [0, 15s).
-			RewriteMinimumAge: time.Duration(rand.IntN(15)) * time.Second,
+			RewriteMinimumAge:  time.Duration(rand.IntN(15)) * time.Second,
+			TargetGarbageRatio: 0.1 + rand.Float64()*0.9, // [0.1, 1.0)
 		}
 		o.Experimental.ValueSeparationPolicy = func() ValueSeparationPolicy { return policy }
 	}
diff --git a/replay/testdata/corpus/simple_val_sep b/replay/testdata/corpus/simple_val_sep
index 5b0062ae..7dbbf568 100644
--- a/replay/testdata/corpus/simple_val_sep
+++ b/replay/testdata/corpus/simple_val_sep
@@ -103,6 +103,6 @@ stat simple_val_sep/MANIFEST-000013 simple_val_sep/000015.sst simple_val_sep/000
 simple_val_sep/MANIFEST-000013:
   size: 250
 simple_val_sep/000015.sst:
-  size: 834
+  size: 865
 simple_val_sep/000016.blob:
   size: 97
diff --git a/replay/testdata/replay_val_sep b/replay/testdata/replay_val_sep
index 100cbaad..1ab9fcf7 100644
--- a/replay/testdata/replay_val_sep
+++ b/replay/testdata/replay_val_sep
@@ -5,34 +5,34 @@ tree
 ----
           /
             build/
-     870      000005.sst
+     906      000005.sst
      101      000006.blob
-     839      000008.sst
+     871      000008.sst
       97      000009.blob
       92      000011.log
-     755      000012.sst
+     756      000012.sst
       63      000014.log
-     834      000015.sst
+     865      000015.sst
       97      000016.blob
        0      LOCK
      152      MANIFEST-000010
      250      MANIFEST-000013
-    2908      OPTIONS-000003
+    2936      OPTIONS-000003
        0      marker.format-version.000011.024
        0      marker.manifest.000003.MANIFEST-000013
             simple_val_sep/
-     834      000015.sst
+     865      000015.sst
       97      000016.blob
      250      MANIFEST-000013
               checkpoint/
-     870        000005.sst
+     906        000005.sst
      101        000006.blob
-     839        000008.sst
+     871        000008.sst
       97        000009.blob
       64        000011.log
-     755        000012.sst
+     756        000012.sst
      187        MANIFEST-000013
-    2908        OPTIONS-000003
+    2936        OPTIONS-000003
        0        marker.format-version.000001.024
        0        marker.manifest.000001.MANIFEST-000013
 
@@ -96,6 +96,7 @@ cat build/OPTIONS-000003
   minimum_size=3
   max_blob_reference_depth=5
   rewrite_minimum_age=15m0s
+  target_garbage_ratio=0.00
 
 [Level "0"]
   block_restart_interval=16
diff --git a/sstable/blob/fetcher_test.go b/sstable/blob/fetcher_test.go
index cd960316..21a41f9d 100644
--- a/sstable/blob/fetcher_test.go
+++ b/sstable/blob/fetcher_test.go
@@ -143,7 +143,7 @@ func TestValueFetcher(t *testing.T) {
 			}.Encode(handleBuf[:])
 			encodedHandleSuffix := handleBuf[:n]
 
-			val, _, err := fetcher.Fetch(ctx, encodedHandleSuffix, base.BlobFileID(blobFileNum), valLen, nil)
+			val, _, err := fetcher.FetchHandle(ctx, encodedHandleSuffix, base.BlobFileID(blobFileNum), valLen, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/sstable/properties_test.go b/sstable/properties_test.go
index 661a6556..a8b5c979 100644
--- a/sstable/properties_test.go
+++ b/sstable/properties_test.go
@@ -93,6 +93,7 @@ var testProps = Properties{
 	NumValuesInValueBlocks: 23,
 	PropertyCollectorNames: "prefix collector names",
 	TopLevelIndexSize:      27,
+	CompressionStats:       "Snappy:1024/2048",
 	UserProperties: map[string]string{
 		"user-prop-a": "1",
 		"user-prop-b": "2",
diff --git a/sstable/runlength_bitmap_test.go b/sstable/runlength_bitmap_test.go
index 7d034d63..089a630c 100644
--- a/sstable/runlength_bitmap_test.go
+++ b/sstable/runlength_bitmap_test.go
@@ -43,7 +43,7 @@ func TestRunLengthBitmap(t *testing.T) {
 			// Validate that decoding the bitmap and re-encoding it yields the
 			// same encoding.
 			enc.Init()
-			for i := range iterSetBitsInRunLengthBitmap(encodedBuf) {
+			for i := range IterSetBitsInRunLengthBitmap(encodedBuf) {
 				enc.Set(i)
 			}
 			roundtripBuf = enc.FinishAndAppend(roundtripBuf[:0])
@@ -87,7 +87,7 @@ func TestRunLengthBitmap_Randomized(t *testing.T) {
 		encoded := enc.FinishAndAppend(nil)
 		enc = BitmapRunLengthEncoder{}
 		enc.Init()
-		for i := range iterSetBitsInRunLengthBitmap(encoded) {
+		for i := range IterSetBitsInRunLengthBitmap(encoded) {
 			enc.Set(i)
 		}
 		reencoded := enc.FinishAndAppend(nil)
diff --git a/sstable/testdata/columnar_writer/simple_binary b/sstable/testdata/columnar_writer/simple_binary
index 5b1ccef0..0567b886 100644
--- a/sstable/testdata/columnar_writer/simple_binary
+++ b/sstable/testdata/columnar_writer/simple_binary
@@ -80,38 +80,39 @@ sstable
  │    ├── 00000    block:0/88
  │    │   
  │    └── trailer [compression=none checksum=0x6147006e]
- ├── properties  offset: 149  length: 622
+ ├── properties  offset: 149  length: 659
  │    ├── 00000    obsolete-key (16) [restart]
  │    ├── 00016    pebble.colblk.schema (68)
- │    ├── 00084    pebble.internal.testkeys.suffixes (41)
- │    ├── 00125    pebble.raw.point-tombstone.key.size (32)
- │    ├── 00157    rocksdb.block.based.table.index.type (43)
- │    ├── 00200    rocksdb.comparator (37)
- │    ├── 00237    rocksdb.compression (23)
- │    ├── 00260    rocksdb.compression_options (106)
- │    ├── 00366    rocksdb.data.size (13)
- │    ├── 00379    rocksdb.deleted.keys (15)
- │    ├── 00394    rocksdb.filter.size (15)
- │    ├── 00409    rocksdb.index.size (14)
- │    ├── 00423    rocksdb.merge.operands (18)
- │    ├── 00441    rocksdb.merge.operator (24)
- │    ├── 00465    rocksdb.num.data.blocks (19)
- │    ├── 00484    rocksdb.num.entries (11)
- │    ├── 00495    rocksdb.num.range-deletions (19)
- │    ├── 00514    rocksdb.property.collectors (70)
- │    ├── 00584    rocksdb.raw.key.size (16)
- │    ├── 00600    rocksdb.raw.value.size (14)
+ │    ├── 00084    pebble.compression_stats (37)
+ │    ├── 00121    pebble.internal.testkeys.suffixes (41)
+ │    ├── 00162    pebble.raw.point-tombstone.key.size (32)
+ │    ├── 00194    rocksdb.block.based.table.index.type (43)
+ │    ├── 00237    rocksdb.comparator (37)
+ │    ├── 00274    rocksdb.compression (23)
+ │    ├── 00297    rocksdb.compression_options (106)
+ │    ├── 00403    rocksdb.data.size (13)
+ │    ├── 00416    rocksdb.deleted.keys (15)
+ │    ├── 00431    rocksdb.filter.size (15)
+ │    ├── 00446    rocksdb.index.size (14)
+ │    ├── 00460    rocksdb.merge.operands (18)
+ │    ├── 00478    rocksdb.merge.operator (24)
+ │    ├── 00502    rocksdb.num.data.blocks (19)
+ │    ├── 00521    rocksdb.num.entries (11)
+ │    ├── 00532    rocksdb.num.range-deletions (19)
+ │    ├── 00551    rocksdb.property.collectors (70)
+ │    ├── 00621    rocksdb.raw.key.size (16)
+ │    ├── 00637    rocksdb.raw.value.size (14)
  │    ├── restart points
- │    │    └── 00614 [restart 0]
- │    └── trailer [compression=none checksum=0x38193e35]
- ├── meta-index  offset: 776  length: 33
- │    ├── 0000    rocksdb.properties block:149/622 [restart]
+ │    │    └── 00651 [restart 0]
+ │    └── trailer [compression=none checksum=0x73801b9f]
+ ├── meta-index  offset: 813  length: 33
+ │    ├── 0000    rocksdb.properties block:149/659 [restart]
  │    ├── restart points
  │    │    └── 00025 [restart 0]
- │    └── trailer [compression=none checksum=0x816ce1ee]
- └── footer  offset: 814  length: 53
+ │    └── trailer [compression=none checksum=0x842c7bfd]
+ └── footer  offset: 851  length: 53
       ├── 000  checksum type: crc32c
-      ├── 001  meta: offset=776, length=33
+      ├── 001  meta: offset=813, length=33
       ├── 004  index: offset=93, length=51
       ├── 041  version: 5
       └── 045  magic number: 0xf09faab3f09faab3
@@ -136,6 +137,7 @@ pebble.colblk.schema: DefaultKeySchema(pebble.internal.testkeys,16)
 rocksdb.merge.operator: pebble.concatenate
 rocksdb.merge.operands: 0
 rocksdb.property.collectors: [pebble.internal.testkeys.suffixes,obsolete-key]
+pebble.compression_stats: NoCompression:88/88
 obsolete-key: hex:01
 pebble.internal.testkeys.suffixes: hex:0000ffffffffffffffffff01
 
@@ -228,37 +230,38 @@ sstable
  │    ├── 00000    block:0/116
  │    │   
  │    └── trailer [compression=none checksum=0xcf86ef66]
- ├── properties  offset: 177  length: 590
+ ├── properties  offset: 177  length: 629
  │    ├── 00000    obsolete-key (16) [restart]
  │    ├── 00016    pebble.colblk.schema (68)
- │    ├── 00084    pebble.internal.testkeys.suffixes (41)
- │    ├── 00125    rocksdb.block.based.table.index.type (43)
- │    ├── 00168    rocksdb.comparator (37)
- │    ├── 00205    rocksdb.compression (23)
- │    ├── 00228    rocksdb.compression_options (106)
- │    ├── 00334    rocksdb.data.size (13)
- │    ├── 00347    rocksdb.deleted.keys (15)
- │    ├── 00362    rocksdb.filter.size (15)
- │    ├── 00377    rocksdb.index.size (14)
- │    ├── 00391    rocksdb.merge.operands (18)
- │    ├── 00409    rocksdb.merge.operator (24)
- │    ├── 00433    rocksdb.num.data.blocks (19)
- │    ├── 00452    rocksdb.num.entries (11)
- │    ├── 00463    rocksdb.num.range-deletions (19)
- │    ├── 00482    rocksdb.property.collectors (70)
- │    ├── 00552    rocksdb.raw.key.size (16)
- │    ├── 00568    rocksdb.raw.value.size (14)
+ │    ├── 00084    pebble.compression_stats (39)
+ │    ├── 00123    pebble.internal.testkeys.suffixes (41)
+ │    ├── 00164    rocksdb.block.based.table.index.type (43)
+ │    ├── 00207    rocksdb.comparator (37)
+ │    ├── 00244    rocksdb.compression (23)
+ │    ├── 00267    rocksdb.compression_options (106)
+ │    ├── 00373    rocksdb.data.size (13)
+ │    ├── 00386    rocksdb.deleted.keys (15)
+ │    ├── 00401    rocksdb.filter.size (15)
+ │    ├── 00416    rocksdb.index.size (14)
+ │    ├── 00430    rocksdb.merge.operands (18)
+ │    ├── 00448    rocksdb.merge.operator (24)
+ │    ├── 00472    rocksdb.num.data.blocks (19)
+ │    ├── 00491    rocksdb.num.entries (11)
+ │    ├── 00502    rocksdb.num.range-deletions (19)
+ │    ├── 00521    rocksdb.property.collectors (70)
+ │    ├── 00591    rocksdb.raw.key.size (16)
+ │    ├── 00607    rocksdb.raw.value.size (14)
  │    ├── restart points
- │    │    └── 00582 [restart 0]
- │    └── trailer [compression=none checksum=0x1341bc55]
- ├── meta-index  offset: 772  length: 33
- │    ├── 0000    rocksdb.properties block:177/590 [restart]
+ │    │    └── 00621 [restart 0]
+ │    └── trailer [compression=none checksum=0x17600a6]
+ ├── meta-index  offset: 811  length: 33
+ │    ├── 0000    rocksdb.properties block:177/629 [restart]
  │    ├── restart points
  │    │    └── 00025 [restart 0]
- │    └── trailer [compression=none checksum=0x3febf671]
- └── footer  offset: 810  length: 53
+ │    └── trailer [compression=none checksum=0xa765d152]
+ └── footer  offset: 849  length: 53
       ├── 000  checksum type: crc32c
-      ├── 001  meta: offset=772, length=33
+      ├── 001  meta: offset=811, length=33
       ├── 004  index: offset=121, length=51
       ├── 041  version: 5
       └── 045  magic number: 0xf09faab3f09faab3
@@ -623,37 +626,38 @@ sstable
  │    ├── 00003    block:449/128
  │    │   
  │    └── trailer [compression=none checksum=0xd5c96d3a]
- ├── properties  offset: 697  length: 593
+ ├── properties  offset: 697  length: 632
  │    ├── 00000    obsolete-key (16) [restart]
  │    ├── 00016    pebble.colblk.schema (68)
- │    ├── 00084    pebble.internal.testkeys.suffixes (41)
- │    ├── 00125    rocksdb.block.based.table.index.type (43)
- │    ├── 00168    rocksdb.comparator (37)
- │    ├── 00205    rocksdb.compression (23)
- │    ├── 00228    rocksdb.compression_options (106)
- │    ├── 00334    rocksdb.data.size (14)
- │    ├── 00348    rocksdb.deleted.keys (15)
- │    ├── 00363    rocksdb.filter.size (15)
- │    ├── 00378    rocksdb.index.size (14)
- │    ├── 00392    rocksdb.merge.operands (18)
- │    ├── 00410    rocksdb.merge.operator (24)
- │    ├── 00434    rocksdb.num.data.blocks (19)
- │    ├── 00453    rocksdb.num.entries (11)
- │    ├── 00464    rocksdb.num.range-deletions (19)
- │    ├── 00483    rocksdb.property.collectors (70)
- │    ├── 00553    rocksdb.raw.key.size (17)
- │    ├── 00570    rocksdb.raw.value.size (15)
+ │    ├── 00084    pebble.compression_stats (39)
+ │    ├── 00123    pebble.internal.testkeys.suffixes (41)
+ │    ├── 00164    rocksdb.block.based.table.index.type (43)
+ │    ├── 00207    rocksdb.comparator (37)
+ │    ├── 00244    rocksdb.compression (23)
+ │    ├── 00267    rocksdb.compression_options (106)
+ │    ├── 00373    rocksdb.data.size (14)
+ │    ├── 00387    rocksdb.deleted.keys (15)
+ │    ├── 00402    rocksdb.filter.size (15)
+ │    ├── 00417    rocksdb.index.size (14)
+ │    ├── 00431    rocksdb.merge.operands (18)
+ │    ├── 00449    rocksdb.merge.operator (24)
+ │    ├── 00473    rocksdb.num.data.blocks (19)
+ │    ├── 00492    rocksdb.num.entries (11)
+ │    ├── 00503    rocksdb.num.range-deletions (19)
+ │    ├── 00522    rocksdb.property.collectors (70)
+ │    ├── 00592    rocksdb.raw.key.size (17)
+ │    ├── 00609    rocksdb.raw.value.size (15)
  │    ├── restart points
- │    │    └── 00585 [restart 0]
- │    └── trailer [compression=none checksum=0xa3e92c5a]
- ├── meta-index  offset: 1295  length: 33
- │    ├── 0000    rocksdb.properties block:697/593 [restart]
+ │    │    └── 00624 [restart 0]
+ │    └── trailer [compression=none checksum=0xaeac6223]
+ ├── meta-index  offset: 1334  length: 33
+ │    ├── 0000    rocksdb.properties block:697/632 [restart]
  │    ├── restart points
  │    │    └── 00025 [restart 0]
- │    └── trailer [compression=none checksum=0x13c9c4b]
- └── footer  offset: 1333  length: 53
+ │    └── trailer [compression=none checksum=0x31899c86]
+ └── footer  offset: 1372  length: 53
       ├── 000  checksum type: crc32c
-      ├── 001  meta: offset=1295, length=33
+      ├── 001  meta: offset=1334, length=33
       ├── 004  index: offset=582, length=110
       ├── 041  version: 5
       └── 045  magic number: 0xf09faab3f09faab3
@@ -681,6 +685,7 @@ pebble.colblk.schema: DefaultKeySchema(pebble.internal.testkeys,16)
 rocksdb.merge.operator: pebble.concatenate
 rocksdb.merge.operands: 0
 rocksdb.property.collectors: [pebble.internal.testkeys.suffixes,obsolete-key]
+pebble.compression_stats: NoCompression:562/562
 obsolete-key: hex:01
 pebble.internal.testkeys.suffixes: hex:0000ffffffffffffffffff01
 
@@ -1014,37 +1019,38 @@ sstable
  │    ├── 00003    block:449/128
  │    │   
  │    └── trailer [compression=none checksum=0xd5c96d3a]
- ├── properties  offset: 697  length: 593
+ ├── properties  offset: 697  length: 632
  │    ├── 00000    obsolete-key (16) [restart]
  │    ├── 00016    pebble.colblk.schema (68)
- │    ├── 00084    pebble.internal.testkeys.suffixes (41)
- │    ├── 00125    rocksdb.block.based.table.index.type (43)
- │    ├── 00168    rocksdb.comparator (37)
- │    ├── 00205    rocksdb.compression (23)
- │    ├── 00228    rocksdb.compression_options (106)
- │    ├── 00334    rocksdb.data.size (14)
- │    ├── 00348    rocksdb.deleted.keys (15)
- │    ├── 00363    rocksdb.filter.size (15)
- │    ├── 00378    rocksdb.index.size (14)
- │    ├── 00392    rocksdb.merge.operands (18)
- │    ├── 00410    rocksdb.merge.operator (24)
- │    ├── 00434    rocksdb.num.data.blocks (19)
- │    ├── 00453    rocksdb.num.entries (11)
- │    ├── 00464    rocksdb.num.range-deletions (19)
- │    ├── 00483    rocksdb.property.collectors (70)
- │    ├── 00553    rocksdb.raw.key.size (17)
- │    ├── 00570    rocksdb.raw.value.size (15)
+ │    ├── 00084    pebble.compression_stats (39)
+ │    ├── 00123    pebble.internal.testkeys.suffixes (41)
+ │    ├── 00164    rocksdb.block.based.table.index.type (43)
+ │    ├── 00207    rocksdb.comparator (37)
+ │    ├── 00244    rocksdb.compression (23)
+ │    ├── 00267    rocksdb.compression_options (106)
+ │    ├── 00373    rocksdb.data.size (14)
+ │    ├── 00387    rocksdb.deleted.keys (15)
+ │    ├── 00402    rocksdb.filter.size (15)
+ │    ├── 00417    rocksdb.index.size (14)
+ │    ├── 00431    rocksdb.merge.operands (18)
+ │    ├── 00449    rocksdb.merge.operator (24)
+ │    ├── 00473    rocksdb.num.data.blocks (19)
+ │    ├── 00492    rocksdb.num.entries (11)
+ │    ├── 00503    rocksdb.num.range-deletions (19)
+ │    ├── 00522    rocksdb.property.collectors (70)
+ │    ├── 00592    rocksdb.raw.key.size (17)
+ │    ├── 00609    rocksdb.raw.value.size (15)
  │    ├── restart points
- │    │    └── 00585 [restart 0]
- │    └── trailer [compression=none checksum=0xa3e92c5a]
- ├── meta-index  offset: 1295  length: 33
- │    ├── 0000    rocksdb.properties block:697/593 [restart]
+ │    │    └── 00624 [restart 0]
+ │    └── trailer [compression=none checksum=0xaeac6223]
+ ├── meta-index  offset: 1334  length: 33
+ │    ├── 0000    rocksdb.properties block:697/632 [restart]
  │    ├── restart points
  │    │    └── 00025 [restart 0]
- │    └── trailer [compression=none checksum=0x13c9c4b]
- └── footer  offset: 1333  length: 53
+ │    └── trailer [compression=none checksum=0x31899c86]
+ └── footer  offset: 1372  length: 53
       ├── 000  checksum type: crc32c
-      ├── 001  meta: offset=1295, length=33
+      ├── 001  meta: offset=1334, length=33
       ├── 004  index: offset=582, length=110
       ├── 041  version: 5
       └── 045  magic number: 0xf09faab3f09faab3
diff --git a/sstable/testdata/copy_span b/sstable/testdata/copy_span
index 7d7b368a..87d33f56 100644
--- a/sstable/testdata/copy_span
+++ b/sstable/testdata/copy_span
@@ -99,7 +99,7 @@ d#0,SET: foobar
 
 copy-span test3 test4 b.SET.10 cc.SET.0
 ----
-copied 889 bytes
+copied 939 bytes
 
 iter test4
 ----
@@ -109,7 +109,7 @@ d#0,SET: foobar
 
 copy-span test3 test5 a.SET.10 bb.SET.0
 ----
-copied 897 bytes
+copied 934 bytes
 
 iter test5
 ----
@@ -139,7 +139,7 @@ d#0,SET: foobar
 
 copy-span test32 test33 b.SET.10 cc.SET.0
 ----
-copied 889 bytes
+copied 939 bytes
 
 iter test33
 ----
@@ -149,7 +149,7 @@ d#0,SET: foobar
 
 copy-span test32 test34 a.SET.10 bb.SET.0
 ----
-copied 897 bytes
+copied 934 bytes
 
 iter test34
 ----
@@ -173,7 +173,7 @@ d#0,SET: foobar
 
 copy-span test3 test4 b.SET.10 cc.SET.0
 ----
-copied 906 bytes
+copied 956 bytes
 
 iter test4
 ----
@@ -183,7 +183,7 @@ d#0,SET: foobar
 
 copy-span test3 test5 a.SET.10 bb.SET.0
 ----
-copied 914 bytes
+copied 951 bytes
 
 iter test5
 ----
@@ -213,7 +213,7 @@ d#0,SET: foobar
 
 copy-span test32 test33 b.SET.10 cc.SET.0
 ----
-copied 906 bytes
+copied 956 bytes
 
 iter test33
 ----
@@ -223,7 +223,7 @@ d#0,SET: foobar
 
 copy-span test32 test34 a.SET.10 bb.SET.0
 ----
-copied 914 bytes
+copied 951 bytes
 
 iter test34
 ----
@@ -270,9 +270,9 @@ sstable
  ├── index  offset: 1235  length: 38
  ├── top-index  offset: 1278  length: 83
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 1366  length: 69
- ├── properties  offset: 1440  length: 583
- ├── meta-index  offset: 2028  length: 88
- └── footer  offset: 2121  length: 57
+ ├── properties  offset: 1440  length: 635
+ ├── meta-index  offset: 2080  length: 88
+ └── footer  offset: 2173  length: 57
 
 props bloom-sst
 ----
@@ -296,6 +296,7 @@ rocksdb.merge.operator: pebble.concatenate
 rocksdb.merge.operands: 0
 rocksdb.property.collectors: [obsolete-key]
 rocksdb.top-level.index.size: 83
+pebble.compression_stats: NoCompression:729/729,Snappy:73/84
 obsolete-key: hex:00
 
 # Ensure that CopySpan copies over a filter block even if the writer options are
diff --git a/sstable/testdata/rewriter_v5 b/sstable/testdata/rewriter_v5
index 241abfb2..e1e2bc76 100644
--- a/sstable/testdata/rewriter_v5
+++ b/sstable/testdata/rewriter_v5
@@ -45,9 +45,9 @@ sstable
  ├── index  offset: 363  length: 37
  ├── top-index  offset: 405  length: 52
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 462  length: 69
- ├── properties  offset: 536  length: 583
- ├── meta-index  offset: 1124  length: 80
- └── footer  offset: 1209  length: 53
+ ├── properties  offset: 536  length: 622
+ ├── meta-index  offset: 1163  length: 80
+ └── footer  offset: 1248  length: 53
 
 scan
 ----
@@ -80,9 +80,9 @@ sstable
  ├── index  offset: 367  length: 37
  ├── top-index  offset: 409  length: 56
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 470  length: 69
- ├── properties  offset: 544  length: 583
- ├── meta-index  offset: 1132  length: 80
- └── footer  offset: 1217  length: 53
+ ├── properties  offset: 544  length: 622
+ ├── meta-index  offset: 1171  length: 80
+ └── footer  offset: 1256  length: 53
 
 scan
 ----
@@ -115,9 +115,9 @@ sstable
  ├── index  offset: 367  length: 37
  ├── top-index  offset: 409  length: 56
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 470  length: 69
- ├── properties  offset: 544  length: 583
- ├── meta-index  offset: 1132  length: 80
- └── footer  offset: 1217  length: 53
+ ├── properties  offset: 544  length: 622
+ ├── meta-index  offset: 1171  length: 80
+ └── footer  offset: 1256  length: 53
 
 scan
 ----
@@ -150,9 +150,9 @@ sstable
  ├── index  offset: 367  length: 37
  ├── top-index  offset: 409  length: 56
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 470  length: 69
- ├── properties  offset: 544  length: 583
- ├── meta-index  offset: 1132  length: 80
- └── footer  offset: 1217  length: 53
+ ├── properties  offset: 544  length: 622
+ ├── meta-index  offset: 1171  length: 80
+ └── footer  offset: 1256  length: 53
 
 scan
 ----
@@ -186,9 +186,9 @@ sstable
  ├── index  offset: 363  length: 37
  ├── top-index  offset: 405  length: 52
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 462  length: 69
- ├── properties  offset: 536  length: 583
- ├── meta-index  offset: 1124  length: 80
- └── footer  offset: 1209  length: 53
+ ├── properties  offset: 536  length: 622
+ ├── meta-index  offset: 1163  length: 80
+ └── footer  offset: 1248  length: 53
 
 scan
 ----
diff --git a/sstable/testdata/rewriter_v6 b/sstable/testdata/rewriter_v6
index 866b57f7..117bbf86 100644
--- a/sstable/testdata/rewriter_v6
+++ b/sstable/testdata/rewriter_v6
@@ -45,9 +45,9 @@ sstable
  ├── index  offset: 363  length: 37
  ├── top-index  offset: 405  length: 52
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 462  length: 69
- ├── properties  offset: 536  length: 583
- ├── meta-index  offset: 1124  length: 88
- └── footer  offset: 1217  length: 57
+ ├── properties  offset: 536  length: 622
+ ├── meta-index  offset: 1163  length: 88
+ └── footer  offset: 1256  length: 57
 
 scan
 ----
@@ -80,9 +80,9 @@ sstable
  ├── index  offset: 367  length: 37
  ├── top-index  offset: 409  length: 56
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 470  length: 69
- ├── properties  offset: 544  length: 583
- ├── meta-index  offset: 1132  length: 88
- └── footer  offset: 1225  length: 57
+ ├── properties  offset: 544  length: 622
+ ├── meta-index  offset: 1171  length: 88
+ └── footer  offset: 1264  length: 57
 
 scan
 ----
@@ -115,9 +115,9 @@ sstable
  ├── index  offset: 367  length: 37
  ├── top-index  offset: 409  length: 56
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 470  length: 69
- ├── properties  offset: 544  length: 583
- ├── meta-index  offset: 1132  length: 88
- └── footer  offset: 1225  length: 57
+ ├── properties  offset: 544  length: 622
+ ├── meta-index  offset: 1171  length: 88
+ └── footer  offset: 1264  length: 57
 
 scan
 ----
@@ -150,9 +150,9 @@ sstable
  ├── index  offset: 367  length: 37
  ├── top-index  offset: 409  length: 56
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 470  length: 69
- ├── properties  offset: 544  length: 583
- ├── meta-index  offset: 1132  length: 88
- └── footer  offset: 1225  length: 57
+ ├── properties  offset: 544  length: 622
+ ├── meta-index  offset: 1171  length: 88
+ └── footer  offset: 1264  length: 57
 
 scan
 ----
@@ -186,9 +186,9 @@ sstable
  ├── index  offset: 363  length: 37
  ├── top-index  offset: 405  length: 52
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 462  length: 69
- ├── properties  offset: 536  length: 583
- ├── meta-index  offset: 1124  length: 88
- └── footer  offset: 1217  length: 57
+ ├── properties  offset: 536  length: 622
+ ├── meta-index  offset: 1163  length: 88
+ └── footer  offset: 1256  length: 57
 
 scan
 ----
diff --git a/sstable/testdata/rewriter_v7 b/sstable/testdata/rewriter_v7
index e15a1396..7cd96041 100644
--- a/sstable/testdata/rewriter_v7
+++ b/sstable/testdata/rewriter_v7
@@ -45,9 +45,9 @@ sstable
  ├── index  offset: 363  length: 37
  ├── top-index  offset: 405  length: 52
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 462  length: 69
- ├── properties  offset: 536  length: 572
- ├── meta-index  offset: 1113  length: 88
- └── footer  offset: 1206  length: 61
+ ├── properties  offset: 536  length: 609
+ ├── meta-index  offset: 1150  length: 88
+ └── footer  offset: 1243  length: 61
 
 scan
 ----
@@ -80,9 +80,9 @@ sstable
  ├── index  offset: 367  length: 37
  ├── top-index  offset: 409  length: 56
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 470  length: 69
- ├── properties  offset: 544  length: 572
- ├── meta-index  offset: 1121  length: 88
- └── footer  offset: 1214  length: 61
+ ├── properties  offset: 544  length: 609
+ ├── meta-index  offset: 1158  length: 88
+ └── footer  offset: 1251  length: 61
 
 scan
 ----
@@ -115,9 +115,9 @@ sstable
  ├── index  offset: 367  length: 37
  ├── top-index  offset: 409  length: 56
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 470  length: 69
- ├── properties  offset: 544  length: 572
- ├── meta-index  offset: 1121  length: 88
- └── footer  offset: 1214  length: 61
+ ├── properties  offset: 544  length: 609
+ ├── meta-index  offset: 1158  length: 88
+ └── footer  offset: 1251  length: 61
 
 scan
 ----
@@ -150,9 +150,9 @@ sstable
  ├── index  offset: 367  length: 37
  ├── top-index  offset: 409  length: 56
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 470  length: 69
- ├── properties  offset: 544  length: 572
- ├── meta-index  offset: 1121  length: 88
- └── footer  offset: 1214  length: 61
+ ├── properties  offset: 544  length: 609
+ ├── meta-index  offset: 1158  length: 88
+ └── footer  offset: 1251  length: 61
 
 scan
 ----
@@ -186,9 +186,9 @@ sstable
  ├── index  offset: 363  length: 37
  ├── top-index  offset: 405  length: 52
  ├── fullfilter.rocksdb.BuiltinBloomFilter  offset: 462  length: 69
- ├── properties  offset: 536  length: 572
- ├── meta-index  offset: 1113  length: 88
- └── footer  offset: 1206  length: 61
+ ├── properties  offset: 536  length: 609
+ ├── meta-index  offset: 1150  length: 88
+ └── footer  offset: 1243  length: 61
 
 scan
 ----
diff --git a/sstable/testdata/virtual_reader_props b/sstable/testdata/virtual_reader_props
index 258ab60a..f84488d8 100644
--- a/sstable/testdata/virtual_reader_props
+++ b/sstable/testdata/virtual_reader_props
@@ -259,7 +259,7 @@ bounds:  [a#1,SET-f#1,SET]
 filenum: 000006
 props:
   rocksdb.num.entries: 1
-  rocksdb.raw.key.size: 6
+  rocksdb.raw.key.size: 5
   rocksdb.raw.value.size: 1
   rocksdb.deleted.keys: 1
   rocksdb.num.range-deletions: 1
@@ -313,7 +313,7 @@ virtualize lower=dd.SET.5 upper=ddd.SET.6 show-props
 bounds:  [dd#5,SET-ddd#6,SET]
 filenum: 000008
 props:
-  rocksdb.num.entries: 2
+  rocksdb.num.entries: 1
   rocksdb.raw.key.size: 10
   rocksdb.raw.value.size: 2
   rocksdb.num.data.blocks: 1
diff --git a/sstable/testdata/writer_blob_value_handles b/sstable/testdata/writer_blob_value_handles
index 510a6c48..2e60194e 100644
Binary files a/sstable/testdata/writer_blob_value_handles and b/sstable/testdata/writer_blob_value_handles differ
diff --git a/sstable/testdata/writer_v5 b/sstable/testdata/writer_v5
index 3d2ffb46..620a1a58 100644
--- a/sstable/testdata/writer_v5
+++ b/sstable/testdata/writer_v5
@@ -235,9 +235,9 @@ sstable
  ├── index  offset: 278  length: 37
  ├── index  offset: 320  length: 37
  ├── top-index  offset: 362  length: 48
- ├── properties  offset: 415  length: 548
- ├── meta-index  offset: 968  length: 33
- └── footer  offset: 1006  length: 53
+ ├── properties  offset: 415  length: 580
+ ├── meta-index  offset: 1000  length: 33
+ └── footer  offset: 1038  length: 53
 
 # Exercise the non-Reader layout-decoding codepath.
 
@@ -251,9 +251,9 @@ sstable
  ├── index  offset: 278  length: 37
  ├── index  offset: 320  length: 37
  ├── top-index  offset: 362  length: 48
- ├── properties  offset: 415  length: 548
- ├── meta-index  offset: 968  length: 33
- └── footer  offset: 1006  length: 53
+ ├── properties  offset: 415  length: 580
+ ├── meta-index  offset: 1000  length: 33
+ └── footer  offset: 1038  length: 53
 
 scan
 ----
diff --git a/sstable/testdata/writer_v6 b/sstable/testdata/writer_v6
index cfcbe98d..6e4ec6ef 100644
--- a/sstable/testdata/writer_v6
+++ b/sstable/testdata/writer_v6
@@ -235,9 +235,9 @@ sstable
  ├── index  offset: 278  length: 37
  ├── index  offset: 320  length: 37
  ├── top-index  offset: 362  length: 48
- ├── properties  offset: 415  length: 548
- ├── meta-index  offset: 968  length: 46
- └── footer  offset: 1019  length: 57
+ ├── properties  offset: 415  length: 580
+ ├── meta-index  offset: 1000  length: 46
+ └── footer  offset: 1051  length: 57
 
 props
 ----
@@ -260,6 +260,7 @@ rocksdb.merge.operator: pebble.concatenate
 rocksdb.merge.operands: 0
 rocksdb.property.collectors: [obsolete-key]
 rocksdb.top-level.index.size: 48
+pebble.compression_stats: Snappy:222/261
 obsolete-key: hex:00
 
 # Exercise the non-Reader layout-decoding codepath.
@@ -274,9 +275,9 @@ sstable
  ├── index  offset: 278  length: 37
  ├── index  offset: 320  length: 37
  ├── top-index  offset: 362  length: 48
- ├── properties  offset: 415  length: 548
- ├── meta-index  offset: 968  length: 46
- └── footer  offset: 1019  length: 57
+ ├── properties  offset: 415  length: 580
+ ├── meta-index  offset: 1000  length: 46
+ └── footer  offset: 1051  length: 57
 
 scan
 ----
diff --git a/sstable/testdata/writer_v7 b/sstable/testdata/writer_v7
index 93dccd7d..d24ab249 100644
--- a/sstable/testdata/writer_v7
+++ b/sstable/testdata/writer_v7
@@ -235,9 +235,9 @@ sstable
  ├── index  offset: 278  length: 37
  ├── index  offset: 320  length: 37
  ├── top-index  offset: 362  length: 48
- ├── properties  offset: 415  length: 554
- ├── meta-index  offset: 974  length: 46
- └── footer  offset: 1025  length: 61
+ ├── properties  offset: 415  length: 550
+ ├── meta-index  offset: 970  length: 46
+ └── footer  offset: 1021  length: 61
 
 props
 ----
@@ -260,6 +260,7 @@ rocksdb.merge.operator: pebble.concatenate
 rocksdb.merge.operands: 0
 rocksdb.property.collectors: [obsolete-key]
 rocksdb.top-level.index.size: 48
+pebble.compression_stats: Snappy:222/261
 obsolete-key: hex:00
 
 # Exercise the non-Reader layout-decoding codepath.
@@ -274,9 +275,9 @@ sstable
  ├── index  offset: 278  length: 37
  ├── index  offset: 320  length: 37
  ├── top-index  offset: 362  length: 48
- ├── properties  offset: 415  length: 554
- ├── meta-index  offset: 974  length: 46
- └── footer  offset: 1025  length: 61
+ ├── properties  offset: 415  length: 550
+ ├── meta-index  offset: 970  length: 46
+ └── footer  offset: 1021  length: 61
 
 scan
 ----
diff --git a/sstable/testdata/writer_value_blocks b/sstable/testdata/writer_value_blocks
index 9cb6ff7d..b6494e4c 100644
--- a/sstable/testdata/writer_value_blocks
+++ b/sstable/testdata/writer_value_blocks
@@ -643,43 +643,44 @@ sstable
  │    └── trailer [compression=none checksum=0x60e7fb82]
  ├── value-index  offset: 680  length: 8
  │    └── trailer [compression=none checksum=0xb327e021]
- ├── properties  offset: 693  length: 610
+ ├── properties  offset: 693  length: 662
  │    ├── 00000    obsolete-key (16) [restart]
  │    ├── 00016    pebble.colblk.schema (68)
- │    ├── 00084    pebble.num.value-blocks (20)
- │    ├── 00104    pebble.num.values.in.value-blocks (21)
- │    ├── 00125    pebble.value-blocks.size (21)
- │    ├── 00146    rocksdb.block.based.table.index.type (43)
- │    ├── 00189    rocksdb.comparator (37)
- │    ├── 00226    rocksdb.compression (16)
- │    ├── 00242    rocksdb.compression_options (106)
- │    ├── 00348    rocksdb.data.size (14)
- │    ├── 00362    rocksdb.deleted.keys (15)
- │    ├── 00377    rocksdb.filter.size (15)
- │    ├── 00392    rocksdb.index.partitions (20)
- │    ├── 00412    rocksdb.index.size (9)
- │    ├── 00421    rocksdb.merge.operands (18)
- │    ├── 00439    rocksdb.merge.operator (24)
- │    ├── 00463    rocksdb.num.data.blocks (19)
- │    ├── 00482    rocksdb.num.entries (11)
- │    ├── 00493    rocksdb.num.range-deletions (19)
- │    ├── 00512    rocksdb.property.collectors (36)
- │    ├── 00548    rocksdb.raw.key.size (16)
- │    ├── 00564    rocksdb.raw.value.size (14)
- │    ├── 00578    rocksdb.top-level.index.size (24)
+ │    ├── 00084    pebble.compression_stats (52)
+ │    ├── 00136    pebble.num.value-blocks (20)
+ │    ├── 00156    pebble.num.values.in.value-blocks (21)
+ │    ├── 00177    pebble.value-blocks.size (21)
+ │    ├── 00198    rocksdb.block.based.table.index.type (43)
+ │    ├── 00241    rocksdb.comparator (37)
+ │    ├── 00278    rocksdb.compression (16)
+ │    ├── 00294    rocksdb.compression_options (106)
+ │    ├── 00400    rocksdb.data.size (14)
+ │    ├── 00414    rocksdb.deleted.keys (15)
+ │    ├── 00429    rocksdb.filter.size (15)
+ │    ├── 00444    rocksdb.index.partitions (20)
+ │    ├── 00464    rocksdb.index.size (9)
+ │    ├── 00473    rocksdb.merge.operands (18)
+ │    ├── 00491    rocksdb.merge.operator (24)
+ │    ├── 00515    rocksdb.num.data.blocks (19)
+ │    ├── 00534    rocksdb.num.entries (11)
+ │    ├── 00545    rocksdb.num.range-deletions (19)
+ │    ├── 00564    rocksdb.property.collectors (36)
+ │    ├── 00600    rocksdb.raw.key.size (16)
+ │    ├── 00616    rocksdb.raw.value.size (14)
+ │    ├── 00630    rocksdb.top-level.index.size (24)
  │    ├── restart points
- │    │    └── 00602 [restart 0]
- │    └── trailer [compression=none checksum=0xc70b061b]
- ├── meta-index  offset: 1308  length: 64
+ │    │    └── 00654 [restart 0]
+ │    └── trailer [compression=none checksum=0x3508516e]
+ ├── meta-index  offset: 1360  length: 64
  │    ├── 0000    pebble.value_index block:680/8 value-blocks-index-lengths: 1(num), 2(offset), 1(length) [restart]
- │    ├── 0027    rocksdb.properties block:693/610 [restart]
+ │    ├── 0027    rocksdb.properties block:693/662 [restart]
  │    ├── restart points
  │    │    ├── 00052 [restart 0]
  │    │    └── 00056 [restart 27]
- │    └── trailer [compression=none checksum=0x28531dc]
- └── footer  offset: 1377  length: 53
+ │    └── trailer [compression=none checksum=0xea069734]
+ └── footer  offset: 1429  length: 53
       ├── 000  checksum type: crc32c
-      ├── 001  meta: offset=1308, length=64
+      ├── 001  meta: offset=1360, length=64
       ├── 004  index: offset=562, length=77
       ├── 041  version: 5
       └── 045  magic number: 0xf09faab3f09faab3
@@ -871,37 +872,38 @@ sstable
  │    ├── 00000    block:0/100
  │    │   
  │    └── trailer [compression=none checksum=0x760132f1]
- ├── properties  offset: 146  length: 540
+ ├── properties  offset: 146  length: 572
  │    ├── 00000    obsolete-key (16) [restart]
  │    ├── 00016    pebble.colblk.schema (68)
- │    ├── 00084    pebble.raw.point-tombstone.key.size (32)
- │    ├── 00116    rocksdb.block.based.table.index.type (43)
- │    ├── 00159    rocksdb.comparator (37)
- │    ├── 00196    rocksdb.compression (16)
- │    ├── 00212    rocksdb.compression_options (106)
- │    ├── 00318    rocksdb.data.size (13)
- │    ├── 00331    rocksdb.deleted.keys (15)
- │    ├── 00346    rocksdb.filter.size (15)
- │    ├── 00361    rocksdb.index.size (14)
- │    ├── 00375    rocksdb.merge.operands (18)
- │    ├── 00393    rocksdb.merge.operator (24)
- │    ├── 00417    rocksdb.num.data.blocks (19)
- │    ├── 00436    rocksdb.num.entries (11)
- │    ├── 00447    rocksdb.num.range-deletions (19)
- │    ├── 00466    rocksdb.property.collectors (36)
- │    ├── 00502    rocksdb.raw.key.size (16)
- │    ├── 00518    rocksdb.raw.value.size (14)
+ │    ├── 00084    pebble.compression_stats (32)
+ │    ├── 00116    pebble.raw.point-tombstone.key.size (32)
+ │    ├── 00148    rocksdb.block.based.table.index.type (43)
+ │    ├── 00191    rocksdb.comparator (37)
+ │    ├── 00228    rocksdb.compression (16)
+ │    ├── 00244    rocksdb.compression_options (106)
+ │    ├── 00350    rocksdb.data.size (13)
+ │    ├── 00363    rocksdb.deleted.keys (15)
+ │    ├── 00378    rocksdb.filter.size (15)
+ │    ├── 00393    rocksdb.index.size (14)
+ │    ├── 00407    rocksdb.merge.operands (18)
+ │    ├── 00425    rocksdb.merge.operator (24)
+ │    ├── 00449    rocksdb.num.data.blocks (19)
+ │    ├── 00468    rocksdb.num.entries (11)
+ │    ├── 00479    rocksdb.num.range-deletions (19)
+ │    ├── 00498    rocksdb.property.collectors (36)
+ │    ├── 00534    rocksdb.raw.key.size (16)
+ │    ├── 00550    rocksdb.raw.value.size (14)
  │    ├── restart points
- │    │    └── 00532 [restart 0]
- │    └── trailer [compression=none checksum=0x9b925786]
- ├── meta-index  offset: 691  length: 33
- │    ├── 0000    rocksdb.properties block:146/540 [restart]
+ │    │    └── 00564 [restart 0]
+ │    └── trailer [compression=none checksum=0x2a939c82]
+ ├── meta-index  offset: 723  length: 33
+ │    ├── 0000    rocksdb.properties block:146/572 [restart]
  │    ├── restart points
  │    │    └── 00025 [restart 0]
- │    └── trailer [compression=none checksum=0xda289a22]
- └── footer  offset: 729  length: 53
+ │    └── trailer [compression=none checksum=0x6483c3e0]
+ └── footer  offset: 761  length: 53
       ├── 000  checksum type: crc32c
-      ├── 001  meta: offset=691, length=33
+      ├── 001  meta: offset=723, length=33
       ├── 004  index: offset=105, length=36
       ├── 041  version: 5
       └── 045  magic number: 0xf09faab3f09faab3
@@ -1015,38 +1017,39 @@ sstable
  │    ├── 00000    block:0/100
  │    │   
  │    └── trailer [compression=none checksum=0x760132f1]
- ├── properties  offset: 146  length: 540
+ ├── properties  offset: 146  length: 572
  │    ├── 00000    obsolete-key (16) [restart]
  │    ├── 00016    pebble.colblk.schema (68)
- │    ├── 00084    pebble.raw.point-tombstone.key.size (32)
- │    ├── 00116    rocksdb.block.based.table.index.type (43)
- │    ├── 00159    rocksdb.comparator (37)
- │    ├── 00196    rocksdb.compression (16)
- │    ├── 00212    rocksdb.compression_options (106)
- │    ├── 00318    rocksdb.data.size (13)
- │    ├── 00331    rocksdb.deleted.keys (15)
- │    ├── 00346    rocksdb.filter.size (15)
- │    ├── 00361    rocksdb.index.size (14)
- │    ├── 00375    rocksdb.merge.operands (18)
- │    ├── 00393    rocksdb.merge.operator (24)
- │    ├── 00417    rocksdb.num.data.blocks (19)
- │    ├── 00436    rocksdb.num.entries (11)
- │    ├── 00447    rocksdb.num.range-deletions (19)
- │    ├── 00466    rocksdb.property.collectors (36)
- │    ├── 00502    rocksdb.raw.key.size (16)
- │    ├── 00518    rocksdb.raw.value.size (14)
+ │    ├── 00084    pebble.compression_stats (32)
+ │    ├── 00116    pebble.raw.point-tombstone.key.size (32)
+ │    ├── 00148    rocksdb.block.based.table.index.type (43)
+ │    ├── 00191    rocksdb.comparator (37)
+ │    ├── 00228    rocksdb.compression (16)
+ │    ├── 00244    rocksdb.compression_options (106)
+ │    ├── 00350    rocksdb.data.size (13)
+ │    ├── 00363    rocksdb.deleted.keys (15)
+ │    ├── 00378    rocksdb.filter.size (15)
+ │    ├── 00393    rocksdb.index.size (14)
+ │    ├── 00407    rocksdb.merge.operands (18)
+ │    ├── 00425    rocksdb.merge.operator (24)
+ │    ├── 00449    rocksdb.num.data.blocks (19)
+ │    ├── 00468    rocksdb.num.entries (11)
+ │    ├── 00479    rocksdb.num.range-deletions (19)
+ │    ├── 00498    rocksdb.property.collectors (36)
+ │    ├── 00534    rocksdb.raw.key.size (16)
+ │    ├── 00550    rocksdb.raw.value.size (14)
  │    ├── restart points
- │    │    └── 00532 [restart 0]
- │    └── trailer [compression=none checksum=0x9b925786]
- ├── meta-index  offset: 691  length: 46
- │    ├── 0000    rocksdb.properties block:146/540
+ │    │    └── 00564 [restart 0]
+ │    └── trailer [compression=none checksum=0x2a939c82]
+ ├── meta-index  offset: 723  length: 46
+ │    ├── 0000    rocksdb.properties block:146/572
  │    │   
- │    └── trailer [compression=none checksum=0x6ac1423]
- └── footer  offset: 742  length: 57
+ │    └── trailer [compression=none checksum=0xefc171d9]
+ └── footer  offset: 774  length: 57
       ├── 000  checksum type: crc32c
-      ├── 001  meta: offset=691, length=46
+      ├── 001  meta: offset=723, length=46
       ├── 004  index: offset=105, length=36
-      ├── 041  footer checksum: 0xb0f9af10
+      ├── 041  footer checksum: 0xc96f3ee8
       ├── 045  version: 6
       └── 049  magic number: 0xf09faab3f09faab3
 
@@ -1220,42 +1223,43 @@ sstable
  │    └── trailer [compression=none checksum=0x460ef26f]
  ├── value-index  offset: 257  length: 3
  │    └── trailer [compression=none checksum=0x762643d7]
- ├── properties  offset: 265  length: 575
+ ├── properties  offset: 265  length: 638
  │    ├── 00000    obsolete-key (13)
  │    ├── 00013    pebble.colblk.schema (65)
- │    ├── 00078    pebble.num.value-blocks (24)
- │    ├── 00102    pebble.num.values.in.value-blocks (34)
- │    ├── 00136    pebble.raw.point-tombstone.key.size (36)
- │    ├── 00172    pebble.value-blocks.size (25)
- │    ├── 00197    rocksdb.block.based.table.index.type (40)
- │    ├── 00237    rocksdb.comparator (42)
- │    ├── 00279    rocksdb.compression (25)
- │    ├── 00304    rocksdb.compression_options (122)
- │    ├── 00426    rocksdb.data.size (19)
- │    ├── 00445    rocksdb.deleted.keys (21)
- │    ├── 00466    rocksdb.filter.size (20)
- │    ├── 00486    rocksdb.index.size (19)
- │    ├── 00505    rocksdb.merge.operands (23)
- │    ├── 00528    rocksdb.merge.operator (40)
- │    ├── 00568    rocksdb.num.data.blocks (24)
- │    ├── 00592    rocksdb.num.entries (20)
- │    ├── 00612    rocksdb.num.range-deletions (28)
- │    ├── 00640    rocksdb.property.collectors (41)
- │    ├── 00681    rocksdb.raw.key.size (21)
- │    ├── 00702    rocksdb.raw.value.size (23)
- │    └── trailer [compression=snappy checksum=0x7caf143a]
- ├── meta-index  offset: 845  length: 72
+ │    ├── 00078    pebble.compression_stats (58)
+ │    ├── 00136    pebble.num.value-blocks (24)
+ │    ├── 00160    pebble.num.values.in.value-blocks (34)
+ │    ├── 00194    pebble.raw.point-tombstone.key.size (36)
+ │    ├── 00230    pebble.value-blocks.size (25)
+ │    ├── 00255    rocksdb.block.based.table.index.type (40)
+ │    ├── 00295    rocksdb.comparator (42)
+ │    ├── 00337    rocksdb.compression (25)
+ │    ├── 00362    rocksdb.compression_options (122)
+ │    ├── 00484    rocksdb.data.size (19)
+ │    ├── 00503    rocksdb.deleted.keys (21)
+ │    ├── 00524    rocksdb.filter.size (20)
+ │    ├── 00544    rocksdb.index.size (19)
+ │    ├── 00563    rocksdb.merge.operands (23)
+ │    ├── 00586    rocksdb.merge.operator (40)
+ │    ├── 00626    rocksdb.num.data.blocks (24)
+ │    ├── 00650    rocksdb.num.entries (20)
+ │    ├── 00670    rocksdb.num.range-deletions (28)
+ │    ├── 00698    rocksdb.property.collectors (41)
+ │    ├── 00739    rocksdb.raw.key.size (21)
+ │    ├── 00760    rocksdb.raw.value.size (23)
+ │    └── trailer [compression=snappy checksum=0xf941a001]
+ ├── meta-index  offset: 908  length: 72
  │    ├── 0000    pebble.value_index block:257/3 value-blocks-index-lengths: 1(num), 1(offset), 1(length)
  │    │   
- │    ├── 0001    rocksdb.properties block:265/575
+ │    ├── 0001    rocksdb.properties block:265/638
  │    │   
- │    └── trailer [compression=none checksum=0xb9aeae4a]
- └── footer  offset: 922  length: 61
+ │    └── trailer [compression=none checksum=0xa018198d]
+ └── footer  offset: 985  length: 61
       ├── 000  checksum type: crc32c
-      ├── 001  meta: offset=845, length=72
+      ├── 001  meta: offset=908, length=72
       ├── 004  index: offset=191, length=36
       ├── 041  attributes: [ValueBlocks,PointKeys]
-      ├── 045  footer checksum: 0x4e964eb0
+      ├── 045  footer checksum: 0x743872ab
       ├── 049  version: 7
       └── 053  magic number: 0xf09faab3f09faab3
 
@@ -1368,36 +1372,37 @@ sstable
  │    ├── 00000    block:0/100
  │    │   
  │    └── trailer [compression=none checksum=0x760132f1]
- ├── properties  offset: 146  length: 525
+ ├── properties  offset: 146  length: 556
  │    ├── 00000    obsolete-key (13)
  │    ├── 00013    pebble.colblk.schema (65)
- │    ├── 00078    pebble.raw.point-tombstone.key.size (36)
- │    ├── 00114    rocksdb.block.based.table.index.type (40)
- │    ├── 00154    rocksdb.comparator (42)
- │    ├── 00196    rocksdb.compression (25)
- │    ├── 00221    rocksdb.compression_options (122)
- │    ├── 00343    rocksdb.data.size (18)
- │    ├── 00361    rocksdb.deleted.keys (21)
- │    ├── 00382    rocksdb.filter.size (20)
- │    ├── 00402    rocksdb.index.size (19)
- │    ├── 00421    rocksdb.merge.operands (23)
- │    ├── 00444    rocksdb.merge.operator (40)
- │    ├── 00484    rocksdb.num.data.blocks (24)
- │    ├── 00508    rocksdb.num.entries (20)
- │    ├── 00528    rocksdb.num.range-deletions (28)
- │    ├── 00556    rocksdb.property.collectors (41)
- │    ├── 00597    rocksdb.raw.key.size (21)
- │    ├── 00618    rocksdb.raw.value.size (23)
- │    └── trailer [compression=snappy checksum=0x5d7f7403]
- ├── meta-index  offset: 676  length: 46
- │    ├── 0000    rocksdb.properties block:146/525
+ │    ├── 00078    pebble.compression_stats (38)
+ │    ├── 00116    pebble.raw.point-tombstone.key.size (36)
+ │    ├── 00152    rocksdb.block.based.table.index.type (40)
+ │    ├── 00192    rocksdb.comparator (42)
+ │    ├── 00234    rocksdb.compression (25)
+ │    ├── 00259    rocksdb.compression_options (122)
+ │    ├── 00381    rocksdb.data.size (18)
+ │    ├── 00399    rocksdb.deleted.keys (21)
+ │    ├── 00420    rocksdb.filter.size (20)
+ │    ├── 00440    rocksdb.index.size (19)
+ │    ├── 00459    rocksdb.merge.operands (23)
+ │    ├── 00482    rocksdb.merge.operator (40)
+ │    ├── 00522    rocksdb.num.data.blocks (24)
+ │    ├── 00546    rocksdb.num.entries (20)
+ │    ├── 00566    rocksdb.num.range-deletions (28)
+ │    ├── 00594    rocksdb.property.collectors (41)
+ │    ├── 00635    rocksdb.raw.key.size (21)
+ │    ├── 00656    rocksdb.raw.value.size (23)
+ │    └── trailer [compression=snappy checksum=0x46ab18a9]
+ ├── meta-index  offset: 707  length: 46
+ │    ├── 0000    rocksdb.properties block:146/556
  │    │   
- │    └── trailer [compression=none checksum=0xf5a62be1]
- └── footer  offset: 727  length: 61
+ │    └── trailer [compression=none checksum=0xd1c840a0]
+ └── footer  offset: 758  length: 61
       ├── 000  checksum type: crc32c
-      ├── 001  meta: offset=676, length=46
+      ├── 001  meta: offset=707, length=46
       ├── 004  index: offset=105, length=36
       ├── 041  attributes: [PointKeys]
-      ├── 045  footer checksum: 0x3446b5e7
+      ├── 045  footer checksum: 0x7660297f
       ├── 049  version: 7
       └── 053  magic number: 0xf09faab3f09faab3
diff --git a/testdata/blob_rewrite b/testdata/blob_rewrite
new file mode 100644
index 00000000..3e2da2b9
--- /dev/null
+++ b/testdata/blob_rewrite
@@ -0,0 +1,79 @@
+init write-new-blob-files minimum-size=5
+----
+
+add
+bar#201,SET:poi
+bax#202,SET:blob{value=yaya}
+----
+# create: 000001.sst
+RawWriter.Add("bar#201,SET", "poi", false)
+RawWriter.Add("bax#202,SET", "yaya", false)
+
+add
+baz#202,SET:blob{value=yuumi}
+foo#209,SET:poiandyaya
+----
+# create: 000002.blob
+RawWriter.AddWithBlobHandle("baz#202,SET", "(f0,blk0,id0,len5)", 0, false)
+RawWriter.AddWithBlobHandle("foo#209,SET", "(f0,blk0,id1,len10)", 0, false)
+
+close-output
+----
+# sync-data: 000001.sst
+# close: 000001.sst
+# sync-data: 000002.blob
+# close: 000002.blob
+Blob file created: 000002 size:[105 (105B)] vals:[15 (15B)]
+{BlockCount: 1, ValueCount: 2, UncompressedValueBytes: 15, FileLen: 105}
+blobrefs:[
+ 0: B000002 15
+]
+
+init preserve-blob-references
+B000002 physical:{000002 size:[82530] vals:[72111]}
+----
+
+add
+a#9,SET:blob{fileNum=000002 value=darcy}
+a#5,SET:blob{fileNum=000002 value=tani}
+b#2,DEL:
+c#9,SET:paddy
+----
+# create: 000003.sst
+RawWriter.AddWithBlobHandle("a#9,SET", "(f0,blk0,id0,len5)", 5, false)
+RawWriter.AddWithBlobHandle("a#5,SET", "(f0,blk0,id1,len4)", 4, false)
+RawWriter.Add("b#2,DEL", "", false)
+RawWriter.Add("c#9,SET", "paddy", false)
+
+close-output
+----
+# sync-data: 000003.sst
+# close: 000003.sst
+no blob file created
+blobrefs:[
+ 0: B000002 9
+]
+
+rewrite-blob 000002 000001 000003
+----
+# create: 000004.blob
+# open: 000001.sst (options: *vfs.randomReadsOption)
+# read-at(846, 61): 000001.sst
+# read-at(769, 77): 000001.sst
+# read-at(209, 560): 000001.sst
+# read-at(184, 25): 000001.sst
+# open: 000003.sst (options: *vfs.randomReadsOption)
+# read-at(848, 61): 000003.sst
+# read-at(771, 77): 000003.sst
+# read-at(193, 578): 000003.sst
+# read-at(168, 25): 000003.sst
+# open: 000002.blob (options: *vfs.randomReadsOption)
+# read-at(67, 38): 000002.blob
+# read-at(37, 30): 000002.blob
+# read-at(0, 37): 000002.blob
+# sync-data: 000004.blob
+# close: 000004.blob
+Successfully rewrote blob file 000002 to 000004
+Input SSTables: [000001 000003]
+SSTables with blob references: 2
+{BlockCount: 1, ValueCount: 4, UncompressedValueBytes: 30, FileLen: 123}
diff --git a/testdata/checkpoint b/testdata/checkpoint
index d0575815..2d9262c2 100644
--- a/testdata/checkpoint
+++ b/testdata/checkpoint
@@ -254,9 +254,9 @@ read-at(700, 61): db/000005.sst
 read-at(649, 51): db/000005.sst
 read-at(132, 517): db/000005.sst
 open: db/000009.sst (options: *vfs.randomReadsOption)
-read-at(704, 61): db/000009.sst
-read-at(653, 51): db/000009.sst
-read-at(136, 517): db/000009.sst
+read-at(709, 61): db/000009.sst
+read-at(658, 51): db/000009.sst
+read-at(136, 522): db/000009.sst
 open: db/000007.sst (options: *vfs.randomReadsOption)
 read-at(700, 61): db/000007.sst
 read-at(649, 51): db/000007.sst
@@ -353,9 +353,9 @@ g 10
 scan db
 ----
 open: db/000010.sst (options: *vfs.randomReadsOption)
-read-at(709, 61): db/000010.sst
-read-at(658, 51): db/000010.sst
-read-at(141, 517): db/000010.sst
+read-at(710, 61): db/000010.sst
+read-at(659, 51): db/000010.sst
+read-at(141, 518): db/000010.sst
 read-at(100, 41): db/000010.sst
 read-at(0, 100): db/000010.sst
 a 1
@@ -582,9 +582,9 @@ close: checkpoints/checkpoint4/000008.log
 scan checkpoints/checkpoint4
 ----
 open: checkpoints/checkpoint4/000010.sst (options: *vfs.randomReadsOption)
-read-at(709, 61): checkpoints/checkpoint4/000010.sst
-read-at(658, 51): checkpoints/checkpoint4/000010.sst
-read-at(141, 517): checkpoints/checkpoint4/000010.sst
+read-at(710, 61): checkpoints/checkpoint4/000010.sst
+read-at(659, 51): checkpoints/checkpoint4/000010.sst
+read-at(141, 518): checkpoints/checkpoint4/000010.sst
 read-at(100, 41): checkpoints/checkpoint4/000010.sst
 read-at(0, 100): checkpoints/checkpoint4/000010.sst
 a 1
@@ -861,7 +861,7 @@ L6:
 # Create a new database with value separation enabled to test that blob files
 # are included in checkpoints.
 
-open valsepdb value-separation=(true,1,5)
+open valsepdb value-separation=(true,1,5,0s,1.0)
 ----
 mkdir-all: valsepdb 0755
 open-dir: .
@@ -1023,9 +1023,9 @@ close: checkpoints/checkpoint8/000004.log
 scan checkpoints/checkpoint8
 ----
 open: checkpoints/checkpoint8/000005.sst (options: *vfs.randomReadsOption)
-read-at(801, 61): checkpoints/checkpoint8/000005.sst
-read-at(724, 77): checkpoints/checkpoint8/000005.sst
-read-at(197, 527): checkpoints/checkpoint8/000005.sst
+read-at(830, 61): checkpoints/checkpoint8/000005.sst
+read-at(753, 77): checkpoints/checkpoint8/000005.sst
+read-at(197, 556): checkpoints/checkpoint8/000005.sst
 read-at(131, 41): checkpoints/checkpoint8/000005.sst
 read-at(0, 131): checkpoints/checkpoint8/000005.sst
 open: checkpoints/checkpoint8/000006.blob (options: *vfs.randomReadsOption)
@@ -1133,9 +1133,9 @@ close: checkpoints/checkpoint9/000007.log
 scan checkpoints/checkpoint9
 ----
 open: checkpoints/checkpoint9/000005.sst (options: *vfs.randomReadsOption)
-read-at(801, 61): checkpoints/checkpoint9/000005.sst
-read-at(724, 77): checkpoints/checkpoint9/000005.sst
-read-at(197, 527): checkpoints/checkpoint9/000005.sst
+read-at(830, 61): checkpoints/checkpoint9/000005.sst
+read-at(753, 77): checkpoints/checkpoint9/000005.sst
+read-at(197, 556): checkpoints/checkpoint9/000005.sst
 read-at(131, 41): checkpoints/checkpoint9/000005.sst
 read-at(0, 131): checkpoints/checkpoint9/000005.sst
 open: checkpoints/checkpoint9/000006.blob (options: *vfs.randomReadsOption)
diff --git a/testdata/compaction/compaction_cancellation b/testdata/compaction/compaction_cancellation
index 435c686f..2ffa1fe9 100644
--- a/testdata/compaction/compaction_cancellation
+++ b/testdata/compaction/compaction_cancellation
@@ -34,7 +34,7 @@ manual compaction cancelled: context canceled, current queued compactions: 0
 # One compaction was done.
 compaction-log
 ----
-[JOB 1] compacted(move) L2 [000004] (760B) Score=0.00 + L3 [] (0B) Score=0.00 -> L3 [000004] (760B), in 1.0s (1.0s total), output rate 760B/s
+[JOB 1] compacted(move) L2 [000004] (797B) Score=0.00 + L3 [] (0B) Score=0.00 -> L3 [000004] (797B), in 1.0s (1.0s total), output rate 797B/s
 
 compact a-z L2 parallel
 ----
@@ -45,8 +45,8 @@ L3:
 
 compaction-log sort
 ----
-[JOB 1] compacted(move) L2 [000005] (760B) Score=0.00 + L3 [] (0B) Score=0.00 -> L3 [000005] (760B), in 1.0s (1.0s total), output rate 760B/s
-[JOB 1] compacted(move) L2 [000006] (760B) Score=0.00 + L3 [] (0B) Score=0.00 -> L3 [000006] (760B), in 1.0s (1.0s total), output rate 760B/s
+[JOB 1] compacted(move) L2 [000005] (797B) Score=0.00 + L3 [] (0B) Score=0.00 -> L3 [000005] (797B), in 1.0s (1.0s total), output rate 797B/s
+[JOB 1] compacted(move) L2 [000006] (797B) Score=0.00 + L3 [] (0B) Score=0.00 -> L3 [000006] (797B), in 1.0s (1.0s total), output rate 797B/s
 
 # Repeat with only blocking the last of the three manual compactions.
 add-ongoing-compaction startLevel=3 outputLevel=4 start=g end=h
@@ -59,5 +59,5 @@ manual compaction cancelled: context canceled, current queued compactions: 0
 # Two compactions were done.
 compaction-log
 ----
-[JOB 1] compacted(move) L3 [000004] (760B) Score=0.00 + L4 [] (0B) Score=0.00 -> L4 [000004] (760B), in 1.0s (1.0s total), output rate 760B/s
-[JOB 1] compacted(move) L3 [000005] (760B) Score=0.00 + L4 [] (0B) Score=0.00 -> L4 [000005] (760B), in 1.0s (1.0s total), output rate 760B/s
+[JOB 1] compacted(move) L3 [000004] (797B) Score=0.00 + L4 [] (0B) Score=0.00 -> L4 [000004] (797B), in 1.0s (1.0s total), output rate 797B/s
+[JOB 1] compacted(move) L3 [000005] (797B) Score=0.00 + L4 [] (0B) Score=0.00 -> L4 [000005] (797B), in 1.0s (1.0s total), output rate 797B/s
diff --git a/testdata/compaction/score_compaction_picked_before_manual b/testdata/compaction/score_compaction_picked_before_manual
index a606cc11..11956bed 100644
--- a/testdata/compaction/score_compaction_picked_before_manual
+++ b/testdata/compaction/score_compaction_picked_before_manual
@@ -15,7 +15,7 @@ L6:
 # compaction.
 compaction-log
 ----
-[JOB 1] compacted(move) L5 [000004] (760B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000004] (760B), in 1.0s (1.0s total), output rate 760B/s
+[JOB 1] compacted(move) L5 [000004] (797B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000004] (797B), in 1.0s (1.0s total), output rate 797B/s
 
 # Do an auto score-based compaction with the same LSM as the previous test.
 define disable-multi-level lbase-max-bytes=1
@@ -33,7 +33,7 @@ L6:
 # Note the score is > 1.0 since these is a score-based compaction.
 compaction-log
 ----
-[JOB 1] compacted(move) L5 [000004] (760B) Score=974.36 + L6 [] (0B) Score=0.00 -> L6 [000004] (760B), in 1.0s (1.0s total), output rate 760B/s
+[JOB 1] compacted(move) L5 [000004] (797B) Score=996.25 + L6 [] (0B) Score=0.00 -> L6 [000004] (797B), in 1.0s (1.0s total), output rate 797B/s
 
 # With the same LSM as the previous test, try to do both a manual and
 # score-based compaction. The score-based compaction runs first.
@@ -64,4 +64,4 @@ set-disable-auto-compact v=true
 # The score-based compaction ran first.
 compaction-log
 ----
-[JOB 1] compacted(move) L5 [000004] (760B) Score=974.36 + L6 [] (0B) Score=0.00 -> L6 [000004] (760B), in 1.0s (1.0s total), output rate 760B/s
+[JOB 1] compacted(move) L5 [000004] (797B) Score=996.25 + L6 [] (0B) Score=0.00 -> L6 [000004] (797B), in 1.0s (1.0s total), output rate 797B/s
diff --git a/testdata/compaction/set_with_del_sstable_Pebblev5 b/testdata/compaction/set_with_del_sstable_Pebblev5
index 7a685caa..43726acc 100644
--- a/testdata/compaction/set_with_del_sstable_Pebblev5
+++ b/testdata/compaction/set_with_del_sstable_Pebblev5
@@ -88,7 +88,7 @@ num-entries: 1
 num-deletions: 1
 num-range-key-sets: 0
 point-deletions-bytes-estimate: 0
-range-deletions-bytes-estimate: 1460
+range-deletions-bytes-estimate: 1520
 
 compact a-e L1
 ----
@@ -106,7 +106,7 @@ num-entries: 2
 num-deletions: 1
 num-range-key-sets: 0
 point-deletions-bytes-estimate: 0
-range-deletions-bytes-estimate: 730
+range-deletions-bytes-estimate: 760
 
 # Same as above, except range tombstone covers multiple grandparent file boundaries.
 
diff --git a/testdata/compaction/set_with_del_sstable_Pebblev6 b/testdata/compaction/set_with_del_sstable_Pebblev6
index 24aac2e8..a9b74f20 100644
--- a/testdata/compaction/set_with_del_sstable_Pebblev6
+++ b/testdata/compaction/set_with_del_sstable_Pebblev6
@@ -88,7 +88,7 @@ num-entries: 1
 num-deletions: 1
 num-range-key-sets: 0
 point-deletions-bytes-estimate: 0
-range-deletions-bytes-estimate: 1494
+range-deletions-bytes-estimate: 1554
 
 compact a-e L1
 ----
@@ -106,7 +106,7 @@ num-entries: 2
 num-deletions: 1
 num-range-key-sets: 0
 point-deletions-bytes-estimate: 0
-range-deletions-bytes-estimate: 747
+range-deletions-bytes-estimate: 777
 
 # Same as above, except range tombstone covers multiple grandparent file boundaries.
 
diff --git a/testdata/compaction/value_separation b/testdata/compaction/value_separation
index cfdad1bc..1ae902f3 100644
--- a/testdata/compaction/value_separation
+++ b/testdata/compaction/value_separation
@@ -1,7 +1,7 @@
 # Test a simple sequence of flushes and compactions where all values are
 # separated.
 
-define value-separation=(true, 1, 3)
+define value-separation=(true, 1, 3, 0, 1.0)
 ----
 
 batch
@@ -12,11 +12,9 @@ set b 2
 compact a-b
 ----
 L6:
-  000005:[a#10,SET-b#11,SET] seqnums:[10-11] points:[a#10,SET-b#11,SET] size:821 blobrefs:[(B000006: 2); depth:1]
+  000005:[a#10,SET-b#11,SET] seqnums:[10-11] points:[a#10,SET-b#11,SET] size:849 blobrefs:[(B000006: 2); depth:1]
 Blob files:
   B000006 physical:{000006 size:[92 (92B)] vals:[2 (2B)]}
-Blob files:
-  B000006: [000006] 92 physical bytes, 2 value bytes
 
 batch
 set c 3
@@ -26,14 +24,11 @@ set d 4
 compact c-d
 ----
 L6:
-  000005:[a#10,SET-b#11,SET] seqnums:[10-11] points:[a#10,SET-b#11,SET] size:821 blobrefs:[(B000006: 2); depth:1]
-  000008:[c#12,SET-d#13,SET] seqnums:[12-13] points:[c#12,SET-d#13,SET] size:821 blobrefs:[(B000009: 2); depth:1]
+  000005:[a#10,SET-b#11,SET] seqnums:[10-11] points:[a#10,SET-b#11,SET] size:849 blobrefs:[(B000006: 2); depth:1]
+  000008:[c#12,SET-d#13,SET] seqnums:[12-13] points:[c#12,SET-d#13,SET] size:849 blobrefs:[(B000009: 2); depth:1]
 Blob files:
   B000006 physical:{000006 size:[92 (92B)] vals:[2 (2B)]}
   B000009 physical:{000009 size:[92 (92B)] vals:[2 (2B)]}
-Blob files:
-  B000006: [000006] 92 physical bytes, 2 value bytes
-  B000009: [000009] 92 physical bytes, 2 value bytes
 
 batch
 set b 5
@@ -43,15 +38,11 @@ set c 6
 compact a-d
 ----
 L6:
-  000013:[a#0,SET-d#0,SET] seqnums:[0-0] points:[a#0,SET-d#0,SET] size:858 blobrefs:[(B000006: 1), (B000012: 2), (B000009: 1); depth:2]
+  000013:[a#0,SET-d#0,SET] seqnums:[0-0] points:[a#0,SET-d#0,SET] size:887 blobrefs:[(B000006: 1), (B000012: 2), (B000009: 1); depth:2]
 Blob files:
   B000006 physical:{000006 size:[92 (92B)] vals:[2 (2B)]}
   B000009 physical:{000009 size:[92 (92B)] vals:[2 (2B)]}
   B000012 physical:{000012 size:[92 (92B)] vals:[2 (2B)]}
-Blob files:
-  B000006: [000006] 92 physical bytes, 2 value bytes
-  B000009: [000009] 92 physical bytes, 2 value bytes
-  B000012: [000012] 92 physical bytes, 2 value bytes
 
 batch
 del-range a e
@@ -64,7 +55,7 @@ compact a-d
 # and the value separation policy is configured to limit the blob reference
 # depth to 3.
 
-define verbose value-separation=(true, 3, 3)
+define verbose value-separation=(true, 3, 3, 0s, 1.0)
 L6 blob-depth=3
   a.SET.0:a
   b.SET.0:blob{fileNum=100002 value=bar}
@@ -73,7 +64,7 @@ L6 blob-depth=3
   z.SET.0:blob{fileNum=100004 value=zoo}
 ----
 L6:
-  000004:[a#0,SET-z#0,SET] seqnums:[0-0] points:[a#0,SET-z#0,SET] size:860 blobrefs:[(B100002: 3), (B100003: 3), (B100004: 3); depth:3]
+  000004:[a#0,SET-z#0,SET] seqnums:[0-0] points:[a#0,SET-z#0,SET] size:889 blobrefs:[(B100002: 3), (B100003: 3), (B100004: 3); depth:3]
 Blob files:
   B100002 physical:{100002 size:[92 (92B)] vals:[3 (3B)]}
   B100003 physical:{100003 size:[92 (92B)] vals:[3 (3B)]}
@@ -89,19 +80,14 @@ set e world
 flush
 ----
 L0.0:
-  000006:[d#10,SET-e#11,SET] seqnums:[10-11] points:[d#10,SET-e#11,SET] size:820 blobrefs:[(B000007: 10); depth:1]
+  000006:[d#10,SET-e#11,SET] seqnums:[10-11] points:[d#10,SET-e#11,SET] size:848 blobrefs:[(B000007: 10); depth:1]
 L6:
-  000004:[a#0,SET-z#0,SET] seqnums:[0-0] points:[a#0,SET-z#0,SET] size:860 blobrefs:[(B100002: 3), (B100003: 3), (B100004: 3); depth:3]
+  000004:[a#0,SET-z#0,SET] seqnums:[0-0] points:[a#0,SET-z#0,SET] size:889 blobrefs:[(B100002: 3), (B100003: 3), (B100004: 3); depth:3]
 Blob files:
   B000007 physical:{000007 size:[100 (100B)] vals:[10 (10B)]}
   B100002 physical:{100002 size:[92 (92B)] vals:[3 (3B)]}
   B100003 physical:{100003 size:[92 (92B)] vals:[3 (3B)]}
   B100004 physical:{100004 size:[92 (92B)] vals:[3 (3B)]}
-Blob files:
-  B000007: [000007] 100 physical bytes, 10 value bytes
-  B100002: [100002] 92 physical bytes, 3 value bytes
-  B100003: [100003] 92 physical bytes, 3 value bytes
-  B100004: [100004] 92 physical bytes, 3 value bytes
 
 # Compacting these two sstables should result in writing the values to a new
 # blob file and the removal of the no longer referenced blob files.
@@ -109,11 +95,9 @@ Blob files:
 compact a-z
 ----
 L6:
-  000008:[a#0,SET-z#0,SET] seqnums:[0-0] points:[a#0,SET-z#0,SET] size:870 blobrefs:[(B000009: 19); depth:1]
+  000008:[a#0,SET-z#0,SET] seqnums:[0-0] points:[a#0,SET-z#0,SET] size:899 blobrefs:[(B000009: 19); depth:1]
 Blob files:
   B000009 physical:{000009 size:[112 (112B)] vals:[19 (19B)]}
-Blob files:
-  B000009: [000009] 112 physical bytes, 19 value bytes
 
 # Ensure we can read the separated values by iterating over the database.
 
@@ -139,14 +123,14 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp   |     multilevel
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w  |    top   in  read
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+----------+------------------
-    0 |     0     0B     0B       0 |    -    0    0 |   41B |     0     0B |     0     0B |     1   820B |    0B |   0 24.9 |    0B    0B    0B
-    1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     1   820B |     0     0B |    0B |   0    0 |    0B    0B    0B
+    0 |     0     0B     0B       0 |    -    0    0 |   41B |     0     0B |     0     0B |     1   848B |    0B |   0 25.6 |    0B    0B    0B
+    1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     1   848B |     0     0B |    0B |   0    0 |    0B    0B    0B
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0 |    0B    0B    0B
-    3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     1   820B |     0     0B |    0B |   0    0 |    0B    0B    0B
+    3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     1   848B |     0     0B |    0B |   0    0 |    0B    0B    0B
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0 |    0B    0B    0B
-    5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     1   820B |     0     0B |    0B |   0    0 |    0B    0B    0B
-    6 |     1   870B     0B       0 |    - 0.00 0.00 |  820B |     0     0B |     0     0B |     1   870B | 1.6KB |   1 1.20 |    0B    0B    0B
-total |     1   870B     0B       0 |    -    -    - |   41B |     0     0B |     3  2.4KB |     2  1.7KB | 1.6KB |   1 49.8 |    0B    0B    0B
+    5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     1   848B |     0     0B |    0B |   0    0 |    0B    0B    0B
+    6 |     1   899B     0B       0 |    - 0.00 0.00 |  848B |     0     0B |     0     0B |     1   899B | 1.7KB |   1 1.19 |    0B    0B    0B
+total |     1   899B     0B       0 |    -    -    - |   41B |     0     0B |     3  2.5KB |     2  1.7KB | 1.7KB |   1 51.2 |    0B    0B    0B
 ------------------------------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 30B  written: 41B (37% overhead)
 Flushes: 1
@@ -156,7 +140,7 @@ MemTables: 1 (256KB)  zombie: 1 (256KB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 870B
+Local tables size: 899B
 Compression types: snappy: 1
 Table stats: all loaded
 Block cache: 4 entries (1.6KB)  hit rate: 70.3%
@@ -170,7 +154,7 @@ Cgo memory usage: 0B  block cache: 0B (data: 0B, maps: 0B, entries: 0B)  memtabl
 
 # Set the minimum size for a separated value to 5.
 
-define value-separation=(true, 5, 3)
+define value-separation=(true, 5, 3, 0s, 1.0)
 ----
 
 batch
@@ -186,7 +170,7 @@ set yaya yaya
 flush
 ----
 L0.0:
-  000005:[bar#10,SET-yaya#13,SET] seqnums:[10-13] points:[bar#10,SET-yaya#13,SET] size:772
+  000005:[bar#10,SET-yaya#13,SET] seqnums:[10-13] points:[bar#10,SET-yaya#13,SET] size:773
 
 batch
 set a a
@@ -201,13 +185,11 @@ set w world
 flush
 ----
 L0.1:
-  000007:[a#14,SET-w#17,SET] seqnums:[14-17] points:[a#14,SET-w#17,SET] size:848 blobrefs:[(B000008: 10); depth:1]
+  000007:[a#14,SET-w#17,SET] seqnums:[14-17] points:[a#14,SET-w#17,SET] size:877 blobrefs:[(B000008: 10); depth:1]
 L0.0:
-  000005:[bar#10,SET-yaya#13,SET] seqnums:[10-13] points:[bar#10,SET-yaya#13,SET] size:772
+  000005:[bar#10,SET-yaya#13,SET] seqnums:[10-13] points:[bar#10,SET-yaya#13,SET] size:773
 Blob files:
   B000008 physical:{000008 size:[100 (100B)] vals:[10 (10B)]}
-Blob files:
-  B000008: [000008] 100 physical bytes, 10 value bytes
 
 get
 a
@@ -222,7 +204,7 @@ w:world
 
 # Configure the database to require keys in the range [a,m) to be in-place.
 
-define required-in-place=(a,m) value-separation=(true,1,3)
+define required-in-place=(a,m) value-separation=(true,1,3,0s,1.0)
 ----
 
 batch
@@ -240,18 +222,16 @@ set m mango
 flush
 ----
 L0.0:
-  000005:[a#10,SET-d#13,SET] seqnums:[10-13] points:[a#10,SET-d#13,SET] size:802
-  000006:[m#14,SET-m#14,SET] seqnums:[14-14] points:[m#14,SET-m#14,SET] size:817 blobrefs:[(B000007: 5); depth:1]
+  000005:[a#10,SET-d#13,SET] seqnums:[10-13] points:[a#10,SET-d#13,SET] size:809
+  000006:[m#14,SET-m#14,SET] seqnums:[14-14] points:[m#14,SET-m#14,SET] size:845 blobrefs:[(B000007: 5); depth:1]
 Blob files:
   B000007 physical:{000007 size:[94 (94B)] vals:[5 (5B)]}
-Blob files:
-  B000007: [000007] 94 physical bytes, 5 value bytes
 
 # Construct an initial state with two overlapping files in L0, both with blob
 # references. Because these files overlap and are in separate sublevels, a
 # compaction that preserves blob references should sum their depths.
 
-define value-separation=(true,1,5) l0-compaction-threshold=2
+define value-separation=(true,1,5,0s,1.0) l0-compaction-threshold=2
 L0 blob-depth=1
   a.SET.9:a
   d.SET.9:blob{fileNum=100001 value=d}
@@ -263,9 +243,9 @@ L0 blob-depth=3
   z.SET.1:blob{fileNum=100004 value=zoo}
 ----
 L0.1:
-  000004:[a#9,SET-d#9,SET] seqnums:[9-9] points:[a#9,SET-d#9,SET] size:830 blobrefs:[(B100001: 1); depth:1]
+  000004:[a#9,SET-d#9,SET] seqnums:[9-9] points:[a#9,SET-d#9,SET] size:858 blobrefs:[(B100001: 1); depth:1]
 L0.0:
-  000005:[a#1,SET-z#1,SET] seqnums:[1-1] points:[a#1,SET-z#1,SET] size:860 blobrefs:[(B100002: 3), (B100003: 3), (B100004: 3); depth:3]
+  000005:[a#1,SET-z#1,SET] seqnums:[1-1] points:[a#1,SET-z#1,SET] size:889 blobrefs:[(B100002: 3), (B100003: 3), (B100004: 3); depth:3]
 Blob files:
   B100001 physical:{100001 size:[90 (90B)] vals:[1 (1B)]}
   B100002 physical:{100002 size:[92 (92B)] vals:[3 (3B)]}
@@ -275,24 +255,19 @@ Blob files:
 compact a-z
 ----
 L1:
-  000006:[a#0,SET-z#0,SET] seqnums:[0-0] points:[a#0,SET-z#0,SET] size:889 blobrefs:[(B100002: 3), (B100001: 1), (B100003: 3), (B100004: 3); depth:4]
+  000006:[a#0,SET-z#0,SET] seqnums:[0-0] points:[a#0,SET-z#0,SET] size:922 blobrefs:[(B100002: 3), (B100001: 1), (B100003: 3), (B100004: 3); depth:4]
 Blob files:
   B100001 physical:{100001 size:[90 (90B)] vals:[1 (1B)]}
   B100002 physical:{100002 size:[92 (92B)] vals:[3 (3B)]}
   B100003 physical:{100003 size:[92 (92B)] vals:[3 (3B)]}
   B100004 physical:{100004 size:[92 (92B)] vals:[3 (3B)]}
-Blob files:
-  B100001: [100001] 90 physical bytes, 1 value bytes
-  B100002: [100002] 92 physical bytes, 3 value bytes
-  B100003: [100003] 92 physical bytes, 3 value bytes
-  B100004: [100004] 92 physical bytes, 3 value bytes
 
 # Construct an initial state with two non-overlapping files in L0, both with
 # blob references. Because these files do NOT overlap and are in the same
 # sublevel, a compaction that preserves blob references should take the MAX of
 # their depths.
 
-define value-separation=(true,1,5) l0-compaction-threshold=2
+define value-separation=(true,1,5,0s,1.0) l0-compaction-threshold=2
 L0 blob-depth=1
   a.SET.9:a
   d.SET.9:blob{fileNum=100001 value=d}
@@ -304,8 +279,8 @@ L0 blob-depth=3
   z.SET.1:blob{fileNum=100004 value=zoo}
 ----
 L0.0:
-  000004:[a#9,SET-d#9,SET] seqnums:[9-9] points:[a#9,SET-d#9,SET] size:830 blobrefs:[(B100001: 1); depth:1]
-  000005:[e#1,SET-z#1,SET] seqnums:[1-1] points:[e#1,SET-z#1,SET] size:860 blobrefs:[(B100002: 3), (B100003: 3), (B100004: 3); depth:3]
+  000004:[a#9,SET-d#9,SET] seqnums:[9-9] points:[a#9,SET-d#9,SET] size:858 blobrefs:[(B100001: 1); depth:1]
+  000005:[e#1,SET-z#1,SET] seqnums:[1-1] points:[e#1,SET-z#1,SET] size:889 blobrefs:[(B100002: 3), (B100003: 3), (B100004: 3); depth:3]
 Blob files:
   B100001 physical:{100001 size:[90 (90B)] vals:[1 (1B)]}
   B100002 physical:{100002 size:[92 (92B)] vals:[3 (3B)]}
@@ -315,19 +290,75 @@ Blob files:
 compact a-z
 ----
 L1:
-  000006:[a#0,SET-z#0,SET] seqnums:[0-0] points:[a#0,SET-z#0,SET] size:883 blobrefs:[(B100001: 1), (B100002: 3), (B100003: 3), (B100004: 3); depth:3]
+  000006:[a#0,SET-z#0,SET] seqnums:[0-0] points:[a#0,SET-z#0,SET] size:912 blobrefs:[(B100001: 1), (B100002: 3), (B100003: 3), (B100004: 3); depth:3]
 Blob files:
   B100001 physical:{100001 size:[90 (90B)] vals:[1 (1B)]}
   B100002 physical:{100002 size:[92 (92B)] vals:[3 (3B)]}
   B100003 physical:{100003 size:[92 (92B)] vals:[3 (3B)]}
   B100004 physical:{100004 size:[92 (92B)] vals:[3 (3B)]}
+
+
+# Configure a database with a minimum rewrite age of 0s (no delay) and a target
+# garbage ratio of 0.0 (no garbage). With this configuration, any blob file that
+# contains any unreferenced values should be immediately compacted.
+
+define value-separation=(true,1,2,0s,0.0)
+----
+
+batch
+set a apple
+set b banana
+set c coconut
+set d dragonfruit
+set e elderberry
+set f fig
+set g grapefruit
+set h honeydew
+----
+
+flush
+----
+L0.0:
+  000005:[a#10,SET-h#17,SET] seqnums:[10-17] points:[a#10,SET-h#17,SET] size:922 blobrefs:[(B000006: 60); depth:1]
 Blob files:
-  B100001: [100001] 90 physical bytes, 1 value bytes
-  B100002: [100002] 92 physical bytes, 3 value bytes
-  B100003: [100003] 92 physical bytes, 3 value bytes
-  B100004: [100004] 92 physical bytes, 3 value bytes
+  B000006 physical:{000006 size:[156 (156B)] vals:[60 (60B)]}
 
-define value-separation=(true,5,5) l0-compaction-threshold=1
+# Overwrite the value of the key 'c'.
+
+batch
+set c cherry
+----
+
+flush
+----
+L0.1:
+  000008:[c#18,SET-c#18,SET] seqnums:[18-18] points:[c#18,SET-c#18,SET] size:845 blobrefs:[(B000009: 6); depth:1]
+L0.0:
+  000005:[a#10,SET-h#17,SET] seqnums:[10-17] points:[a#10,SET-h#17,SET] size:922 blobrefs:[(B000006: 60); depth:1]
+Blob files:
+  B000006 physical:{000006 size:[156 (156B)] vals:[60 (60B)]}
+  B000009 physical:{000009 size:[95 (95B)] vals:[6 (6B)]}
+
+# Compacting the two sstables will result in 'coconut' in the first blob file
+# becoming unreferenced.
+
+compact a-b
+----
+L6:
+  000010:[a#0,SET-h#0,SET] seqnums:[0-0] points:[a#0,SET-h#0,SET] size:900 blobrefs:[(B000006: 53), (B000009: 6); depth:2]
+Blob files:
+  B000006 physical:{000006 size:[156 (156B)] vals:[60 (60B)]}
+  B000009 physical:{000009 size:[95 (95B)] vals:[6 (6B)]}
+
+auto-compact
+----
+L6:
+  000010:[a#0,SET-h#0,SET] seqnums:[0-0] points:[a#0,SET-h#0,SET] size:900 blobrefs:[(B000006: 53), (B000009: 6); depth:2]
+Blob files:
+  B000006 physical:{000011 size:[150 (150B)] vals:[53 (53B)]}
+  B000009 physical:{000009 size:[95 (95B)] vals:[6 (6B)]}
+
+define value-separation=(true,5,5,0s,1.0) l0-compaction-threshold=1
 ----
 
 # Test writing a non-trivial amount of data. With a key length of 4, we'll write
@@ -344,25 +375,25 @@ wrote 475254 keys
 flush
 ----
 L0.0:
-  000005:[a@1#10,SET-bkmx@1#25669,SET] seqnums:[10-25669] points:[a@1#10,SET-bkmx@1#25669,SET] size:395027 blobrefs:[(B000006: 1642240); depth:1]
-  000007:[bkmy@1#25670,SET-cv@1#51332,SET] seqnums:[25670-51332] points:[bkmy@1#25670,SET-cv@1#51332,SET] size:394850 blobrefs:[(B000008: 1642432); depth:1]
-  000009:[cva@1#51333,SET-efnh@1#77002,SET] seqnums:[51333-77002] points:[cva@1#51333,SET-efnh@1#77002,SET] size:394334 blobrefs:[(B000010: 1642880); depth:1]
-  000011:[efni@1#77003,SET-fqaj@1#102665,SET] seqnums:[77003-102665] points:[efni@1#77003,SET-fqaj@1#102665,SET] size:394767 blobrefs:[(B000012: 1642432); depth:1]
-  000013:[fqak@1#102666,SET-hamh@1#128297,SET] seqnums:[102666-128297] points:[fqak@1#102666,SET-hamh@1#128297,SET] size:396846 blobrefs:[(B000014: 1640448); depth:1]
-  000015:[hami@1#128298,SET-ikzi@1#153958,SET] seqnums:[128298-153958] points:[hami@1#128298,SET-ikzi@1#153958,SET] size:394969 blobrefs:[(B000016: 1642304); depth:1]
-  000017:[ikzj@1#153959,SET-jvoe@1#179669,SET] seqnums:[153959-179669] points:[ikzj@1#153959,SET-jvoe@1#179669,SET] size:391542 blobrefs:[(B000018: 1645504); depth:1]
-  000019:[jvof@1#179670,SET-lgaf@1#205305,SET] seqnums:[179670-205305] points:[jvof@1#179670,SET-lgaf@1#205305,SET] size:396075 blobrefs:[(B000020: 1640704); depth:1]
-  000021:[lgag@1#205306,SET-mqno@1#230974,SET] seqnums:[205306-230974] points:[lgag@1#205306,SET-mqno@1#230974,SET] size:394413 blobrefs:[(B000022: 1642816); depth:1]
-  000023:[mqnp@1#230975,SET-obac@1#256624,SET] seqnums:[230975-256624] points:[mqnp@1#230975,SET-obac@1#256624,SET] size:395667 blobrefs:[(B000024: 1641600); depth:1]
-  000025:[obad@1#256625,SET-plml@1#282266,SET] seqnums:[256625-282266] points:[obad@1#256625,SET-plml@1#282266,SET] size:396218 blobrefs:[(B000026: 1641088); depth:1]
-  000027:[plmm@1#282267,SET-qvzf@1#307920,SET] seqnums:[282267-307920] points:[plmm@1#282267,SET-qvzf@1#307920,SET] size:395365 blobrefs:[(B000028: 1641856); depth:1]
-  000029:[qvzg@1#307921,SET-sgld@1#333553,SET] seqnums:[307921-333553] points:[qvzg@1#307921,SET-sgld@1#333553,SET] size:396776 blobrefs:[(B000030: 1640512); depth:1]
-  000031:[sgle@1#333554,SET-tqxr@1#359200,SET] seqnums:[333554-359200] points:[sgle@1#333554,SET-tqxr@1#359200,SET] size:395880 blobrefs:[(B000032: 1641408); depth:1]
-  000033:[tqxs@1#359201,SET-vbls@1#384890,SET] seqnums:[359201-384890] points:[tqxs@1#359201,SET-vbls@1#384890,SET] size:392950 blobrefs:[(B000034: 1644160); depth:1]
-  000035:[vblt@1#384891,SET-wlyf@1#410537,SET] seqnums:[384891-410537] points:[vblt@1#384891,SET-wlyf@1#410537,SET] size:395878 blobrefs:[(B000036: 1641408); depth:1]
-  000037:[wlyg@1#410538,SET-xwmc@1#436222,SET] seqnums:[410538-436222] points:[wlyg@1#410538,SET-xwmc@1#436222,SET] size:393324 blobrefs:[(B000038: 1643840); depth:1]
-  000039:[xwmd@1#436223,SET-zhar@1#461926,SET] seqnums:[436223-461926] points:[xwmd@1#436223,SET-zhar@1#461926,SET] size:391986 blobrefs:[(B000040: 1645056); depth:1]
-  000041:[zhas@1#461927,SET-zzzz@1#475263,SET] seqnums:[461927-475263] points:[zhas@1#461927,SET-zzzz@1#475263,SET] size:207451 blobrefs:[(B000042: 853568); depth:1]
+  000005:[a@1#10,SET-bkmx@1#25669,SET] seqnums:[10-25669] points:[a@1#10,SET-bkmx@1#25669,SET] size:396640 blobrefs:[(B000006: 1642240); depth:1]
+  000007:[bkmy@1#25670,SET-cv@1#51332,SET] seqnums:[25670-51332] points:[bkmy@1#25670,SET-cv@1#51332,SET] size:396466 blobrefs:[(B000008: 1642432); depth:1]
+  000009:[cva@1#51333,SET-efnh@1#77002,SET] seqnums:[51333-77002] points:[cva@1#51333,SET-efnh@1#77002,SET] size:395950 blobrefs:[(B000010: 1642880); depth:1]
+  000011:[efni@1#77003,SET-fqaj@1#102665,SET] seqnums:[77003-102665] points:[efni@1#77003,SET-fqaj@1#102665,SET] size:396383 blobrefs:[(B000012: 1642432); depth:1]
+  000013:[fqak@1#102666,SET-hamh@1#128297,SET] seqnums:[102666-128297] points:[fqak@1#102666,SET-hamh@1#128297,SET] size:398462 blobrefs:[(B000014: 1640448); depth:1]
+  000015:[hami@1#128298,SET-ikzi@1#153958,SET] seqnums:[128298-153958] points:[hami@1#128298,SET-ikzi@1#153958,SET] size:396585 blobrefs:[(B000016: 1642304); depth:1]
+  000017:[ikzj@1#153959,SET-jvoe@1#179669,SET] seqnums:[153959-179669] points:[ikzj@1#153959,SET-jvoe@1#179669,SET] size:393162 blobrefs:[(B000018: 1645504); depth:1]
+  000019:[jvof@1#179670,SET-lgaf@1#205305,SET] seqnums:[179670-205305] points:[jvof@1#179670,SET-lgaf@1#205305,SET] size:397687 blobrefs:[(B000020: 1640704); depth:1]
+  000021:[lgag@1#205306,SET-mqno@1#230974,SET] seqnums:[205306-230974] points:[lgag@1#205306,SET-mqno@1#230974,SET] size:396029 blobrefs:[(B000022: 1642816); depth:1]
+  000023:[mqnp@1#230975,SET-obac@1#256624,SET] seqnums:[230975-256624] points:[mqnp@1#230975,SET-obac@1#256624,SET] size:397283 blobrefs:[(B000024: 1641600); depth:1]
+  000025:[obad@1#256625,SET-plml@1#282266,SET] seqnums:[256625-282266] points:[obad@1#256625,SET-plml@1#282266,SET] size:397834 blobrefs:[(B000026: 1641088); depth:1]
+  000027:[plmm@1#282267,SET-qvzf@1#307920,SET] seqnums:[282267-307920] points:[plmm@1#282267,SET-qvzf@1#307920,SET] size:396981 blobrefs:[(B000028: 1641856); depth:1]
+  000029:[qvzg@1#307921,SET-sgld@1#333553,SET] seqnums:[307921-333553] points:[qvzg@1#307921,SET-sgld@1#333553,SET] size:398392 blobrefs:[(B000030: 1640512); depth:1]
+  000031:[sgle@1#333554,SET-tqxr@1#359200,SET] seqnums:[333554-359200] points:[sgle@1#333554,SET-tqxr@1#359200,SET] size:397496 blobrefs:[(B000032: 1641408); depth:1]
+  000033:[tqxs@1#359201,SET-vbls@1#384890,SET] seqnums:[359201-384890] points:[tqxs@1#359201,SET-vbls@1#384890,SET] size:394570 blobrefs:[(B000034: 1644160); depth:1]
+  000035:[vblt@1#384891,SET-wlyf@1#410537,SET] seqnums:[384891-410537] points:[vblt@1#384891,SET-wlyf@1#410537,SET] size:397494 blobrefs:[(B000036: 1641408); depth:1]
+  000037:[wlyg@1#410538,SET-xwmc@1#436222,SET] seqnums:[410538-436222] points:[wlyg@1#410538,SET-xwmc@1#436222,SET] size:394941 blobrefs:[(B000038: 1643840); depth:1]
+  000039:[xwmd@1#436223,SET-zhar@1#461926,SET] seqnums:[436223-461926] points:[xwmd@1#436223,SET-zhar@1#461926,SET] size:393606 blobrefs:[(B000040: 1645056); depth:1]
+  000041:[zhas@1#461927,SET-zzzz@1#475263,SET] seqnums:[461927-475263] points:[zhas@1#461927,SET-zzzz@1#475263,SET] size:208259 blobrefs:[(B000042: 853568); depth:1]
 Blob files:
   B000006 physical:{000006 size:[1704578 (1.6MB)] vals:[1642240 (1.6MB)]}
   B000008 physical:{000008 size:[1704776 (1.6MB)] vals:[1642432 (1.6MB)]}
@@ -383,26 +414,6 @@ Blob files:
   B000038 physical:{000038 size:[1706254 (1.6MB)] vals:[1643840 (1.6MB)]}
   B000040 physical:{000040 size:[1707508 (1.6MB)] vals:[1645056 (1.6MB)]}
   B000042 physical:{000042 size:[886008 (865KB)] vals:[853568 (834KB)]}
-Blob files:
-  B000006: [000006] 1704578 physical bytes, 1642240 value bytes
-  B000008: [000008] 1704776 physical bytes, 1642432 value bytes
-  B000010: [000010] 1705238 physical bytes, 1642880 value bytes
-  B000012: [000012] 1704776 physical bytes, 1642432 value bytes
-  B000014: [000014] 1702730 physical bytes, 1640448 value bytes
-  B000016: [000016] 1704644 physical bytes, 1642304 value bytes
-  B000018: [000018] 1707970 physical bytes, 1645504 value bytes
-  B000020: [000020] 1702994 physical bytes, 1640704 value bytes
-  B000022: [000022] 1705172 physical bytes, 1642816 value bytes
-  B000024: [000024] 1703918 physical bytes, 1641600 value bytes
-  B000026: [000026] 1703390 physical bytes, 1641088 value bytes
-  B000028: [000028] 1704182 physical bytes, 1641856 value bytes
-  B000030: [000030] 1702796 physical bytes, 1640512 value bytes
-  B000032: [000032] 1703720 physical bytes, 1641408 value bytes
-  B000034: [000034] 1706584 physical bytes, 1644160 value bytes
-  B000036: [000036] 1703720 physical bytes, 1641408 value bytes
-  B000038: [000038] 1706254 physical bytes, 1643840 value bytes
-  B000040: [000040] 1707508 physical bytes, 1645056 value bytes
-  B000042: [000042] 886008 physical bytes, 853568 value bytes
 
 # Schedule automatic compactions. These compactions should write data to L6. The
 # resulting sstables will reference multiple blob files but maintain a blob
@@ -412,15 +423,15 @@ Blob files:
 auto-compact
 ----
 L6:
-  000044:[a@1#0,SET-czms@1#0,SET] seqnums:[0-0] points:[a@1#0,SET-czms@1#0,SET] size:710255 blobrefs:[(B000006: 1642240), (B000008: 1642432), (B000010: 201984); depth:1]
-  000045:[czmt@1#0,SET-fyxn@1#0,SET] seqnums:[0-0] points:[czmt@1#0,SET-fyxn@1#0,SET] size:713611 blobrefs:[(B000010: 1440896), (B000012: 1642432), (B000014: 399936); depth:1]
-  000046:[fyxo@1#0,SET-iykr@1#0,SET] seqnums:[0-0] points:[fyxo@1#0,SET-iykr@1#0,SET] size:709530 blobrefs:[(B000014: 1240512), (B000016: 1642304), (B000018: 604544); depth:1]
-  000047:[iyks@1#0,SET-lxxi@1#0,SET] seqnums:[0-0] points:[iyks@1#0,SET-lxxi@1#0,SET] size:710414 blobrefs:[(B000018: 1040960), (B000020: 1640704), (B000022: 804800); depth:1]
-  000048:[lxxj@1#0,SET-oxiv@1#0,SET] seqnums:[0-0] points:[lxxj@1#0,SET-oxiv@1#0,SET] size:711889 blobrefs:[(B000022: 838016), (B000024: 1641600), (B000026: 1004864); depth:1]
-  000049:[oxiw@1#0,SET-rwta@1#0,SET] seqnums:[0-0] points:[oxiw@1#0,SET-rwta@1#0,SET] size:714274 blobrefs:[(B000026: 636224), (B000028: 1641856), (B000030: 1204160); depth:1]
-  000050:[rwtb@1#0,SET-uwen@1#0,SET] seqnums:[0-0] points:[rwtb@1#0,SET-uwen@1#0,SET] size:712412 blobrefs:[(B000030: 436352), (B000032: 1641408), (B000034: 1406720); depth:1]
-  000051:[uweo@1#0,SET-xvqm@1#0,SET] seqnums:[0-0] points:[uweo@1#0,SET-xvqm@1#0,SET] size:711660 blobrefs:[(B000034: 237440), (B000036: 1641408), (B000038: 1606400); depth:1]
-  000052:[xvqn@1#0,SET-zzzz@1#0,SET] seqnums:[0-0] points:[xvqn@1#0,SET-zzzz@1#0,SET] size:517915 blobrefs:[(B000038: 37440), (B000040: 1645056), (B000042: 853568); depth:1]
+  000044:[a@1#0,SET-czms@1#0,SET] seqnums:[0-0] points:[a@1#0,SET-czms@1#0,SET] size:715291 blobrefs:[(B000006: 1642240), (B000008: 1642432), (B000010: 201984); depth:1]
+  000045:[czmt@1#0,SET-fyxn@1#0,SET] seqnums:[0-0] points:[czmt@1#0,SET-fyxn@1#0,SET] size:718640 blobrefs:[(B000010: 1440896), (B000012: 1642432), (B000014: 399936); depth:1]
+  000046:[fyxo@1#0,SET-iykr@1#0,SET] seqnums:[0-0] points:[fyxo@1#0,SET-iykr@1#0,SET] size:714594 blobrefs:[(B000014: 1240512), (B000016: 1642304), (B000018: 604544); depth:1]
+  000047:[iyks@1#0,SET-lxxi@1#0,SET] seqnums:[0-0] points:[iyks@1#0,SET-lxxi@1#0,SET] size:715507 blobrefs:[(B000018: 1040960), (B000020: 1640704), (B000022: 804800); depth:1]
+  000048:[lxxj@1#0,SET-oxiv@1#0,SET] seqnums:[0-0] points:[lxxj@1#0,SET-oxiv@1#0,SET] size:716968 blobrefs:[(B000022: 838016), (B000024: 1641600), (B000026: 1004864); depth:1]
+  000049:[oxiw@1#0,SET-rwta@1#0,SET] seqnums:[0-0] points:[oxiw@1#0,SET-rwta@1#0,SET] size:719355 blobrefs:[(B000026: 636224), (B000028: 1641856), (B000030: 1204160); depth:1]
+  000050:[rwtb@1#0,SET-uwen@1#0,SET] seqnums:[0-0] points:[rwtb@1#0,SET-uwen@1#0,SET] size:717499 blobrefs:[(B000030: 436352), (B000032: 1641408), (B000034: 1406720); depth:1]
+  000051:[uweo@1#0,SET-xvqm@1#0,SET] seqnums:[0-0] points:[uweo@1#0,SET-xvqm@1#0,SET] size:716744 blobrefs:[(B000034: 237440), (B000036: 1641408), (B000038: 1606400); depth:1]
+  000052:[xvqn@1#0,SET-zzzz@1#0,SET] seqnums:[0-0] points:[xvqn@1#0,SET-zzzz@1#0,SET] size:521142 blobrefs:[(B000038: 37440), (B000040: 1645056), (B000042: 853568); depth:1]
 Blob files:
   B000006 physical:{000006 size:[1704578 (1.6MB)] vals:[1642240 (1.6MB)]}
   B000008 physical:{000008 size:[1704776 (1.6MB)] vals:[1642432 (1.6MB)]}
@@ -441,32 +452,12 @@ Blob files:
   B000038 physical:{000038 size:[1706254 (1.6MB)] vals:[1643840 (1.6MB)]}
   B000040 physical:{000040 size:[1707508 (1.6MB)] vals:[1645056 (1.6MB)]}
   B000042 physical:{000042 size:[886008 (865KB)] vals:[853568 (834KB)]}
-Blob files:
-  B000006: [000006] 1704578 physical bytes, 1642240 value bytes
-  B000008: [000008] 1704776 physical bytes, 1642432 value bytes
-  B000010: [000010] 1705238 physical bytes, 1642880 value bytes
-  B000012: [000012] 1704776 physical bytes, 1642432 value bytes
-  B000014: [000014] 1702730 physical bytes, 1640448 value bytes
-  B000016: [000016] 1704644 physical bytes, 1642304 value bytes
-  B000018: [000018] 1707970 physical bytes, 1645504 value bytes
-  B000020: [000020] 1702994 physical bytes, 1640704 value bytes
-  B000022: [000022] 1705172 physical bytes, 1642816 value bytes
-  B000024: [000024] 1703918 physical bytes, 1641600 value bytes
-  B000026: [000026] 1703390 physical bytes, 1641088 value bytes
-  B000028: [000028] 1704182 physical bytes, 1641856 value bytes
-  B000030: [000030] 1702796 physical bytes, 1640512 value bytes
-  B000032: [000032] 1703720 physical bytes, 1641408 value bytes
-  B000034: [000034] 1706584 physical bytes, 1644160 value bytes
-  B000036: [000036] 1703720 physical bytes, 1641408 value bytes
-  B000038: [000038] 1706254 physical bytes, 1643840 value bytes
-  B000040: [000040] 1707508 physical bytes, 1645056 value bytes
-  B000042: [000042] 886008 physical bytes, 853568 value bytes
 
 
 excise-dryrun b c
 ----
 would excise 1 files.
   del-table:     L6 000044
-  add-table:     L6 000053(000044):[a@1#0,SET-azzz@1#0,SET] seqnums:[0-0] points:[a@1#0,SET-azzz@1#0,SET] size:242560(710255) blobrefs:[(B000006: 560843), (B000008: 560908), (B000010: 68979); depth:1]
-  add-table:     L6 000054(000044):[c@1#0,SET-czms@1#0,SET] seqnums:[0-0] points:[c@1#0,SET-czms@1#0,SET] size:230320(710255) blobrefs:[(B000006: 532542), (B000008: 532604), (B000010: 65498); depth:1]
+  add-table:     L6 000053(000044):[a@1#0,SET-azzz@1#0,SET] seqnums:[0-0] points:[a@1#0,SET-azzz@1#0,SET] size:242560(715291) blobrefs:[(B000006: 556894), (B000008: 556959), (B000010: 68494); depth:1]
+  add-table:     L6 000054(000044):[c@1#0,SET-czms@1#0,SET] seqnums:[0-0] points:[c@1#0,SET-czms@1#0,SET] size:230320(715291) blobrefs:[(B000006: 528792), (B000008: 528854), (B000010: 65037); depth:1]
   add-backing:   000044
diff --git a/testdata/compaction_delete_only_hints b/testdata/compaction_delete_only_hints
index b24a1c03..4f9dc405 100644
--- a/testdata/compaction_delete_only_hints
+++ b/testdata/compaction_delete_only_hints
@@ -88,7 +88,7 @@ maybe-compact
 Deletion hints:
   L0.000004 b-r seqnums(tombstone=200-230, file-smallest=30, type=point-key-only)
 Compactions:
-  [JOB 100] compacted(delete-only) multilevel L2 [000005] (764B) Score=0.00 + L3 [000006] (764B) Score=0.00 + L4 [000007] (764B) Score=0.00 -> L6 [000008] (94B), in 1.0s (2.0s total), output rate 94B/s
+  [JOB 100] compacted(delete-only) multilevel L2 [000005] (769B) Score=0.00 + L3 [000006] (769B) Score=0.00 + L4 [000007] (769B) Score=0.00 -> L6 [000008] (94B), in 1.0s (2.0s total), output rate 94B/s
 
 # Verify that compaction correctly handles the presence of multiple
 # overlapping hints which might delete a file multiple times. All of the
@@ -127,7 +127,7 @@ maybe-compact
 Deletion hints:
   L1.000005 b-r seqnums(tombstone=200-230, file-smallest=30, type=point-key-only)
 Compactions:
-  [JOB 100] compacted(delete-only) multilevel L2 [000005] (764B) Score=0.00 + L3 [000006] (764B) Score=0.00 + L4 [000007] (764B) Score=0.00 -> L6 [000008] (94B), in 1.0s (2.0s total), output rate 94B/s
+  [JOB 100] compacted(delete-only) multilevel L2 [000005] (769B) Score=0.00 + L3 [000006] (769B) Score=0.00 + L4 [000007] (769B) Score=0.00 -> L6 [000008] (94B), in 1.0s (2.0s total), output rate 94B/s
 
 # Test a range tombstone that is already compacted into L6.
 
@@ -206,7 +206,7 @@ maybe-compact
 Deletion hints:
   (none)
 Compactions:
-  [JOB 100] compacted(delete-only) multilevel L2 [000005] (764B) Score=0.00 + L3 [000006] (764B) Score=0.00 + L4 [000007] (764B) Score=0.00 -> L6 [000009] (94B), in 1.0s (2.0s total), output rate 94B/s
+  [JOB 100] compacted(delete-only) multilevel L2 [000005] (769B) Score=0.00 + L3 [000006] (769B) Score=0.00 + L4 [000007] (769B) Score=0.00 -> L6 [000009] (94B), in 1.0s (2.0s total), output rate 94B/s
 
 # A deletion hint present on an sstable in a higher level should NOT result in a
 # deletion-only compaction incorrectly removing an sstable in L6 following an
@@ -255,7 +255,7 @@ L0.000001 a-z seqnums(tombstone=5-27, file-smallest=0, type=point-key-only)
 close-snapshot
 10
 ----
-[JOB 100] compacted(elision-only) L6 [000004] (829B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (753B), in 1.0s (2.0s total), output rate 753B/s
+[JOB 100] compacted(elision-only) L6 [000004] (829B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (758B), in 1.0s (2.0s total), output rate 758B/s
 
 # In previous versions of the code, the deletion hint was removed by the
 # elision-only compaction because it zeroed sequence numbers of keys with
@@ -474,7 +474,7 @@ maybe-compact
 Deletion hints:
   (none)
 Compactions:
-  [JOB 100] compacted(delete-only) multilevel L1 [000005] (756B) Score=0.00 + L2 [000006] (764B) Score=0.00 + L3 [000007] (764B) Score=0.00 + L4 [000008] (764B) Score=0.00 -> L6 [000009 000010] (95B), in 1.0s (2.0s total), output rate 95B/s
+  [JOB 100] compacted(delete-only) multilevel L1 [000005] (756B) Score=0.00 + L2 [000006] (769B) Score=0.00 + L3 [000007] (769B) Score=0.00 + L4 [000008] (769B) Score=0.00 -> L6 [000009 000010] (95B), in 1.0s (2.0s total), output rate 95B/s
 
 describe-lsm
 ----
@@ -608,7 +608,7 @@ maybe-compact
 Deletion hints:
   (none)
 Compactions:
-  [JOB 100] compacted(delete-only) L6 [000004] (901B) Score=0.00 -> L6 [000007 000008] (186B), in 1.0s (2.0s total), output rate 186B/s
+  [JOB 100] compacted(delete-only) L6 [000004] (928B) Score=0.00 -> L6 [000007 000008] (186B), in 1.0s (2.0s total), output rate 186B/s
 
 describe-lsm
 ----
@@ -753,4 +753,4 @@ L0.000006 c-h seqnums(tombstone=11-12, file-smallest=10, type=point-and-range-ke
 close-snapshot
 11
 ----
-[JOB 100] compacted(delete-only) L6 [000004] (970B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
+[JOB 100] compacted(delete-only) L6 [000004] (997B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
diff --git a/testdata/compaction_picker_pick_file b/testdata/compaction_picker_pick_file
index 941eb40f..7e52848b 100644
--- a/testdata/compaction_picker_pick_file
+++ b/testdata/compaction_picker_pick_file
@@ -16,9 +16,9 @@ L2:
 file-sizes
 ----
 L1:
-  000004:[b#11,SET-c#11,SET]: 773 bytes (773B)
+  000004:[b#11,SET-c#11,SET]: 778 bytes (778B)
 L2:
-  000005:[c#0,SET-d#0,SET]: 760 bytes (760B)
+  000005:[c#0,SET-d#0,SET]: 761 bytes (761B)
 
 pick-file L1
 ----
@@ -131,12 +131,12 @@ L6:
 file-sizes
 ----
 L5:
-  000004:[c#11,SET-e#11,SET]: 99399 bytes (97KB)
-  000005:[f#11,SET-f#11,SET]: 58120 bytes (57KB)
+  000004:[c#11,SET-e#11,SET]: 99408 bytes (97KB)
+  000005:[f#11,SET-f#11,SET]: 58129 bytes (57KB)
 L6:
-  000006:[c#0,SET-c#0,SET]: 66314 bytes (65KB)
-  000007:[e#0,SET-e#0,SET]: 66314 bytes (65KB)
-  000008:[f#0,SET-f#0,SET]: 66314 bytes (65KB)
+  000006:[c#0,SET-c#0,SET]: 66323 bytes (65KB)
+  000007:[e#0,SET-e#0,SET]: 66323 bytes (65KB)
+  000008:[f#0,SET-f#0,SET]: 66323 bytes (65KB)
 
 # Sst 5 is picked since 65KB/57KB is less than 130KB/97KB.
 pick-file L5
@@ -168,12 +168,12 @@ file-sizes
 L5:
   000010:[c#11,SET-c#11,SET]: 32862 bytes (32KB)
   000011:[e#11,SET-e#11,SET]: 191 bytes (191B)
-  000005:[f#11,SET-f#11,SET]: 58120 bytes (57KB)
+  000005:[f#11,SET-f#11,SET]: 58129 bytes (57KB)
 L6:
-  000006:[c#0,SET-c#0,SET]: 66314 bytes (65KB)
-  000009:[d#13,SET-d#13,SET]: 758 bytes (758B)
-  000007:[e#0,SET-e#0,SET]: 66314 bytes (65KB)
-  000008:[f#0,SET-f#0,SET]: 66314 bytes (65KB)
+  000006:[c#0,SET-c#0,SET]: 66323 bytes (65KB)
+  000009:[d#13,SET-d#13,SET]: 763 bytes (763B)
+  000007:[e#0,SET-e#0,SET]: 66323 bytes (65KB)
+  000008:[f#0,SET-f#0,SET]: 66323 bytes (65KB)
 
 # Superficially, sst 10 causes write amp of 65KB/32KB which is worse than sst
 # 5. But the garbage of ~64KB in the backing sst 4 is equally distributed
@@ -207,12 +207,12 @@ file-sizes
 ----
 L5:
   000011:[e#11,SET-e#11,SET]: 191 bytes (191B)
-  000005:[f#11,SET-f#11,SET]: 58120 bytes (57KB)
+  000005:[f#11,SET-f#11,SET]: 58129 bytes (57KB)
 L6:
-  000012:[c#15,SET-c#15,SET]: 758 bytes (758B)
-  000009:[d#13,SET-d#13,SET]: 758 bytes (758B)
-  000007:[e#0,SET-e#0,SET]: 66314 bytes (65KB)
-  000008:[f#0,SET-f#0,SET]: 66314 bytes (65KB)
+  000012:[c#15,SET-c#15,SET]: 763 bytes (763B)
+  000009:[d#13,SET-d#13,SET]: 763 bytes (763B)
+  000007:[e#0,SET-e#0,SET]: 66323 bytes (65KB)
+  000008:[f#0,SET-f#0,SET]: 66323 bytes (65KB)
 
 # Even though picking sst 11 seems to cause poor write amp of 65KB/126B, it is
 # picked because it is blamed for all the garbage in backing sst 4 (~96KB),
diff --git a/testdata/compaction_picker_scores b/testdata/compaction_picker_scores
index f503202e..02b1b902 100644
--- a/testdata/compaction_picker_scores
+++ b/testdata/compaction_picker_scores
@@ -37,7 +37,7 @@ num-entries: 1
 num-deletions: 1
 num-range-key-sets: 0
 point-deletions-bytes-estimate: 0
-range-deletions-bytes-estimate: 328895
+range-deletions-bytes-estimate: 328905
 
 scores
 ----
@@ -80,7 +80,7 @@ L1       0B       0.00     0.00           0.00
 L2       0B       0.00     0.00           0.00
 L3       0B       0.00     0.00           0.00
 L4       0B       0.00     0.00           0.00
-L5       780B     0.00     0.01           0.01
+L5       816B     0.00     0.01           0.01
 L6       321KB    1.11     1.11           1.11
 
 enable-table-stats
@@ -103,7 +103,7 @@ L1       0B       0.00     0.00           0.00
 L2       0B       0.00     0.00           0.00
 L3       0B       0.00     0.00           0.00
 L4       0B       0.00     0.00           0.00
-L5       780B     0.01     0.01           2.51
+L5       816B     0.01     0.01           2.51
 L6       321KB    1.11     1.11           1.11
 
 # Run a similar test as above, but this time the table containing the DELs is
@@ -212,11 +212,11 @@ L6       386KB    0.00     0.42           0.42
 lsm verbose
 ----
 L5:
-  000004:[aa#2,SET-dd#2,SET] seqnums:[2-2] points:[aa#2,SET-dd#2,SET] size:525403
-  000005:[e#2,SET-e#2,SET] seqnums:[2-2] points:[e#2,SET-e#2,SET] size:131858
+  000004:[aa#2,SET-dd#2,SET] seqnums:[2-2] points:[aa#2,SET-dd#2,SET] size:525413
+  000005:[e#2,SET-e#2,SET] seqnums:[2-2] points:[e#2,SET-e#2,SET] size:131868
 L6:
-  000006:[a#1,SET-d#1,SET] seqnums:[1-1] points:[a#1,SET-d#1,SET] size:263255
-  000007:[e#1,SET-e#1,SET] seqnums:[1-1] points:[e#1,SET-e#1,SET] size:131858
+  000006:[a#1,SET-d#1,SET] seqnums:[1-1] points:[a#1,SET-d#1,SET] size:263265
+  000007:[e#1,SET-e#1,SET] seqnums:[1-1] points:[e#1,SET-e#1,SET] size:131868
 
 # Attempting to schedule a compaction should begin a L5->L6 compaction.
 
diff --git a/testdata/compaction_tombstones b/testdata/compaction_tombstones
index a4327c09..735611ce 100644
--- a/testdata/compaction_tombstones
+++ b/testdata/compaction_tombstones
@@ -85,7 +85,7 @@ range-deletions-bytes-estimate: 0
 
 maybe-compact
 ----
-[JOB 100] compacted(elision-only) L6 [000004] (771B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (753B), in 1.0s (2.0s total), output rate 753B/s
+[JOB 100] compacted(elision-only) L6 [000004] (802B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (758B), in 1.0s (2.0s total), output rate 758B/s
 
 version
 ----
@@ -134,7 +134,7 @@ close-snapshot
 close-snapshot
 103
 ----
-[JOB 100] compacted(elision-only) L6 [000004] (910B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
+[JOB 100] compacted(elision-only) L6 [000004] (939B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
 
 # Test a table that contains both deletions and non-deletions, but whose
 # non-deletions well outnumber its deletions. The table should not be
@@ -208,7 +208,7 @@ range-deletions-bytes-estimate: 16824
 
 maybe-compact
 ----
-[JOB 100] compacted(default) L5 [000004 000005] (26KB) Score=89.09 + L6 [000007] (17KB) Score=0.00 -> L6 [000009] (25KB), in 1.0s (2.0s total), output rate 25KB/s
+[JOB 100] compacted(default) L5 [000004 000005] (26KB) Score=89.06 + L6 [000007] (17KB) Score=0.00 -> L6 [000009] (25KB), in 1.0s (2.0s total), output rate 25KB/s
 
 define level-max-bytes=(L5 : 1000) auto-compactions=off
 L5
@@ -243,7 +243,7 @@ range-deletions-bytes-estimate: 0
 
 maybe-compact
 ----
-[JOB 100] compacted(default) L5 [000004] (775B) Score=6.15 + L6 [000006] (13KB) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
+[JOB 100] compacted(default) L5 [000004] (806B) Score=6.20 + L6 [000006] (13KB) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s
 
 # A table containing only range keys is not eligible for elision.
 # RANGEKEYDEL or RANGEKEYUNSET.
@@ -323,7 +323,7 @@ range-deletions-bytes-estimate: 94
 
 maybe-compact
 ----
-[JOB 100] compacted(elision-only) L6 [000004] (972B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (751B), in 1.0s (2.0s total), output rate 751B/s
+[JOB 100] compacted(elision-only) L6 [000004] (1003B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (751B), in 1.0s (2.0s total), output rate 751B/s
 
 # Close the DB, asserting that the reference counts balance.
 close
@@ -376,7 +376,7 @@ range-deletions-bytes-estimate: 8380
 maybe-compact
 ----
 [JOB 100] compacted(delete-only) L6 [000007] (13KB) Score=0.00 -> L6 [000000] (8.2KB), in 1.0s (2.0s total), output rate 8.2KB/s
-[JOB 101] compacted(default) L5 [000004] (767B) Score=3.04 + L6 [000006] (13KB) Score=0.00 -> L6 [000000] (4.8KB), in 1.0s (2.0s total), output rate 4.8KB/s
+[JOB 101] compacted(default) L5 [000004] (798B) Score=3.10 + L6 [000006] (13KB) Score=0.00 -> L6 [000000] (4.8KB), in 1.0s (2.0s total), output rate 4.8KB/s
 
 # The same LSM as above. However, this time, with point tombstone weighting at
 # 2x, the table with the point tombstone (000004) will be selected as the
@@ -422,7 +422,7 @@ range-deletions-bytes-estimate: 8380
 maybe-compact
 ----
 [JOB 100] compacted(delete-only) L6 [000007] (13KB) Score=0.00 -> L6 [000000] (8.2KB), in 1.0s (2.0s total), output rate 8.2KB/s
-[JOB 101] compacted(default) L5 [000004] (767B) Score=3.04 + L6 [000006] (13KB) Score=0.00 -> L6 [000000] (4.8KB), in 1.0s (2.0s total), output rate 4.8KB/s
+[JOB 101] compacted(default) L5 [000004] (798B) Score=3.10 + L6 [000006] (13KB) Score=0.00 -> L6 [000000] (4.8KB), in 1.0s (2.0s total), output rate 4.8KB/s
 
 # These tests demonstrate the behavior of the tombstone density compaction feature
 # in Pebble. This feature identifies files with a high density of tombstones and
@@ -497,8 +497,8 @@ tombstone-dense-blocks-ratio: 0.9
 # should indicate "move" as the compaction type.
 maybe-compact
 ----
-[JOB 100] compacted(tombstone-density) L5 [000004] (778B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000000] (751B), in 1.0s (2.0s total), output rate 751B/s
-[JOB 101] compacted(move) L4 [000004] (778B) Score=0.00 + L5 [] (0B) Score=0.00 -> L5 [000000] (778B), in 1.0s (2.0s total), output rate 778B/s
+[JOB 100] compacted(tombstone-density) L5 [000004] (806B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000000] (751B), in 1.0s (2.0s total), output rate 751B/s
+[JOB 101] compacted(move) L4 [000004] (806B) Score=0.00 + L5 [] (0B) Score=0.00 -> L5 [000000] (806B), in 1.0s (2.0s total), output rate 806B/s
 
 # Verify the result - the file should now be in L5
 # The file should maintain its original content since it was just moved rather than recompacted
@@ -556,7 +556,7 @@ tombstone-dense-blocks-ratio: 0.9
 # because there are overlapping files in L5 that prevent the optimization
 maybe-compact
 ----
-[JOB 100] compacted(tombstone-density) L4 [000004] (778B) Score=0.00 + L5 [000005] (764B) Score=0.00 -> L5 [000007] (751B), in 1.0s (2.0s total), output rate 751B/s
+[JOB 100] compacted(tombstone-density) L4 [000004] (806B) Score=0.00 + L5 [000005] (769B) Score=0.00 -> L5 [000007] (751B), in 1.0s (2.0s total), output rate 751B/s
 
 # Verify the result - the file was recompacted with the overlapping L5 file
 # The output file should be different from the input files
diff --git a/testdata/event_listener b/testdata/event_listener
index 319f7eff..d10713a2 100644
--- a/testdata/event_listener
+++ b/testdata/event_listener
@@ -166,7 +166,7 @@ close: db/marker.manifest.000004.MANIFEST-000011
 remove: db/marker.manifest.000003.MANIFEST-000009
 sync: db
 [JOB 6] MANIFEST created 000011
-[JOB 6] compacted(default) L0 [000005 000008] (1.5KB) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000010] (750B), in 1.0s (3.0s total), output rate 750B/s
+[JOB 6] compacted(default) L0 [000005 000008] (1.5KB) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000010] (755B), in 1.0s (3.0s total), output rate 755B/s
 close: db/000005.sst
 close: db/000008.sst
 remove: db/MANIFEST-000006
@@ -211,10 +211,10 @@ remove: db/MANIFEST-000009
 ingest
 ----
 open: ext/0
-read-at(689, 61): ext/0
-read-at(639, 50): ext/0
-read-at(122, 517): ext/0
-read-at(122, 517): ext/0
+read-at(694, 61): ext/0
+read-at(644, 50): ext/0
+read-at(122, 522): ext/0
+read-at(122, 522): ext/0
 read-at(81, 41): ext/0
 read-at(0, 81): ext/0
 close: ext/0
@@ -232,21 +232,21 @@ sync: db
 remove: db/MANIFEST-000011
 [JOB 10] MANIFEST deleted 000011
 remove: ext/0
-[JOB 10] ingested L0:000015 (750B)
+[JOB 10] ingested L0:000015 (755B)
 
 metrics
 ----
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     2  1.5KB     0B       0 |    - 0.40 0.40 |   97B |     1   750B |     0     0B |     3  2.2KB |    0B |   2 23.1
+    0 |     2  1.5KB     0B       0 |    - 0.40 0.40 |   97B |     1   755B |     0     0B |     3  2.2KB |    0B |   2 23.1
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     1   750B     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     1   750B | 1.5KB |   1 0.50
-total |     3  2.2KB     0B       0 |    -    -    - |  847B |     1   750B |     0     0B |     4  3.7KB | 1.5KB |   3 4.53
+    6 |     1   755B     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     1   755B | 1.5KB |   1 0.51
+total |     3  2.2KB     0B       0 |    -    -    - |  852B |     1   755B |     0     0B |     4  3.8KB | 1.5KB |   3 4.52
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 48B  written: 97B (102% overhead)
 Flushes: 3
@@ -276,18 +276,18 @@ ingest-flushable
 ----
 sync-data: wal/000012.log
 open: ext/a
-read-at(689, 61): ext/a
-read-at(639, 50): ext/a
-read-at(122, 517): ext/a
-read-at(122, 517): ext/a
+read-at(694, 61): ext/a
+read-at(644, 50): ext/a
+read-at(122, 522): ext/a
+read-at(122, 522): ext/a
 read-at(81, 41): ext/a
 read-at(0, 81): ext/a
 close: ext/a
 open: ext/b
-read-at(689, 61): ext/b
-read-at(639, 50): ext/b
-read-at(122, 517): ext/b
-read-at(122, 517): ext/b
+read-at(694, 61): ext/b
+read-at(644, 50): ext/b
+read-at(122, 522): ext/b
+read-at(122, 522): ext/b
 read-at(81, 41): ext/b
 read-at(0, 81): ext/b
 close: ext/b
@@ -309,7 +309,7 @@ sync: wal
 [JOB 13] WAL created 000020
 remove: ext/a
 remove: ext/b
-[JOB 11] ingested as flushable 000017 (750B), 000018 (750B)
+[JOB 11] ingested as flushable 000017 (755B), 000018 (755B)
 sync-data: wal/000020.log
 close: wal/000020.log
 create: wal/000021.log
@@ -332,7 +332,7 @@ close: db/marker.manifest.000007.MANIFEST-000023
 remove: db/marker.manifest.000006.MANIFEST-000016
 sync: db
 [JOB 16] MANIFEST created 000023
-[JOB 16] flushed 2 ingested flushables L0:000017 (750B) + L6:000018 (750B) in 1.0s (2.0s total), output rate 1.5KB/s
+[JOB 16] flushed 2 ingested flushables L0:000017 (755B) + L6:000018 (755B) in 1.0s (2.0s total), output rate 1.5KB/s
 remove: db/MANIFEST-000014
 [JOB 16] MANIFEST deleted 000014
 [JOB 17] flushing 1 memtable (100B) to L0
@@ -350,8 +350,8 @@ level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | ta
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     1   750B |     0     0B |     1   750B | 1.5KB |   1 0.50
-total |     6  4.4KB     0B       0 |    -    -    - | 2.3KB |     3  2.2KB |     0     0B |     5  6.0KB | 1.5KB |   5 2.57
+    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     1   755B |     0     0B |     1   755B | 1.5KB |   1 0.51
+total |     6  4.4KB     0B       0 |    -    -    - | 2.3KB |     3  2.2KB |     0     0B |     5  6.0KB | 1.5KB |   5 2.56
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 82B  written: 132B (61% overhead)
 Flushes: 6
diff --git a/testdata/flushable_ingest b/testdata/flushable_ingest
index 5f77d763..c977e99c 100644
--- a/testdata/flushable_ingest
+++ b/testdata/flushable_ingest
@@ -27,9 +27,44 @@ set d 1
 blockFlush
 ----
 
+# One WAL, corresponding to the mutable memtable containing the set.
+ls
+----
+000002.log
+LOCK
+MANIFEST-000001
+OPTIONS-000003
+ext
+ext1
+ext2
+ext3
+marker.format-version.000011.024
+marker.manifest.000001.MANIFEST-000001
+
+# Ingest can complete despite the flush being blocked.
 ingest ext1 ext2 ext3
 ----
 
+lsm
+----
+
+# Two more WALs, one for the flushable ingest, and one for the mutable
+# memtable.
+ls
+----
+000002.log
+000004.sst
+000005.sst
+000006.sst
+000007.log
+000008.log
+LOCK
+MANIFEST-000001
+OPTIONS-000003
+ext
+marker.format-version.000011.024
+marker.manifest.000001.MANIFEST-000001
+
 allowFlush
 ----
 
@@ -45,9 +80,10 @@ a:1
 b:1
 d:1
 
-# We expect 1 WAL for an immutable memtable, 1 file for the ingested ssts,
-# one for the mutable memtable. We also expect 3 ssts corresponding to the
-# ingested files.
+# We expect 1 WAL for an immutable memtable, 1 WAL for the ingested ssts, one
+# for the mutable memtable. We also expect 3 ssts corresponding to the
+# ingested files. The assumption here is that this command runs before the
+# flush completes.
 ls
 ----
 000002.log
@@ -163,6 +199,20 @@ blockFlush
 ingest ext4
 ----
 
+ls
+----
+000002.log
+000004.sst
+000005.log
+000006.log
+LOCK
+MANIFEST-000001
+OPTIONS-000003
+ext
+ext5
+marker.format-version.000011.024
+marker.manifest.000001.MANIFEST-000001
+
 allowFlush
 ----
 
diff --git a/testdata/ingest_external b/testdata/ingest_external
index dee473dd..3ca680eb 100644
--- a/testdata/ingest_external
+++ b/testdata/ingest_external
@@ -330,8 +330,8 @@ gi: (foo, .)
 lsm verbose
 ----
 L6:
-  000004(000004):[gc#10,DELSIZED-gf#inf,RANGEDEL] seqnums:[10-10] points:[gc#10,DELSIZED-gf#inf,RANGEDEL] size:1356(1356)
-  000005(000005):[gg#11,DELSIZED-gj#inf,RANGEDEL] seqnums:[11-11] points:[gg#11,DELSIZED-gj#inf,RANGEDEL] size:937(937)
+  000004(000004):[gc#10,DELSIZED-gf#inf,RANGEDEL] seqnums:[10-10] points:[gc#10,DELSIZED-gf#inf,RANGEDEL] size:1389(1389)
+  000005(000005):[gg#11,DELSIZED-gj#inf,RANGEDEL] seqnums:[11-11] points:[gg#11,DELSIZED-gj#inf,RANGEDEL] size:940(940)
 
 download g h via-backing-file-download
 ----
@@ -341,8 +341,8 @@ ok
 lsm verbose
 ----
 L6:
-  000006(000006):[gc#10,DELSIZED-gf#inf,RANGEDEL] seqnums:[10-10] points:[gc#10,DELSIZED-gf#inf,RANGEDEL] size:1019(1019)
-  000007(000007):[gg#11,DELSIZED-gj#inf,RANGEDEL] seqnums:[11-11] points:[gg#11,DELSIZED-gj#inf,RANGEDEL] size:934(934)
+  000006(000006):[gc#10,DELSIZED-gf#inf,RANGEDEL] seqnums:[10-10] points:[gc#10,DELSIZED-gf#inf,RANGEDEL] size:1033(1033)
+  000007(000007):[gg#11,DELSIZED-gj#inf,RANGEDEL] seqnums:[11-11] points:[gg#11,DELSIZED-gj#inf,RANGEDEL] size:932(932)
 
 reopen
 ----
diff --git a/testdata/iter_histories/blob_references b/testdata/iter_histories/blob_references
index 6c246733..d5761e64 100644
--- a/testdata/iter_histories/blob_references
+++ b/testdata/iter_histories/blob_references
@@ -12,13 +12,11 @@ L6
   d@2.SET.2:v
 ----
 L5:
-  000004:[b@9#9,SET-d@9#9,SET] seqnums:[9-9] points:[b@9#9,SET-d@9#9,SET] size:932 blobrefs:[(B000921: 10); depth:1]
+  000004:[b@9#9,SET-d@9#9,SET] seqnums:[9-9] points:[b@9#9,SET-d@9#9,SET] size:964 blobrefs:[(B000921: 10); depth:1]
 L6:
-  000005:[b@2#2,SET-d@2#2,SET] seqnums:[2-2] points:[b@2#2,SET-d@2#2,SET] size:927 blobrefs:[(B000921: 6); depth:1]
+  000005:[b@2#2,SET-d@2#2,SET] seqnums:[2-2] points:[b@2#2,SET-d@2#2,SET] size:959 blobrefs:[(B000921: 6); depth:1]
 Blob files:
   B000921 physical:{000921 size:[106 (106B)] vals:[16 (16B)]}
-Blob files:
-  B000921: [000921] 106 physical bytes, 16 value bytes
 
 combined-iter
 first
@@ -74,15 +72,12 @@ L6
   f@2.SETWITHDEL.3:blob{fileNum=000039 value=grapes}
 ----
 L5:
-  000004:[b@9#9,SETWITHDEL-e@1#9,SETWITHDEL] seqnums:[9-9] points:[b@9#9,SETWITHDEL-e@1#9,SETWITHDEL] size:933 blobrefs:[(B000039: 14), (B000921: 20); depth:2]
+  000004:[b@9#9,SETWITHDEL-e@1#9,SETWITHDEL] seqnums:[9-9] points:[b@9#9,SETWITHDEL-e@1#9,SETWITHDEL] size:965 blobrefs:[(B000039: 14), (B000921: 20); depth:2]
 L6:
-  000005:[b@2#9,SETWITHDEL-f@2#3,SETWITHDEL] seqnums:[2-9] points:[b@2#9,SETWITHDEL-f@2#3,SETWITHDEL] size:941 blobrefs:[(B000039: 11), (B000921: 13); depth:2]
+  000005:[b@2#9,SETWITHDEL-f@2#3,SETWITHDEL] seqnums:[2-9] points:[b@2#9,SETWITHDEL-f@2#3,SETWITHDEL] size:973 blobrefs:[(B000039: 11), (B000921: 13); depth:2]
 Blob files:
   B000039 physical:{000039 size:[117 (117B)] vals:[25 (25B)]}
   B000921 physical:{000921 size:[125 (125B)] vals:[33 (33B)]}
-Blob files:
-  B000039: [000039] 117 physical bytes, 25 value bytes
-  B000921: [000921] 125 physical bytes, 33 value bytes
 
 # The iterator stats should indicate that only the first value from each file
 # should trigger a read of the blob file. Once loaded, subsequent reads of the
@@ -142,11 +137,9 @@ L6
   l.SETWITHDEL.2:blob{fileNum=000009 value=peach blockID=6 valueID=0}
 ----
 L6:
-  000004:[a#2,SETWITHDEL-l#2,SETWITHDEL] seqnums:[2-2] points:[a#2,SETWITHDEL-l#2,SETWITHDEL] size:1031 blobrefs:[(B000009: 96); depth:1]
+  000004:[a#2,SETWITHDEL-l#2,SETWITHDEL] seqnums:[2-2] points:[a#2,SETWITHDEL-l#2,SETWITHDEL] size:1063 blobrefs:[(B000009: 96); depth:1]
 Blob files:
   B000009 physical:{000009 size:[322 (322B)] vals:[96 (96B)]}
-Blob files:
-  B000009: [000009] 322 physical bytes, 96 value bytes
 
 combined-iter
 first
@@ -217,11 +210,9 @@ L6
   b.SETWITHDEL.2:blob{fileNum=000009 value=keylime}
 ----
 L6:
-  000004:[a#5,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[2-5] points:[b#2,SETWITHDEL-b#2,SETWITHDEL] ranges:[a#5,RANGEKEYSET-d#inf,RANGEKEYSET] size:1080 blobrefs:[(B000009: 7); depth:1]
+  000004:[a#5,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[2-5] points:[b#2,SETWITHDEL-b#2,SETWITHDEL] ranges:[a#5,RANGEKEYSET-d#inf,RANGEKEYSET] size:1111 blobrefs:[(B000009: 7); depth:1]
 Blob files:
   B000009 physical:{000009 size:[96 (96B)] vals:[7 (7B)]}
-Blob files:
-  B000009: [000009] 96 physical bytes, 7 value bytes
 
 combined-iter
 seek-lt c
diff --git a/testdata/large_keys b/testdata/large_keys
index 084be70a..d4930ac6 100644
--- a/testdata/large_keys
+++ b/testdata/large_keys
@@ -168,16 +168,16 @@ set a(p,1000000)rove
 flush verbose
 ----
 L0.0:
-  000005:[a(p,1000000)a(l,2)ing#11,SET-a(p,1000000)eal#12,SET] seqnums:[10-12] points:[a(p,1000000)a(l,2)ing#11,SET-a(p,1000000)eal#12,SET] size:423391
-  000006:[a(p,1000000)e(l,2)ate#13,SET-a(p,1000000)etizers#15,SET] seqnums:[13-15] points:[a(p,1000000)e(l,2)ate#13,SET-a(p,1000000)etizers#15,SET] size:423412
-  000007:[a(p,1000000)etizing#16,SET-a(p,1000000)lauding#18,SET] seqnums:[16-18] points:[a(p,1000000)etizing#16,SET-a(p,1000000)lauding#18,SET] size:423397
-  000008:[a(p,1000000)le#19,SET-a(p,1000000)les#20,SET] seqnums:[19-23] points:[a(p,1000000)le#19,SET-a(p,1000000)les#20,SET] size:423401
-  000009:[a(p,1000000)letini#21,SET-a(p,1000000)lication#24,SET] seqnums:[21-24] points:[a(p,1000000)letini#21,SET-a(p,1000000)lication#24,SET] size:423412
-  000010:[a(p,1000000)ly#25,SET-a(p,1000000)o(l,2)o#27,SET] seqnums:[25-27] points:[a(p,1000000)ly#25,SET-a(p,1000000)o(l,2)o#27,SET] size:423377
-  000011:[a(p,1000000)raisal#28,SET-a(p,1000000)raiser#30,SET] seqnums:[28-30] points:[a(p,1000000)raisal#28,SET-a(p,1000000)raiser#30,SET] size:423397
-  000012:[a(p,1000000)raisers#31,SET-a(p,1000000)rentice#33,SET] seqnums:[31-33] points:[a(p,1000000)raisers#31,SET-a(p,1000000)rentice#33,SET] size:423403
-  000013:[a(p,1000000)rentices#34,SET-a(p,1000000)roval#36,SET] seqnums:[34-36] points:[a(p,1000000)rentices#34,SET-a(p,1000000)roval#36,SET] size:423410
-  000014:[a(p,1000000)rovals#37,SET-a(p,1000000)rove#38,SET] seqnums:[37-38] points:[a(p,1000000)rovals#37,SET-a(p,1000000)rove#38,SET] size:282487
+  000005:[a(p,1000000)a(l,2)ing#11,SET-a(p,1000000)eal#12,SET] seqnums:[10-12] points:[a(p,1000000)a(l,2)ing#11,SET-a(p,1000000)eal#12,SET] size:423430
+  000006:[a(p,1000000)e(l,2)ate#13,SET-a(p,1000000)etizers#15,SET] seqnums:[13-15] points:[a(p,1000000)e(l,2)ate#13,SET-a(p,1000000)etizers#15,SET] size:423451
+  000007:[a(p,1000000)etizing#16,SET-a(p,1000000)lauding#18,SET] seqnums:[16-18] points:[a(p,1000000)etizing#16,SET-a(p,1000000)lauding#18,SET] size:423436
+  000008:[a(p,1000000)le#19,SET-a(p,1000000)les#20,SET] seqnums:[19-23] points:[a(p,1000000)le#19,SET-a(p,1000000)les#20,SET] size:423440
+  000009:[a(p,1000000)letini#21,SET-a(p,1000000)lication#24,SET] seqnums:[21-24] points:[a(p,1000000)letini#21,SET-a(p,1000000)lication#24,SET] size:423451
+  000010:[a(p,1000000)ly#25,SET-a(p,1000000)o(l,2)o#27,SET] seqnums:[25-27] points:[a(p,1000000)ly#25,SET-a(p,1000000)o(l,2)o#27,SET] size:423416
+  000011:[a(p,1000000)raisal#28,SET-a(p,1000000)raiser#30,SET] seqnums:[28-30] points:[a(p,1000000)raisal#28,SET-a(p,1000000)raiser#30,SET] size:423436
+  000012:[a(p,1000000)raisers#31,SET-a(p,1000000)rentice#33,SET] seqnums:[31-33] points:[a(p,1000000)raisers#31,SET-a(p,1000000)rentice#33,SET] size:423442
+  000013:[a(p,1000000)rentices#34,SET-a(p,1000000)roval#36,SET] seqnums:[34-36] points:[a(p,1000000)rentices#34,SET-a(p,1000000)roval#36,SET] size:423449
+  000014:[a(p,1000000)rovals#37,SET-a(p,1000000)rove#38,SET] seqnums:[37-38] points:[a(p,1000000)rovals#37,SET-a(p,1000000)rove#38,SET] size:282525
 
 layout filename=000006.sst
 ----
@@ -189,9 +189,9 @@ sstable
  ├── index  offset: 187971  length: 46971
  ├── index  offset: 234947  length: 46973
  ├── top-index  offset: 281925  length: 140828
- ├── properties  offset: 422758  length: 557
- ├── meta-index  offset: 423320  length: 34
- └── footer  offset: 423359  length: 53
+ ├── properties  offset: 422758  length: 596
+ ├── meta-index  offset: 423359  length: 34
+ └── footer  offset: 423398  length: 53
 
 properties file=000006
 raw.key.size
@@ -225,16 +225,16 @@ L0.1:
   000018:[a(p,1000000)le#42,RANGEDEL-a(p,1000000)rentice#inf,RANGEDEL] seqnums:[42-45] points:[a(p,1000000)le#42,RANGEDEL-a(p,1000000)rentice#inf,RANGEDEL] size:7000821
   000019:[a(p,1000000)rentices#46,RANGEDEL-a(p,1000000)rove#inf,RANGEDEL] seqnums:[46-47] points:[a(p,1000000)rentices#46,RANGEDEL-a(p,1000000)rove#inf,RANGEDEL] size:4000779
 L0.0:
-  000005:[a(p,1000000)a(l,2)ing#11,SET-a(p,1000000)eal#12,SET] seqnums:[10-12] points:[a(p,1000000)a(l,2)ing#11,SET-a(p,1000000)eal#12,SET] size:423391
-  000006:[a(p,1000000)e(l,2)ate#13,SET-a(p,1000000)etizers#15,SET] seqnums:[13-15] points:[a(p,1000000)e(l,2)ate#13,SET-a(p,1000000)etizers#15,SET] size:423412
-  000007:[a(p,1000000)etizing#16,SET-a(p,1000000)lauding#18,SET] seqnums:[16-18] points:[a(p,1000000)etizing#16,SET-a(p,1000000)lauding#18,SET] size:423397
-  000008:[a(p,1000000)le#19,SET-a(p,1000000)les#20,SET] seqnums:[19-23] points:[a(p,1000000)le#19,SET-a(p,1000000)les#20,SET] size:423401
-  000009:[a(p,1000000)letini#21,SET-a(p,1000000)lication#24,SET] seqnums:[21-24] points:[a(p,1000000)letini#21,SET-a(p,1000000)lication#24,SET] size:423412
-  000010:[a(p,1000000)ly#25,SET-a(p,1000000)o(l,2)o#27,SET] seqnums:[25-27] points:[a(p,1000000)ly#25,SET-a(p,1000000)o(l,2)o#27,SET] size:423377
-  000011:[a(p,1000000)raisal#28,SET-a(p,1000000)raiser#30,SET] seqnums:[28-30] points:[a(p,1000000)raisal#28,SET-a(p,1000000)raiser#30,SET] size:423397
-  000012:[a(p,1000000)raisers#31,SET-a(p,1000000)rentice#33,SET] seqnums:[31-33] points:[a(p,1000000)raisers#31,SET-a(p,1000000)rentice#33,SET] size:423403
-  000013:[a(p,1000000)rentices#34,SET-a(p,1000000)roval#36,SET] seqnums:[34-36] points:[a(p,1000000)rentices#34,SET-a(p,1000000)roval#36,SET] size:423410
-  000014:[a(p,1000000)rovals#37,SET-a(p,1000000)rove#38,SET] seqnums:[37-38] points:[a(p,1000000)rovals#37,SET-a(p,1000000)rove#38,SET] size:282487
+  000005:[a(p,1000000)a(l,2)ing#11,SET-a(p,1000000)eal#12,SET] seqnums:[10-12] points:[a(p,1000000)a(l,2)ing#11,SET-a(p,1000000)eal#12,SET] size:423430
+  000006:[a(p,1000000)e(l,2)ate#13,SET-a(p,1000000)etizers#15,SET] seqnums:[13-15] points:[a(p,1000000)e(l,2)ate#13,SET-a(p,1000000)etizers#15,SET] size:423451
+  000007:[a(p,1000000)etizing#16,SET-a(p,1000000)lauding#18,SET] seqnums:[16-18] points:[a(p,1000000)etizing#16,SET-a(p,1000000)lauding#18,SET] size:423436
+  000008:[a(p,1000000)le#19,SET-a(p,1000000)les#20,SET] seqnums:[19-23] points:[a(p,1000000)le#19,SET-a(p,1000000)les#20,SET] size:423440
+  000009:[a(p,1000000)letini#21,SET-a(p,1000000)lication#24,SET] seqnums:[21-24] points:[a(p,1000000)letini#21,SET-a(p,1000000)lication#24,SET] size:423451
+  000010:[a(p,1000000)ly#25,SET-a(p,1000000)o(l,2)o#27,SET] seqnums:[25-27] points:[a(p,1000000)ly#25,SET-a(p,1000000)o(l,2)o#27,SET] size:423416
+  000011:[a(p,1000000)raisal#28,SET-a(p,1000000)raiser#30,SET] seqnums:[28-30] points:[a(p,1000000)raisal#28,SET-a(p,1000000)raiser#30,SET] size:423436
+  000012:[a(p,1000000)raisers#31,SET-a(p,1000000)rentice#33,SET] seqnums:[31-33] points:[a(p,1000000)raisers#31,SET-a(p,1000000)rentice#33,SET] size:423442
+  000013:[a(p,1000000)rentices#34,SET-a(p,1000000)roval#36,SET] seqnums:[34-36] points:[a(p,1000000)rentices#34,SET-a(p,1000000)roval#36,SET] size:423449
+  000014:[a(p,1000000)rovals#37,SET-a(p,1000000)rove#38,SET] seqnums:[37-38] points:[a(p,1000000)rovals#37,SET-a(p,1000000)rove#38,SET] size:282525
 
 layout filename=000017.sst
 ----
diff --git a/testdata/metrics b/testdata/metrics
index a5e26160..304241f0 100644
--- a/testdata/metrics
+++ b/testdata/metrics
@@ -57,14 +57,14 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     1   755B     0B       0 |    - 0.25 0.25 |   28B |     0     0B |     0     0B |     1   755B |    0B |   1 27.0
+    0 |     1   756B     0B       0 |    - 0.25 0.25 |   28B |     0     0B |     0     0B |     1   756B |    0B |   1 27.0
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     6 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-total |     1   755B     0B       0 |    -    -    - |   28B |     0     0B |     0     0B |     1   783B |    0B |   1 28.0
+total |     1   756B     0B       0 |    -    -    - |   28B |     0     0B |     0     0B |     1   784B |    0B |   1 28.0
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 17B  written: 28B (65% overhead)
 Flushes: 1
@@ -74,7 +74,7 @@ MemTables: 1 (256KB)  zombie: 1 (256KB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 755B
+Local tables size: 756B
 Compression types: snappy: 1
 Table stats: all loaded
 Block cache: 2 entries (795B)  hit rate: 0.0%
@@ -92,7 +92,7 @@ Iter category stats:
 
 disk-usage
 ----
-3.6KB
+3.7KB
 
 batch
 set b 2
@@ -128,8 +128,8 @@ level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | ta
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     2  1.5KB | 1.5KB |   1 1.00
-total |     2  1.5KB     0B       0 |    -    -    - |   64B |     0     0B |     0     0B |     4  3.0KB | 1.5KB |   1 48.3
+    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     2  1.5KB | 1.5KB |   1 1.01
+total |     2  1.5KB     0B       0 |    -    -    - |   64B |     0     0B |     0     0B |     4  3.0KB | 1.5KB |   1 48.5
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 34B  written: 64B (88% overhead)
 Flushes: 2
@@ -158,7 +158,7 @@ Iter category stats:
 
 disk-usage
 ----
-6.0KB
+6.1KB
 
 # Closing iter a will release one of the zombie memtables.
 
@@ -176,8 +176,8 @@ level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | ta
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     2  1.5KB | 1.5KB |   1 1.00
-total |     2  1.5KB     0B       0 |    -    -    - |   64B |     0     0B |     0     0B |     4  3.0KB | 1.5KB |   1 48.3
+    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     2  1.5KB | 1.5KB |   1 1.01
+total |     2  1.5KB     0B       0 |    -    -    - |   64B |     0     0B |     0     0B |     4  3.0KB | 1.5KB |   1 48.5
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 34B  written: 64B (88% overhead)
 Flushes: 2
@@ -221,15 +221,15 @@ level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | ta
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     2  1.5KB | 1.5KB |   1 1.00
-total |     2  1.5KB     0B       0 |    -    -    - |   64B |     0     0B |     0     0B |     4  3.0KB | 1.5KB |   1 48.3
+    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     2  1.5KB | 1.5KB |   1 1.01
+total |     2  1.5KB     0B       0 |    -    -    - |   64B |     0     0B |     0     0B |     4  3.0KB | 1.5KB |   1 48.5
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 34B  written: 64B (88% overhead)
 Flushes: 2
 Compactions: 1  estimated debt: 0B  in progress: 0 (0B)  canceled: 0 (0B)  failed: 0  problem spans: 0
              default: 1  delete: 0  elision: 0  move: 0  read: 0  tombstone-density: 0  rewrite: 0  copy: 0  multi-level: 0
 MemTables: 1 (256KB)  zombie: 2 (512KB)
-Zombie tables: 1 (755B, local: 755B)
+Zombie tables: 1 (756B, local: 756B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
 Local tables size: 1.5KB
@@ -269,8 +269,8 @@ level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | ta
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     2  1.5KB | 1.5KB |   1 1.00
-total |     2  1.5KB     0B       0 |    -    -    - |   64B |     0     0B |     0     0B |     4  3.0KB | 1.5KB |   1 48.3
+    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     2  1.5KB | 1.5KB |   1 1.01
+total |     2  1.5KB     0B       0 |    -    -    - |   64B |     0     0B |     0     0B |     4  3.0KB | 1.5KB |   1 48.5
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 34B  written: 64B (88% overhead)
 Flushes: 2
@@ -350,24 +350,24 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     7  5.7KB     0B       0 |    - 0.25 0.25 |  165B |     0     0B |     0     0B |     9  7.1KB |    0B |   1 52.1
+    0 |     7  5.9KB     0B       0 |    - 0.25 0.25 |  165B |     0     0B |     0     0B |     9  7.4KB |    0B |   1 53.5
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     2  1.5KB | 1.5KB |   1 1.00
-total |     9  7.1KB     0B       0 |    -    -    - |  165B |     0     0B |     0     0B |    11  8.8KB | 1.5KB |   2 62.3
+    6 |     2  1.5KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     2  1.5KB | 1.5KB |   1 1.01
+total |     9  7.4KB     0B       0 |    -    -    - |  165B |     0     0B |     0     0B |    11  9.0KB | 1.5KB |   2 63.7
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 116B  written: 165B (42% overhead)
 Flushes: 3
-Compactions: 1  estimated debt: 7.8KB  in progress: 0 (0B)  canceled: 0 (0B)  failed: 0  problem spans: 0
+Compactions: 1  estimated debt: 8.0KB  in progress: 0 (0B)  canceled: 0 (0B)  failed: 0  problem spans: 0
              default: 1  delete: 0  elision: 0  move: 0  read: 0  tombstone-density: 0  rewrite: 0  copy: 0  multi-level: 0
 MemTables: 1 (256KB)  zombie: 1 (256KB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 7.1KB
+Local tables size: 7.4KB
 Compression types: snappy: 9
 Table stats: all loaded
 Block cache: 0 entries (0B)  hit rate: 33.3%
@@ -422,14 +422,14 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     0     0B     0B       0 |    -    0    0 |  165B |     0     0B |     0     0B |     9  7.1KB |    0B |   0 52.1
+    0 |     0     0B     0B       0 |    -    0    0 |  165B |     0     0B |     0     0B |     9  7.4KB |    0B |   0 53.5
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     9  7.1KB     0B       0 |    - 0.00 0.00 | 7.1KB |     0     0B |     0     0B |     9  7.1KB | 7.1KB |   1 1.00
-total |     9  7.1KB     0B       0 |    -    -    - |  165B |     0     0B |     0     0B |    18   14KB | 7.1KB |   1 97.4
+    6 |     9  7.4KB     0B       0 |    - 0.00 0.00 | 7.4KB |     0     0B |     0     0B |     9  7.4KB | 7.4KB |   1 1.00
+total |     9  7.4KB     0B       0 |    -    -    - |  165B |     0     0B |     0     0B |    18   15KB | 7.4KB |   1  100
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 116B  written: 165B (42% overhead)
 Flushes: 3
@@ -439,7 +439,7 @@ MemTables: 1 (256KB)  zombie: 1 (256KB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 7.1KB
+Local tables size: 7.4KB
 Compression types: snappy: 9
 Table stats: all loaded
 Block cache: 0 entries (0B)  hit rate: 10.0%
@@ -545,14 +545,14 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     6  4.4KB     0B       0 |    - 0.50 0.50 |  211B |     3  2.2KB |     0     0B |    12  9.4KB |    0B |   2 51.5
+    0 |     6  4.5KB     0B       0 |    - 0.50 0.50 |  211B |     3  2.2KB |     0     0B |    12  9.6KB |    0B |   2 52.5
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     9  7.1KB     0B       0 |    - 0.00 0.00 | 7.1KB |     0     0B |     0     0B |     9  7.1KB | 7.1KB |   1 1.00
-total |    15   12KB     0B       0 |    -    -    - | 2.4KB |     3  2.2KB |     0     0B |    21   19KB | 7.1KB |   3 8.31
+    6 |     9  7.4KB     0B       0 |    - 0.00 0.00 | 7.4KB |     0     0B |     0     0B |     9  7.4KB | 7.4KB |   1 1.00
+total |    15   12KB     0B       0 |    -    -    - | 2.4KB |     3  2.2KB |     0     0B |    21   19KB | 7.4KB |   3 8.45
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 176B  written: 211B (20% overhead)
 Flushes: 8
@@ -630,18 +630,18 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |    13  9.6KB     0B       0 |    - 0.50 0.50 |  277B |     3  2.2KB |     0     0B |    19   14KB |    0B |   2 58.3
+    0 |    13  9.6KB     0B       0 |    - 0.50 0.50 |  277B |     3  2.2KB |     0     0B |    19   15KB |    0B |   2 59.1
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     9  7.1KB     0B       0 |    - 0.00 0.00 | 7.1KB |     0     0B |     0     0B |     9  7.1KB | 7.1KB |   1 1.00
-total |    22   17KB     0B       0 |    -    -    - | 2.5KB |     3  2.2KB |     0     0B |    28   24KB | 7.1KB |   3 10.2
+    6 |     9  7.4KB     0B       0 |    - 0.00 0.00 | 7.4KB |     0     0B |     0     0B |     9  7.4KB | 7.4KB |   1 1.00
+total |    22   17KB     0B       0 |    -    -    - | 2.5KB |     3  2.2KB |     0     0B |    28   25KB | 7.4KB |   3 10.3
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 223B  written: 277B (24% overhead)
 Flushes: 9
-Compactions: 2  estimated debt: 17KB  in progress: 0 (0B)  canceled: 0 (0B)  failed: 0  problem spans: 0
+Compactions: 2  estimated debt: 18KB  in progress: 0 (0B)  canceled: 0 (0B)  failed: 0  problem spans: 0
              default: 2  delete: 0  elision: 0  move: 0  read: 0  tombstone-density: 0  rewrite: 0  copy: 0  multi-level: 0
 MemTables: 1 (1.0MB)  zombie: 1 (1.0MB)
 Zombie tables: 0 (0B, local: 0B)
@@ -727,14 +727,14 @@ metrics zero-cache-hits-misses
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |    11  8.1KB     0B       0 |    - 0.50 0.50 |  277B |     3  2.2KB |     0     0B |    19   14KB |    0B |   2 58.3
+    0 |    11  8.1KB     0B       0 |    - 0.50 0.50 |  277B |     3  2.2KB |     0     0B |    19   15KB |    0B |   2 59.1
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |    10  7.9KB     0B       0 |    - 0.00 0.00 | 7.1KB |     1   758B |     0     0B |     9  7.1KB | 7.1KB |   1 1.00
-total |    21   16KB     0B       0 |    -    -    - | 3.2KB |     4  3.0KB |     0     0B |    28   25KB | 7.1KB |   3 8.09
+    6 |    10  8.1KB     0B       0 |    - 0.00 0.00 | 7.4KB |     1   763B |     0     0B |     9  7.4KB | 7.4KB |   1 1.00
+total |    21   16KB     0B       0 |    -    -    - | 3.3KB |     4  3.0KB |     0     0B |    28   25KB | 7.4KB |   3 8.18
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 223B  written: 277B (24% overhead)
 Flushes: 9
@@ -868,14 +868,14 @@ metrics zero-cache-hits-misses
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     0     0B     0B       0 |    -    0    0 |  277B |     3  2.2KB |     0     0B |    19   14KB |    0B |   0 58.3
+    0 |     0     0B     0B       0 |    -    0    0 |  277B |     3  2.2KB |     0     0B |    19   15KB |    0B |   0 59.1
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |    18   14KB     0B       0 |    - 0.00 0.00 |  14KB |     2  1.5KB |     0     0B |    16   12KB |  14KB |   1 0.85
-total |    18   14KB     0B       0 |    -    -    - | 4.0KB |     5  3.7KB |     0     0B |    35   31KB |  14KB |   1 8.09
+    6 |    18   14KB     0B       0 |    - 0.00 0.00 |  15KB |     2  1.5KB |     0     0B |    16   13KB |  15KB |   1 0.85
+total |    18   14KB     0B       0 |    -    -    - | 4.0KB |     5  3.7KB |     0     0B |    35   31KB |  15KB |   1 8.18
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 223B  written: 277B (24% overhead)
 Flushes: 9
@@ -926,14 +926,14 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     3  2.2KB     0B       0 |    - 0.25 0.25 |   38B |     0     0B |     0     0B |     3  2.2KB |    0B |   1 59.6
+    0 |     3  2.2KB     0B       0 |    - 0.25 0.25 |   38B |     0     0B |     0     0B |     3  2.2KB |    0B |   1 59.7
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     6 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-total |     3  2.2KB     0B       0 |    -    -    - |   38B |     0     0B |     0     0B |     3  2.2KB |    0B |   1 60.6
+total |     3  2.2KB     0B       0 |    -    -    - |   38B |     0     0B |     0     0B |     3  2.3KB |    0B |   1 60.7
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 27B  written: 38B (41% overhead)
 Flushes: 1
@@ -970,14 +970,14 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     0     0B     0B       0 |    -    0    0 |   38B |     0     0B |     0     0B |     3  2.2KB |    0B |   0 59.6
+    0 |     0     0B     0B       0 |    -    0    0 |   38B |     0     0B |     0     0B |     3  2.2KB |    0B |   0 59.7
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     3  2.2KB     0B       0 |    - 0.00 0.00 | 2.2KB |     0     0B |     0     0B |     3  2.2KB | 2.2KB |   1 1.00
-total |     3  2.2KB     0B       0 |    -    -    - |   38B |     0     0B |     0     0B |     6  4.5KB | 2.2KB |   1  120
+    6 |     3  2.2KB     0B       0 |    - 0.00 0.00 | 2.2KB |     0     0B |     0     0B |     3  2.2KB | 2.2KB |   1 1.01
+total |     3  2.2KB     0B       0 |    -    -    - |   38B |     0     0B |     0     0B |     6  4.5KB | 2.2KB |   1  121
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 27B  written: 38B (41% overhead)
 Flushes: 1
@@ -1030,14 +1030,14 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     1   758B     0B       0 |    - 0.25 0.25 |   38B |     1   758B |     0     0B |     3  2.2KB |    0B |   1 59.6
+    0 |     1   763B     0B       0 |    - 0.25 0.25 |   38B |     1   763B |     0     0B |     3  2.2KB |    0B |   1 59.7
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     3  2.2KB     0B       0 |    - 0.00 0.00 | 2.2KB |     0     0B |     0     0B |     3  2.2KB | 2.2KB |   1 1.00
-total |     4  3.0KB     0B       0 |    -    -    - |  796B |     1   758B |     0     0B |     6  5.2KB | 2.2KB |   2 6.70
+    6 |     3  2.2KB     0B       0 |    - 0.00 0.00 | 2.2KB |     0     0B |     0     0B |     3  2.2KB | 2.2KB |   1 1.01
+total |     4  3.0KB     0B       0 |    -    -    - |  801B |     1   763B |     0     0B |     6  5.2KB | 2.2KB |   2 6.69
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 27B  written: 38B (41% overhead)
 Flushes: 1
@@ -1082,14 +1082,14 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     2  1.5KB     0B       0 |    - 0.50 0.50 |   74B |     1   758B |     0     0B |     4  2.9KB |    0B |   2 40.8
+    0 |     2  1.5KB     0B       0 |    - 0.50 0.50 |   74B |     1   763B |     0     0B |     4  3.0KB |    0B |   2 40.9
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     3  2.2KB     0B       0 |    - 0.00 0.00 | 2.2KB |     0     0B |     0     0B |     3  2.2KB | 2.2KB |   1 1.00
-total |     5  3.7KB     0B       0 |    -    -    - |  832B |     1   758B |     0     0B |     7  6.0KB | 2.2KB |   3 7.36
+    6 |     3  2.2KB     0B       0 |    - 0.00 0.00 | 2.2KB |     0     0B |     0     0B |     3  2.2KB | 2.2KB |   1 1.01
+total |     5  3.7KB     0B       0 |    -    -    - |  837B |     1   763B |     0     0B |     7  6.0KB | 2.2KB |   3 7.35
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 44B  written: 74B (68% overhead)
 Flushes: 2
@@ -1099,7 +1099,7 @@ MemTables: 1 (256KB)  zombie: 1 (256KB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 755B
+Local tables size: 756B
 Compression types: snappy: 5
 Table stats: all loaded
 Block cache: 2 entries (787B)  hit rate: 0.0%
@@ -1140,7 +1140,7 @@ MemTables: 1 (512KB)  zombie: 1 (256KB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 755B
+Local tables size: 756B
 Compression types: snappy: 5
 Table stats: all loaded
 Block cache: 0 entries (0B)  hit rate: 0.0%
@@ -1173,8 +1173,8 @@ level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | ta
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     3  2.2KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     1   758B | 2.2KB |   1 0.50
-total |     3  2.2KB     0B       0 |    -    -    - |    0B |     0     0B |     0     0B |     1   758B | 2.2KB |   1    0
+    6 |     3  2.2KB     0B       0 |    - 0.00 0.00 | 1.5KB |     0     0B |     0     0B |     1   763B | 2.2KB |   1 0.50
+total |     3  2.2KB     0B       0 |    -    -    - |    0B |     0     0B |     0     0B |     1   763B | 2.2KB |   1    0
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 0B  written: 0B (0% overhead)
 Flushes: 1
@@ -1184,7 +1184,7 @@ MemTables: 1 (512KB)  zombie: 1 (256KB)
 Zombie tables: 0 (0B, local: 0B)
 Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
-Local tables size: 758B
+Local tables size: 763B
 Compression types: snappy: 3
 Table stats: all loaded
 Block cache: 0 entries (0B)  hit rate: 0.0%
@@ -1249,14 +1249,14 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     2  1.5KB     0B       0 |    - 0.25 0.25 |  106B |     0     0B |     0     0B |     5  3.7KB |    0B |   1 35.9
+    0 |     2  1.5KB     0B       0 |    - 0.25 0.25 |  106B |     0     0B |     0     0B |     5  3.7KB |    0B |   1 36.2
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     3  2.2KB     0B       0 |    - 0.00 0.00 |    0B |     0     0B |     3  2.2KB |     0     0B |    0B |   1    0
-total |     5  3.7KB     0B       0 |    -    -    - |  106B |     0     0B |     3  2.2KB |     5  3.8KB |    0B |   2 36.9
+    6 |     3  2.3KB     0B       0 |    - 0.00 0.00 |    0B |     0     0B |     3  2.3KB |     0     0B |    0B |   1    0
+total |     5  3.7KB     0B       0 |    -    -    - |  106B |     0     0B |     3  2.3KB |     5  3.9KB |    0B |   2 37.2
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 57B  written: 106B (86% overhead)
 Flushes: 3
@@ -1291,14 +1291,14 @@ metrics
       |                             |                |       |   ingested   |     moved    |    written   |       |    amp
 level | tables  size val-bl vtables | score  ff  cff |   in  | tables  size | tables  size | tables  size |  read |   r   w
 ------+-----------------------------+----------------+-------+--------------+--------------+--------------+-------+---------
-    0 |     2  1.5KB     0B       0 |    - 0.25 0.25 |  106B |     0     0B |     0     0B |     5  3.7KB |    0B |   1 35.9
+    0 |     2  1.5KB     0B       0 |    - 0.25 0.25 |  106B |     0     0B |     0     0B |     5  3.7KB |    0B |   1 36.2
     1 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     2 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     3 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     4 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
     5 |     0     0B     0B       0 |    -    0    0 |    0B |     0     0B |     0     0B |     0     0B |    0B |   0    0
-    6 |     3  2.2KB     0B       0 |    - 0.00 0.00 |    0B |     0     0B |     3  2.2KB |     0     0B |    0B |   1    0
-total |     5  3.7KB     0B       0 |    -    -    - |  106B |     0     0B |     3  2.2KB |     5  3.8KB |    0B |   2 36.9
+    6 |     3  2.3KB     0B       0 |    - 0.00 0.00 |    0B |     0     0B |     3  2.3KB |     0     0B |    0B |   1    0
+total |     5  3.7KB     0B       0 |    -    -    - |  106B |     0     0B |     3  2.3KB |     5  3.9KB |    0B |   2 37.2
 ----------------------------------------------------------------------------------------------------------------------------
 WAL: 1 files (0B)  in: 57B  written: 106B (86% overhead)
 Flushes: 3
diff --git a/tool/testdata/find-val-sep-db/MANIFEST-000001 b/tool/testdata/find-val-sep-db/MANIFEST-000001
index 401b4efc..935e0924 100644
Binary files a/tool/testdata/find-val-sep-db/MANIFEST-000001 and b/tool/testdata/find-val-sep-db/MANIFEST-000001 differ
diff --git a/tool/testdata/find-val-sep-db/OPTIONS-000003 b/tool/testdata/find-val-sep-db/OPTIONS-000003
index 654b6531..14f9e62b 100644
--- a/tool/testdata/find-val-sep-db/OPTIONS-000003
+++ b/tool/testdata/find-val-sep-db/OPTIONS-000003
@@ -52,6 +52,7 @@
   minimum_size=3
   max_blob_reference_depth=10
   rewrite_minimum_age=0s
+  target_garbage_ratio=0.00
 
 [Level "0"]
   block_restart_interval=16
diff --git a/tool/testdata/manifest_dump b/tool/testdata/manifest_dump
index 3db4d95c..edf9d921 100644
--- a/tool/testdata/manifest_dump
+++ b/tool/testdata/manifest_dump
@@ -358,13 +358,13 @@ MANIFEST-000001
   log-num:       4
   next-file-num: 7
   last-seq-num:  13
-  add-table:     L0 000005:[aaa\x00#10,SET-ddd\x00#13,SET] seqnums:[10-13] points:[aaa\x00#10,SET-ddd\x00#13,SET] size:848 blobrefs:[(B000006: 18); depth:1] (2025-06-24T14:25:43Z)
+  add-table:     L0 000005:[aaa\x00#10,SET-ddd\x00#13,SET] seqnums:[10-13] points:[aaa\x00#10,SET-ddd\x00#13,SET] size:848 blobrefs:[(B000006: 18); depth:1] (2025-06-24T21:39:58Z)
   add-blob-file: B000006 physical:{000006 size:[109 (109B)] vals:[18 (18B)]}
 108/2
   log-num:       7
   next-file-num: 10
   last-seq-num:  15
-  add-table:     L0 000008:[eee\x00#14,SET-fff\x00#15,SET] seqnums:[14-15] points:[eee\x00#14,SET-fff\x00#15,SET] size:824 blobrefs:[(B000009: 13); depth:1] (2025-06-24T14:25:43Z)
+  add-table:     L0 000008:[eee\x00#14,SET-fff\x00#15,SET] seqnums:[14-15] points:[eee\x00#14,SET-fff\x00#15,SET] size:824 blobrefs:[(B000009: 13); depth:1] (2025-06-24T21:39:58Z)
   add-blob-file: B000009 physical:{000009 size:[103 (103B)] vals:[13 (13B)]}
 EOF
 L0.0: