-
Type:
Improvement
-
Resolution: Fixed
-
Priority:
Major - P3
-
Affects Version/s: None
-
Component/s: Query Execution
-
None
-
Query Execution
-
Fully Compatible
-
QE 2026-08-03
-
None
-
None
-
None
-
None
-
None
-
None
-
None
Overview
In the SBE hash-join lowering of $lookup, LookupHashTableIter collects the matched inner-document buffer indices for each outer document (array-key case) into a std::set. Because std::set is node-based, this allocates and frees tree nodes on every outer row. Replacing it with a reused std::vector that is sorted and de-duplicated (and whose capacity is retained across rows) removes this per-row allocation churn.
Background
Investigating the Aggregation.Lookup.LocalArrayOfObject microbenchmark (aggregation_read_commands, "Standalone inMemory ARM") with an on-CPU flamegraph of the SBE path (trySbeRestricted lowers this $lookup to EQ_LOOKUP / HashJoin) showed ~27-33% of total CPU in tcmalloc allocation/free. One contributor is LookupHashTableIter::_hashTableMatchSet (std::set): initSearchArray() inserts matched indices into it and reset()->clear() frees the nodes, once per outer document.
Proposed change
In {{src/mongo/db/exec/sbe/stages/lookup_hash_table.
{h,cpp}}}:
* Drop _hashTableMatchSet (and its iterator) and route the array-key path through the already-present _hashTableMatchVector.
* initSearchArray() appends all matches into the vector, then std::sort + std::unique/erase to produce the same sorted, de-duplicated result the std::set provided.
* clear() keeps _hashTableMatchVector's capacity across outer rows (no per-row reallocation).
* getAllMatchingIndices() and getNextMatchingIndex() return/iterate the vector for both scalar and array outer keys (both the HashLookupStage batch consumer and the HashLookupUnwindStage streaming consumer handle a vector).
Output ordering and de-duplication semantics are unchanged (sorted unique indices).
Measured impact
Local prototype, ARM, inMemory, trySbeRestricted, 5 trials x 20s, Aggregation.Lookup.LocalArrayOfObject:
* baseline: ops_per_sec = 123.0
* with change (B): ops_per_sec = 126.2 -- +2.6%
Correctness verified against a harness exercising interior/boundary docs, a duplicate-local-key doc (matched once), and a no-match doc: PASS.
Files
* src/mongo/db/exec/sbe/stages/lookup_hash_table.h
* src/mongo/db/exec/sbe/stages/lookup_hash_table.cpp
Notes / follow-up
* Prototype patch attached (changeB_reuse_match_vector.patch).
* The RecordIndexCollection variant still nominally includes std::set_; it is now only ever a std::vector_ and could be simplified in a follow-up.
* Independent of, and additive with, the companion ticket (skipping the outer-key arrayToSet). Combined prototype measured ops_per_sec = 131.7 (+7.1%).
- is related to
-
SERVER-132094 SBE $lookup: skip per-row arrayToSet on the outer (probe) key in the hash-join path
-
- Closed
-