docs/components/read_flow/09_merge_resolution.md
Files: db/merge_helper.h, db/merge_helper.cc, db/merge_context.h, db/db_iter.cc, db/memtable.cc, include/rocksdb/merge_operator.h
When a key has merge operands (written via DB::Merge()), the read path must collect operands across layers and apply the user's MergeOperator to produce a final result. Merge resolution happens in three contexts: point lookups, iteration, and compaction.
MergeContext (see MergeContext in db/merge_context.h) accumulates merge operands as the search progresses through layers:
| Aspect | Detail |
|---|---|
| Storage | std::vector<Slice> backed by operand_list_ |
| Collection order | Newest to oldest (following the search order) |
| Presentation order | Oldest to newest (reversed before passing to merge operator) |
| Pinning | Operands can be pinned via PinnedIteratorsManager to avoid copies |
PushOperand() appends each operand as found. GetOperands() reverses the list (if needed) before returning to the caller.
During Get(), merge operands are collected as the search moves through layers:
In MemTable (see SaveValue() in db/memtable.cc):
kTypeMerge is encountered, push the operand and set merge_in_progress = truekTypeValue) is found later in the same memtable, call TimedFullMerge() immediatelymerge_in_progress to the next layerAcross MemTableListVersion (immutable memtables):
In Version::Get() (SST files):
GetContext tracks merge state via kMerge statekMerge state, resolve with no base valueDBIter::MergeValuesNewToOld() in db/db_iter.cc resolves merge chains during forward iteration:
Step 1: Push the first merge operand (the one at the current iterator position)
Step 2: Advance the internal iterator via Next(). Since InternalKey ordering is user_key ASC, sequence DESC, subsequent entries for the same user key have older sequences.
Step 3: For each subsequent entry with the same user key:
kTypeMerge: Push another operand, continuekTypeValue / kTypeBlobIndex: Found base value, call MergeWithPlainBaseValue()kTypeDeletion: Stop -- merge without base valueStep 4: If no base value or deletion found (end of key history), call MergeHelper::TimedFullMerge() with no base value.
Key Invariant: Merge operands are collected newest to oldest, but presented to the merge operator in chronological order (oldest to newest).
Full Merge (base value + operands -> result):
MergeOperator::FullMergeV3() in MergeHelper::TimedFullMerge()kTypeValue or kTypeWideColumnEntity)Partial Merge (operands -> fewer operands):
MergeOperator::PartialMergeMulti() during compactionMergeOperator::AllowSingleOperand() returns true)Note: max_successive_merges (see ColumnFamilyOptions in include/rocksdb/advanced_options.h) is a separate write-path optimization that eagerly merges during memtable insertion. It does not control compaction-side partial merges.
During compaction, merge operands cannot be combined across snapshot boundaries (see MergeHelper::MergeUntil() in db/merge_helper.cc):
If a snapshot exists between two merge operands' sequence numbers, they must be preserved separately because:
The check uses SnapshotChecker::CheckInSnapshot() to determine if an operand's sequence number falls within the range protected by a snapshot.
Key Invariant: Cannot merge operands across snapshot boundaries. Individual operands visible to snapshots must be preserved.
Merge resolution cost grows with the number of operands:
max_successive_merges resolves merges at write time when the memtable already contains operands for the same keyNUMBER_MERGE_FAILURES and merge-related statistics to identify performance issues