-
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.