SBE $lookup: reuse a sorted vector instead of a per-row std::set for hash-join match indices

XMLWordPrintableJSON

    • Type: Improvement
    • Resolution: Fixed
    • Priority: Major - P3
    • 9.1.0-rc0
    • 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%).

            Assignee:
            Jan Steemann
            Reporter:
            Jan Steemann
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: