• Type: Bug
    • Resolution: Unresolved
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • None
    • Query Execution
    • ALL
    • QE 2026-09-28, QE 2026-10-12, QE 2026-10-26
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      $group: {_id: null, all: {$push: "$$ROOT"}} running in SBE reports no inUseTrackedMemBytes / peakTrackedMemBytes in $currentOp, in the slow-query log, or in explain, while the same pipeline keyed on a field does, and the classic engine reports it for both. Both memory-tracking feature flags are enabled. Seen on 9.1 master (647e3623) during TUNE-1432 sys-perf runs, where mongotune's query memory load shedding could not see the memory of the single-bucket $group operations that were filling the heap.

      Repro (9.1 master, default internalQueryFrameworkControl: trySbeRestricted)

      // seed 20000 docs {i, g: i % 100, pad: 2 KB string} into qmls.big
      const c = db.getSiblingDB("qmls").big;
      c.explain("executionStats").aggregate([{$match: {i: {$lt: 5000}}}, {$group: {_id: null, all: {$push: "$$ROOT"}}}]);
      // -> slotBasedPlan, peakTrackedMemBytes: 0
      c.explain("executionStats").aggregate([{$match: {i: {$lt: 5000}}}, {$group: {_id: "$g", all: {$push: "$$ROOT"}}}]);
      // -> slotBasedPlan, peakTrackedMemBytes: 137214
      db.adminCommand({getParameter: 1, featureFlagQueryMemoryTracking: 1, featureFlagExpressionMemoryTracking: 1});
      // -> both value: true, currentlyEnabled: true
      

      With internalQueryFrameworkControl: forceClassicEngine the _id: null pipeline reports about 26 MB of inUseTrackedMemBytes in $currentOp while running.

      Cause

      HashAggStage creates a publishing tracker (createChunkedSimpleMemoryUsageTrackerForSBE, src/mongo/db/exec/sbe/stages/hash_agg.cpp:169), but the only write to it is _memoryTracker.value().set(_ht->size() * estimatedRowSize) inside checkMemoryUsageAndSpillIfNecessary (src/mongo/db/exec/sbe/stages/hashagg_base.cpp:249-254). That call sits under if (groupByListHasSlots) (hash_agg.cpp:278-289, flag defined at hash_agg.cpp:239 as !_inKeyAccessors.empty()), added so a single-bucket group never spills. The stage builder folds a single constant _id into zero group-by slots (src/mongo/db/query/stage_builder/sbe/gen_group.cpp:1740-1745), so the guard is false, the tracker stays at 0 for the life of the query, and CurOp omits both fields (curop.cpp appends them only when non-zero). BlockHashAggStage has no such guard (block_hashagg.cpp:707-717). jstests/noPassthrough/memory_tracking/group_reports_memory_metrics.js only uses a non-constant key, so the gap is not covered.

      Memory reporting appears to be coupled to the spill decision by accident; splitting the tracker update out of the spill guard (or calling it unconditionally as the block stage does) would fix it.

      Impact

      Query memory load shedding, server-side (queryMemoryLoadShedding*) and mongotune's, ranks and sheds by in-use tracked bytes. A $group that accumulates everything into one bucket is the shape most likely to fill the heap, and it is invisible to both.

            Assignee:
            Unassigned
            Reporter:
            Zixuan Zhuang
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: