
Engineering Series
This post is part of the Distributed Database Systems: A Senior Engineer’s Guide series.
How Distributed SQL Execution Engines Really Work
How distributed SQL execution actually works—query planning, data movement, joins, aggregations, and why your queries sometimes explode in cost.
Pillar Post: Query Shape & Execution Reality. This is a deep dive into how data moves across a cluster to satisfy a query. For how these systems fail, see Why HTAP Systems Fail Quietly.
Most engineers treat distributed SQL databases as "PostgreSQL, but faster and spread across many nodes." This mental model fails the moment you hit production and discover that a simple JOIN that ran in 100ms on your laptop now takes 30 seconds at scale.
The execution engine is why.
TL;DR
- Distributed execution has coordinators and workers—aggregators plan and merge, leaves execute
- Query planning is cheap; data movement is expensive—the network is your bottleneck
- Joins explode when data isn't co-located—mismatched shard keys = reshuffling gigabytes
- Aggregations fan-in—partial results on leaves, final merge on aggregator
- "It worked in staging" means nothing—execution behavior changes with data size and distribution
- Most "slow database" complaints are actually data movement problems
Understanding execution gives you predictive power over query performance.
The Mental Model: Coordinators vs Workers
In PostgreSQL, there's one process executing your query.
In a distributed system like SingleStore, your query runs across two layers:
Aggregators (Coordinators)
Role: Planning and merging
What they do:
- Parse SQL
- Build execution plan
- Dispatch subqueries to leaves
- Merge partial results
- Return final result to client
What they DON'T do:
- Store data (usually)
- Execute full table scans
- Do heavy computation (unless necessary)
Leaves (Workers)
Role: Data storage and local execution
What they do:
- Store data shards
- Execute local scans
- Perform local joins (if data is co-located)
- Compute partial aggregations
- Send results to aggregator
Key insight: Each leaf only sees its own shard. It has no idea what data is on other leaves.
Query Planning vs Query Execution
This is where most engineers get confused.
Planning (Cheap)
The aggregator:
- Parses your SQL
- Figures out which tables are involved
- Determines which shards have the data
- Decides how to split the work
- Generates a plan
Cost: Milliseconds
Network traffic: Zero
Execution (Expensive)
The real work:
- Aggregator sends plan to leaves
- Leaves scan their local data
- Leaves execute local operations (filters, joins, aggregations)
- Leaves send results back to aggregator
- Aggregator merges results
Cost: Seconds to minutes
Network traffic: Gigabytes (potentially)
Total time: ~60ms
Planning: 3ms
Execution: 57ms
The plan is not the problem. The data movement is.
Where Data Actually Moves (And Why That's Expensive)
In a single-node database, data moves from disk to CPU to RAM.
In a distributed database, data also moves across the network.
Three Types of Data Movement
1. Local scan (no network)
sqlSELECT * FROM orders WHERE order_date > '2026-01-01';
Each leaf scans its own shard. Results go to aggregator.
Network cost: Only the results (could be small)
2. Broadcast (aggregator → all leaves)
sqlSELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.user_id;
If users is small and not sharded, the aggregator broadcasts the entire table to all leaves.
Network cost: size(users) × number_of_leaves
3. Reshuffle (leaf → leaf)
sqlSELECT o.*, p.name
FROM orders o
JOIN products p ON o.product_id = p.product_id;
If orders is sharded by order_id and products is sharded by product_id, data must be reshuffled.
Network cost: size(orders) + size(products) (potentially)
The Hidden Cost
Moving 1GB across a network:
- 10 Gbps network: ~1 second
- 1 Gbps network: ~10 seconds
But you're not just moving data once. You're:
- Serializing it
- Compressing it
- Sending it
- Decompressing it
- Deserializing it
Real time: 2-5x the naive calculation.
This is why queries that work fine in staging (small data, single node) explode in production.
Joins in a Distributed System
Co-Located Join (Fast)
Both tables are sharded on the join key:
sql-- orders sharded by user_id
-- user_activity sharded by user_id
SELECT o.order_id, a.activity
FROM orders o
JOIN user_activity a ON o.user_id = a.user_id;
Network cost: Minimal (just results)
Execution: Parallel on each leaf
This is the ideal case.
Reshuffle Join (Slow)
Tables are sharded on different keys:
sql-- orders sharded by order_id
-- products sharded by product_id
SELECT o.order_id, p.name
FROM orders o
JOIN products p ON o.product_id = p.product_id;
What happens:
- Each leaf scans its local orders
- Data is reshuffled across the network by
product_id - Each leaf now has orders + products for the same product IDs
- Join happens locally
- Results go to aggregator
Network cost: Entire dataset moves across the network
Time: 10-100x slower than co-located join
This is the failure mode.
Broadcast Join (Medium)
One table is small:
sql-- orders is large, sharded
-- order_status is tiny (10 rows)
SELECT o.*, s.description
FROM orders o
JOIN order_status s ON o.status_id = s.status_id;
What happens:
- Aggregator broadcasts
order_status(tiny) to all leaves - Each leaf joins locally
Network cost: size(small_table) × number_of_leaves
Time: Fast (if the small table is actually small)
Aggregations and Fan-In
Aggregations in distributed systems use partial → final pattern.
sqlSELECT user_id, COUNT(*), SUM(amount)
FROM orders
GROUP BY user_id;
Execution:
On each leaf:
sqlSELECT user_id, COUNT(*) as cnt, SUM(amount) as total
FROM orders
WHERE <local_shard>
GROUP BY user_id;
On aggregator:
sql-- Merge partial results
SELECT user_id, SUM(cnt), SUM(total)
FROM <partial_results>
GROUP BY user_id;
Why This Matters
Aggregations that work: SUM, COUNT, AVG, MIN, MAX
Aggregations that don't work well: DISTINCT, MEDIAN, PERCENTILE
Why?
COUNT DISTINCT can't be easily combined from partial results. The aggregator needs to see all unique values.
Example:
sqlSELECT COUNT(DISTINCT user_id) FROM orders;
What happens:
- Each leaf sends all unique
user_idvalues to aggregator - Aggregator deduplicates
Network cost: Potentially gigabytes (all unique IDs)
Why "It Worked in Staging" Means Nothing
Your staging environment:
- Data: 1GB
- Nodes: 1 leaf
- Query: Fast
Production:
- Data: 1TB
- Nodes: 10 leaves
- Query: Timeout
What changed?
1. Data Movement Scales Non-Linearly
Staging:
- No reshuffling (single node)
- No network overhead
- All data in memory
Production:
- Reshuffling 100GB across network
- Network becomes bottleneck
- Data doesn't fit in memory
2. Query Plans Change
The query planner chooses different strategies based on:
- Table sizes
- Data distribution
- Available memory
Staging plan: Broadcast join
Production plan: Reshuffle join (because the "small" table is now 50GB)
3. Sharding Matters
Staging: Single shard, no coordination
Production: 10 shards, coordination overhead, potential skew
The Diagram That Makes Everything Click
This is the execution model you need in your head:
Key insight: Steps 3b-3c (reshuffle) are optional—but when they happen, they dominate execution time.
Your goal: Design schemas and queries that avoid reshuffling.
Failure Modes Engineers Misattribute to "Slow Databases"
Failure Mode 1: "The Database Is Slow"
Symptom: Query takes 30 seconds
Actual cause: Reshuffle join moving 50GB across network
Fix: Shard tables on the join key
Failure Mode 2: "High CPU on Database"
Symptom: Database CPU at 90%
Actual cause: Aggregator is merging 10M rows from leaves
Fix: Add LIMIT clause, or filter earlier
Failure Mode 3: "Queries Work Locally But Not in Production"
Symptom: Local query: 100ms. Production: timeout.
Actual cause: Query plan changed because table sizes are different
Fix: Test with production-scale data
Failure Mode 4: "Adding Indexes Didn't Help"
Symptom: Added index, query still slow
Actual cause: Query is network-bound, not CPU-bound
Fix: Optimize data movement, not index structure
Failure Mode 5: "RAM Upgrade Didn't Help"
Symptom: Doubled RAM, no improvement
Actual cause: Query is bottlenecked on network, not memory
Fix: Reduce data movement
Sideways Failure: One Query, Cluster-Wide Impact
The execution engine has shared resources:
What happens:
- One expensive query starts reshuffling 100GB
- Network saturates
- Other queries queue, waiting for network bandwidth
- Application sees "slow database"
- Alert fires
- Engineer investigates the symptom (slow queries)
- Root cause is the expensive query that started it all
The pattern: In distributed systems, symptoms appear far from causes.
What I'd Do Differently Next Time
1. Test Queries at Production Scale
Don't trust staging. Use production data size, even if it's in a dev environment.
Tooling:
EXPLAINon production (read-only)- Synthetic data generators
- Cloned production database (sanitized)
2. Choose Shard Keys Based on Join Patterns
Shard key is the most important schema decision.
Ask:
- What are the most common joins?
- Can I co-locate data for those joins?
- What's the cardinality of the shard key?
Bad: Shard on order_id when you frequently join with user_id
Good: Shard on user_id if most queries filter or join on users
3. Monitor Data Movement, Not Just CPU
Track:
- Network bytes sent/received per query
- Reshuffle frequency
- Broadcast sizes
Alert when:
- A query moves >10GB
- Network bandwidth exceeds 70%
- Reshuffle operations spike
4. Use EXPLAIN Before Every Deploy
sqlEXPLAIN FORMAT=JSON
SELECT o.*, p.name
FROM orders o
JOIN products p ON o.product_id = p.product_id;
Look for:
"exchange"(data movement between nodes)"broadcast"(full table sent to all nodes)"repartition"(data reshuffled)
If you see any of these, understand the cost.
5. Add Query Limits by Default
sql-- Bad: unbounded
SELECT * FROM orders WHERE user_id = 123;
-- Good: explicit limit
SELECT * FROM orders WHERE user_id = 123 LIMIT 1000;
Limits prevent accidental full-table scans.
6. Separate OLTP and OLAP Workloads
Don't mix:
- Transactional queries (point lookups)
- Analytical queries (full scans, aggregations)
Why: They have different execution patterns and resource needs.
Solution: Replicate data to separate clusters.
Final Takeaway
Understanding the execution engine gives you predictive power.
You can look at a query and know:
- Will it reshape data?
- How much network traffic will it generate?
- Why it might work locally but fail in production?
The execution engine isn't magic.
It's coordinators dispatching work, workers executing locally, and network moving data.
Optimize for that, and your queries will scale.
Further Reading
- Lessons Learned Running SingleStore in Production
- SingleStore vs PostgreSQL: When Distributed SQL Actually Wins
- How SingleStore Handles Real-Time Analytics at Scale
Have execution engine questions? Email me or connect on LinkedIn.

About the Author
Shahid Moosa is a Cloud Database Support Engineer specializing in distributed systems, AWS, and SingleStore. He helps teams build scalable, reliable data infrastructure.
Get in touch →Share this briefing
Related Posts
Why HTAP Systems Fail Quietly (And How to Notice Early)
HTAP systems don't fail with a bang; they fail sideways through memory contention and silent performance drift. Learn why mixed workloads are a silent production risk.
Debugging Slow Database Queries: A Systematic Approach
Learn how to identify and fix slow database queries using query plans, indexes, and optimization techniques from real production scenarios.
Distributed SQL Deep Dive: A 5-Part Series
A guided journey through the architecture, performance, and operational reality of HTAP and distributed SQL systems.