Skip to main content
unbound mongodb at scale

Scatter-Gather: The Query Router's Last Resort

3 min read Chapter 52 of 72

Scatter-Gather Operations

When mongos receives a query that includes the shard key in the filter, it routes the query to the specific shard that owns the matching data. This is a targeted query. When the filter does not include the shard key, mongos must send the query to every shard, collect results, and merge them. This is a scatter-gather query.

Scatter-gather queries have two costs: latency (bounded by the slowest shard’s response) and resource consumption (every shard performs the query, even if most return zero results).

Scatter-gather vs targeted routing diagram. Targeted: mongos sends query to 1 shard, receives 1 response. Scatter-gather: mongos sends to all 4 shards, waits for all 4 responses, merges. Shows latency as max(shard responses) for scatter-gather vs single shard for targeted.

Identifying Scatter-Gather in explain()

// Targeted query: includes shard key
db.readings.explain("executionStats").find({
  sensorId: "sensor-00042",
  ts: { $gte: ISODate("2024-06-01"), $lt: ISODate("2024-06-02") }
})

// explain output includes:
// "queryPlanner": {
//   "winningPlan": {
//     "stage": "SINGLE_SHARD",        <- targeted
//     "shards": [{ "shardName": "shard-2", ... }]
//   }
// }

// Scatter-gather query: no shard key in filter
db.readings.explain("executionStats").find({
  temperature: { $gt: 100 }
})

// explain output includes:
// "queryPlanner": {
//   "winningPlan": {
//     "stage": "SHARD_MERGE",          <- scatter-gather
//     "shards": [
//       { "shardName": "shard-1", ... },
//       { "shardName": "shard-2", ... },
//       { "shardName": "shard-3", ... },
//       { "shardName": "shard-4", ... }
//     ]
//   }
// }

The SHARD_MERGE stage in the explain output is the indicator of a scatter-gather query. Every shard executes the query independently, and mongos merges the results.

The Cost Model

For a 4-shard cluster where each shard can answer a query in 5ms:

  • Targeted query: 5ms (1 shard) + 1ms (routing) = 6ms
  • Scatter-gather: max(5ms, 5ms, 5ms, 5ms) + 1ms (routing) + 2ms (merge) = 8ms

The difference seems small. On a 16-shard cluster:

  • Targeted query: 5ms + 1ms = 6ms
  • Scatter-gather: max(5ms across 16 shards, with variance) + 1ms + 5ms (merge 16 result sets) = 15ms

The merge cost grows linearly with shard count. Worse, the slowest shard determines the latency. If shard 7 is experiencing a GC pause and responds in 200ms, the scatter-gather query takes 200ms+ even though 15 shards responded in 5ms.

// Measure scatter-gather impact in the application
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class ScatterGatherBenchmark {

    // SLOW: Scatter-gather query (no shard key)
    @Benchmark
    public List<Document> scatterGather() {
        return collection.find(Filters.gt("temperature", 100))
            .limit(100)
            .into(new ArrayList<>());
    }

    // FAST: Targeted query (includes shard key)
    @Benchmark
    public List<Document> targeted() {
        return collection.find(
            Filters.and(
                Filters.eq("sensorId", "sensor-00042"),
                Filters.gt("temperature", 100)
            ))
            .limit(100)
            .into(new ArrayList<>());
    }
}