AdvancedSpark · 16 min · 3 challenges

The Architecture Recital

Renata is 40 minutes into an onsite loop at LinkedIn when the data platform interviewer puts a query on the whiteboard: a filtered events table joined to users, grouped by country. 'Walk me through what Spark does with this.' She has recited this before. Parser, analyzer, optimizer, physical plan, stages, tasks, all in about a minute, and she mentions adaptive execution at the end. The interviewer nods and asks the question her recital never prepared her for: 'Which of those decisions would you expect to be wrong on this query, and how would you find out?' Nothing in the answer she memorized ever goes wrong. This lesson covers the other half of the answer: where Spark's committed plan depends on guesses, which guesses each optimizer gets wrong, and a case where adaptive execution caused the slowdown.

What you will be able to do

State the static-plan limit in a sentence and name the 3 runtime facts a compile-time plan cannot see.
State the static-plan limit in a sentence and name the 3 runtime facts a compile-time plan cannot see.
Work the arithmetic that turns a missing or uniform statistic into the wrong join strategy.
Work the arithmetic that turns a missing or uniform statistic into the wrong join strategy.
Name the blind spot of the rule-based optimizer, the cost model and adaptive execution, each with the Spark UI evidence that confirms it.
Name the blind spot of the rule-based optimizer, the cost model and adaptive execution, each with the Spark UI evidence that confirms it.
Tell a specific story of adaptive execution making a job slower, with the change, the evidence, the mechanism and the cost of the fix.
Tell a specific story of adaptive execution making a job slower, with the change, the evidence, the mechanism and the cost of the fix.

Where the Model Breaks

Daily Life
Interviews
At this depth the interviewer takes for granted that you can trace a query from the driver to the executors. What they want to know is whether you understand that the trace describes decisions made once, on the driver, before any input has been read. Every choice in the physical plan, from the join strategy to the reducer count to which side becomes the hash table, is a bet placed on estimates. A recital scores at the points where those bets lose.

What the planner does read

The planner starts from real numbers about stored data. For a file-based table that has never been analyzed, it takes the total file size from the directory listing and uses it as the table's sizeInBytes. After ANALYZE TABLE, it reads the stored size and, with column statistics, the distinct count, min, max and null count of each column. Both are facts about the table at rest. Neither says how many rows survive a predicate in this particular query, how the join key is spread, or how large an intermediate result will be once it has been joined, filtered and aggregated.

3 facts the planner cannot know

Runtime factWhat the static plan assumesWhat actually happens
Selectivity of a filterWithout column statistics, a filter keeps its whole input size`event_type = 'purchase'` keeps 0.4% of a 2GB table, about 8MB
Distribution of the join keyHash partitioning spreads rows evenly across reducersA bot account owns 30% of rows, so a single reducer gets 30% of the work
Size of each shuffle's output`spark.sql.shuffle.partitions`, 200 by default, suits every shuffle200 reducers over 50MB gives 250KB tasks; over 2TB it gives 10GB tasks
Each wrong assumption has its own price. A missed selectivity costs a shuffle that a broadcast would have avoided. A skewed key costs a straggler that holds the whole stage open while every other core idles. A wrong partition count costs scheduler overhead from thousands of tiny tasks in one direction, or spill to disk from a few huge ones in the other.

Watching skew land on a single task

The program below joins 12 orders to 5 users with automatic broadcast turned off, so Spark plans a sort-merge join and shuffles both sides by user_id into 3 reducer partitions. Adaptive execution is turned off as well (spark.sql.adaptive.enabled set to false), because on data this small its coalescing would merge all 3 reducers into 1 and hide the static plan. User 1 owns 8 of the 12 orders. Follow those 8 rows through the shuffle.

spark.conf.set("spark.sql.adaptive.enabled", "false")
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1)
result = orders.join(users, "user_id").collect()
orders12 rows
order_id
user_id
amount
101
1
20
102
1
35
103
2
12
104
1
8
105
3
50
106
1
14
107
4
22
108
1
9
109
1
41
110
5
17
111
1
26
112
1
30
users5 rows
user_id
country
1
US
2
JP
3
DE
4
BR
5
IN
Preparing the animation

Every row for user 1 hashes to the same reducer, whichever reducer that turns out to be, because hash partitioning is a pure function of the key. That task handles at least 8 order rows while the other 2 share 4 at most. Nothing in the static plan reacts: the partitioner was fixed at plan time, and it has no way to split a key whose size it never measured. Scale the same shape to 30% of 4 billion rows over 200 reducers and the hot partition holds 1.2 billion rows against about 14 million in each of the others. With adaptive execution on, this join is the case its skew handling is built for: it cuts the oversized partition into several tasks and gives each one the matching users partition. Put the same hot key under groupBy("user_id") with collect_list, which partial aggregation can't shrink, and no rule splits it: one task receives 1.2 billion rows while the other 199 finish in seconds.

Where to look when you suspect the model broke

4 checks, in the order to run them
  • Stages tab, task summary: compare the max task duration with the median. A max 20 times the median points at skew, and adding executors will not move it.
  • Run df.explain(mode="cost") to print the size and row estimates the planner used, then compare them with each operator's number of output rows on the SQL tab.
  • SQL tab, exchange nodes: shuffle bytes written total shows what each shuffle actually moved, the figure the partition count should have been sized for.
  • Stages tab, spill columns: a nonzero Spill (Disk) on a reduce stage means its partitions outgrew execution memory.
Spark has 3 layers aimed at this gap, each acting at a different moment: rewrite rules that need no data, a cost model that reads stored statistics, and adaptive execution that re-plans between stages using measured output. Each closes part of the gap and leaves a blind spot of its own, and those blind spots are what the rest of this lesson takes apart.
TIP
When a stage is slow, read the max-to-median task duration before touching executor memory. Skew and a bad partition count look identical on the jobs page and opposite in the task summary: skew has a long tail, a bad count has uniformly slow or uniformly tiny tasks.

Frame the Static-Plan Limit

Daily Life
Interviews
The opening sentence of a strong recital names the limit before the interviewer has to dig for it: Catalyst produces a single physical plan from what it can estimate at compile time, and adaptive execution can revise that plan only at shuffle boundaries, using sizes measured after a stage finishes. Every later claim in the answer hangs off that frame, so it pays to rehearse it on a concrete query.

The query on the table

SELECT
u.country,
COUNT(*) AS purchases
FROM events AS e
JOIN users AS u
ON e.user_id = u.user_id
WHERE e.event_type = 'purchase'
GROUP BY u.country

Walk the estimate the way the planner does. With the cost-based optimizer off, which is the default (spark.sql.cbo.enabled is false), a filter's output is estimated at the size of its input. The planner sees 2GB of events joined to 400MB of users, neither below the 10MB broadcast threshold, and picks a sort-merge join. That plan shuffles both sides by user_id and then shuffles again by country for the aggregation: 3 exchanges and 4 stages.

What the planner estimates
InputEstimated size
events after filter2GB
users400MB
Join chosensort-merge
What the job measures
InputMeasured size
events after filter8MB
users400MB
Join that fitsbroadcast hash

Statistics move the guess, and the guess stays a guess

spark.sql("ANALYZE TABLE events COMPUTE STATISTICS FOR COLUMNS event_type, user_id")
spark.conf.set("spark.sql.cbo.enabled", "true")

With column statistics and no histogram, the cost model estimates an equality filter as 1 divided by the column's distinct count. Events has 25 event types, so the estimate is 4% of 2GB, roughly 80MB. That shrinks the error from about 250 times to 10 times, and 80MB is still 8 times over the 10MB broadcast threshold. Purchases are 0.4% of events because event types are far from uniform: page views make up most rows and purchases are rare. An estimate that assumes every value is equally common has no way to see that. Histograms (spark.sql.statistics.histogram.enabled, off by default) would, at the price of a heavier ANALYZE that must be rerun as the data shifts.

What adaptive execution changes, and when

Adaptive query execution, on by default since Spark 3.2, re-plans at each shuffle boundary from the sizes the finished map tasks report. Here, once the events map stage completes, AQE sees around 8MB of shuffle output, under spark.sql.adaptive.autoBroadcastJoinThreshold (which takes the 10MB static value when unset), and rewrites the sort-merge join into a broadcast hash join. Notice what it can't take back. The users map stage was submitted alongside the events stage and has usually already written its 400MB of shuffle files. AQE saves the sort and reads those files through a local shuffle reader instead of fetching them across the network. The exchange by country stays.

ExchangeStatic planAfter the adaptive re-plan
events by `user_id`8MB shuffled, then sorted8MB collected and broadcast
users by `user_id`400MB shuffled, fetched over the network, sorted400MB written, read locally, no sort
Partial counts by `country`Shuffled to the final aggregateUnchanged
Stages44, with a cheaper join stage

The frame, said out loud

✓A frame with no limit
  • Spark builds a logical plan, optimizes it and generates a physical plan.
  • Catalyst applies rules like predicate pushdown.
  • AQE then optimizes the plan at runtime.
✗A frame that names the limit
  • The driver commits to a physical plan from estimates before reading data.
  • Without column statistics a filter is assumed to keep its whole input, so a selective filter loses its broadcast.
  • AQE revises the plan only between stages, from shuffle sizes, never inside a running stage.
The frame, applied to this query
'Spark picks its physical plan on the driver from what it can estimate before reading data. On this query that estimate treats the purchase filter as keeping all 2GB, so it plans a sort-merge join and 3 shuffles. Adaptive execution can correct the join after the events stage reports about 8MB, but only at that boundary, after the users side has mostly been written.'

The 0.4% share and the 25 event types are this lesson's example. On your own tables, explain(mode="cost") prints the planner's estimates for the same comparison.

TIP
Give the limit a number. '2GB estimated, 8MB actual, so the static plan shuffled and sorted 400MB of users it could have streamed past a broadcast' lands harder than 'estimates can be off.' A number shows you have read a real plan.

Volunteer Each Blind Spot

Daily Life
Interviews
An interviewer probing a recital is searching for the edge of what you know, asking 'and when does that go wrong?' after each layer until the answers run out. Volunteering each layer's blind spot before that question arrives changes the conversation. It shows you have watched the layer fail on real data, and it moves the interviewer from testing recall to discussing a decision, which is the conversation they came to have.

Each layer and the thing it cannot see

LayerWhen it decidesBlind spotEvidence
Rule-based optimizerCompile time, no dataA Python UDF is opaque: no predicate pushdown through it, no row estimate after itA `BatchEvalPython` node above a scan whose `PushedFilters` list is empty
Cost modelCompile time, stored statisticsStale statistics, uniform values, independent predicates`explain(mode="cost")` row counts far from each operator's `number of output rows`
Adaptive executionBetween stages, from shuffle sizesNothing before the first exchange; no skew fix for aggregations; bytes stand in for work`AQEShuffleRead` partition count and sizes on the SQL tab

The cost model's 3 assumptions

//

Stale statistics

Statistics are a snapshot from the last ANALYZE TABLE. A dimension that has grown from 6MB to 900MB since then still reads as 6MB, so the planner broadcasts it. Spark falls back to file sizes for a table that was never analyzed, but once a size is stored the planner uses it, and automatic size updates (spark.sql.statistics.size.autoUpdate.enabled) are off by default. The direction of the error decides the damage: an overestimate costs a shuffle you didn't need, while an underestimate can broadcast a table that does not fit in memory.

//

Uniform values

Without histograms, equality on a column is estimated as 1 over its distinct count, the same rule that put purchases at 4% when they were 0.4%. The rule misses the other way for a common value: a filter on page views is also estimated at 4% when it keeps most of the table, and a plan sized for 80MB meets well over a gigabyte.
//

Independent predicates

For country = 'JP' AND language = 'ja', the cost model multiplies the 2 selectivities. If each matches 3% of 50 million users, the estimate is 0.09%, or 45,000 rows. The columns describe nearly the same fact, so the real answer sits close to 3%, about 1.5 million rows: a 33 times underestimate, in the direction that ends in a memory error.

Adaptive execution's own blind spots

What AQE measures, and what that leaves out
  • No demotion of a static broadcast. AQE promotes sort-merge joins to broadcast; it has no rule that turns a broadcast the static plan already chose back into a shuffle, so a broadcast picked from stale statistics runs as planned.
  • No skew fix for aggregations. Skew handling splits oversized partitions of a sort-merge or shuffled hash join. A groupBy on a hot key still sends that key to a single task. Partial aggregation helps only when the partial result is small, as with a count or sum; collect_list still ships every value.
  • Bytes stand in for work. Partition coalescing and the runtime broadcast decision both read serialized, compressed shuffle sizes. Data that compresses 10 to 1, or rows that a later UDF or explode expands, look cheap to AQE and cost far more to process.
  • The skew split has a price. Cutting a skewed partition into N pieces means the other side's matching partition is read N times. When both sides are skewed on the same partition, the pieces multiply.
  • No new shuffle allowed. If splitting a skewed partition would force an extra exchange later in the plan, AQE skips the fix unless spark.sql.adaptive.forceOptimizeSkewedJoin is set.

The skew rule fires when a partition passes 2 tests at once: larger than 5 times the median partition (spark.sql.adaptive.skewJoin.skewedPartitionFactor) and larger than 256MB (spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes). Take a join whose median reduce partition is 200MB and whose hot partition is 12GB. The rule cuts the hot side into pieces near the 64MB advisory size, up to about 192 of them, bounded by how many map tasks wrote into it. Each piece is joined against the whole matching partition from the other side, so a 300MB partition there is read around 192 times: roughly 57GB of shuffle reads to fix a 12GB hot spot. The trade usually wins because those reads run in parallel across the cluster, and it explains why a join stage's read total can dwarf what was written to it.

How to volunteer one

The form that works is a sentence per blind spot, carrying the mechanism, the symptom and the place you would confirm it. Keep to the layers the question touched. Reciting every weakness unprompted reads as a prepared speech, while naming the one that bites this query reads as a diagnosis.
A blind spot in 3 parts
'The cost model would estimate that JP-and-Japanese filter at 0.09% of users because it multiplies selectivities, but the columns are correlated, so it's closer to 3%. If that estimate decided a broadcast, I would expect memory errors on the join stage, and I would compare explain(mode="cost") with the output row counts on the SQL tab.'
The phrasing does the work of 3 follow-up questions. The mechanism answers 'why', the symptom answers 'what would you see', and the UI location answers 'how would you know'. An interviewer who gets all 3 unasked has nothing left to probe on that layer and moves to the next one, or to the fix.
✓Do
  • Pair each blind spot with the UI metric that would confirm it
  • Say which way the estimate errs and what that direction costs
  • Name the config that changes the behaviour, with its default
✗Don't
  • List every optimizer weakness before the question reaches that layer
  • Call a documented design limit a bug
  • Claim AQE fixes skew without saying which operators it covers

A Tour With No Failure Modes

Daily Life
Interviews
The most common answer to 'walk me through Spark's execution' at this depth is a polished tour: every phase named in order, every component described accurately, and no case where any of it produces a bad plan. Interviewers hear it often enough that it signals preparation more than experience. A correct tour scores as the baseline, and the follow-up questions are built to get past it.
text or DataFrame to unresolved plan
Parser
names resolved against the catalog
Analyzer
rule batches
Logical optimizer
join strategy
Physical planner
cost model
between query stages
Adaptive re-plan
whole-stage Java code
Codegen
one per partition
Tasks

The tour as usually given. A plan travels along every arrow; only the adaptive step sees runtime sizes, and only once a stage has written its shuffle files.

The same tour, with the failure modes left in

PhaseWhat the tour saysWhat it leaves out
AnalyzerResolves tables and columnsIts failures are loud: an unresolved column fails before a single task runs
Logical optimizerPushes filters down, prunes columnsStops at a Python UDF, so a filter on a UDF result reads every row group
Physical plannerChooses the join by sizeSizes come from stale statistics or file listings; a selective filter looks as big as its input
Adaptive re-planFixes the plan at runtimeMeasures compressed bytes, and only after a stage completes
CodegenFuses operators into one functionFalls back past `spark.sql.codegen.maxFields` (100) or when the generated method grows too large; a Python UDF breaks the fused pipeline
TasksRun in parallelA hot key turns the stage's tail serial

What the follow-ups are looking for

After a tour, the interviewer's next questions are predictable because they all point at the gap it skipped: 'What if the filter is very selective?', 'What if one customer has half the orders?', 'What does AQE actually measure?' Each is an invitation to describe a failure mode. A recital that already contains a couple of them frees the interviewer to go deeper, into how you chose a fix and what it cost, which is the part of this question they learn the most from.

Rewriting a paragraph of the tour

Take the sentence about adaptive execution, the one most tours end on, and rewrite it so it carries its own limit. The rewrite adds 1 sentence.
✓As toured
  • AQE re-optimizes the plan at runtime using real statistics.
  • It coalesces partitions, switches to broadcast joins and handles skew.
✗With its edge
  • AQE re-plans at shuffle boundaries from the sizes map tasks report.
  • Those sizes are compressed bytes, so a stage that expands rows can be coalesced to too few tasks.
  • Its skew handling covers joins, so a skewed groupBy still needs a salted key or a two-step aggregate.

A self-check after your recital

Did I name a decision the driver made before reading any data?
Did I name a decision the driver made before reading any data?
Did I say which estimate fed that decision and where the estimate came from?
Did I say which estimate fed that decision and where the estimate came from?
Did I give a case where that estimate is wrong, with a number attached?
Did I give a case where that estimate is wrong, with a number attached?
Did I name the Spark UI view that would show the miss?
Did I name the Spark UI view that would show the miss?
Did I stop before the interviewer had to interrupt?
Did I stop before the interviewer had to interrupt?
Scoring yourself against those 5 questions after a practice answer catches the tour pattern quickly, because a tour passes the last check and fails the first 4. Recording a 2-minute answer and reading it back as a transcript makes the gap visible: the transcript will be full of component names and short on numbers.

Scoring a sample tour

A 4-sentence tour
'Spark parses the query and the analyzer resolves the tables against the catalog. Catalyst pushes the purchase filter down and prunes unused columns. The planner picks a sort-merge join because both tables are large, and AQE optimizes it at runtime. Then whole-stage codegen compiles each stage and the executors run one task per partition.'
CheckResultWhy
A decision made before dataPass, barelyIt names the join choice, but not that the driver fixed it
The estimate behind itFail'Both tables are large' hides that the filtered side was assumed to be 2GB
A wrong case with a numberFailNothing says the filter keeps 8MB, so the lost broadcast never comes up
The UI viewFailNo mention of `explain(mode="cost")` or the exchange sizes on the SQL tab
Stopped in timePassAbout 25 seconds spoken

One added sentence repairs all 3 failing checks: 'The planner assumed the purchase filter kept all 2GB of events, the job measures about 8MB, and I would confirm it by comparing explain(mode="cost") with the output rows on the SQL tab.' The tour keeps its order and its length stays under a minute; it gains the evidence an interviewer would otherwise spend several questions extracting.

A tour with no failure modes still fits a 60-second opener. The miss is offering it as the whole answer when the question asked how the system behaves on a particular query.

A Time AQE Made It Worse

Daily Life
Interviews
Interviewers ask for a backfire story because a story is hard to fake: the details either connect or they do not. A credible one has 4 parts in order: what changed, what the Spark UI showed, the mechanism that explains it, and the fix together with what the fix cost. Here is one built on a mechanism you can reproduce, with numbers that hold together.

The job

A nightly job joins a day of order events to customers, groups by order_id, and runs a Python UDF that parses each order's JSON payload into 40 fields. The team had tuned spark.sql.shuffle.partitions to 400 for this job on a cluster with 200 executor cores, so the UDF stage ran 400 tasks in 2 waves of about 2.5 minutes each, roughly 5 minutes for the stage. Then the platform team moved the cluster to a Spark version with adaptive execution on by default, and set spark.sql.adaptive.coalescePartitions.parallelismFirst to false to cut the count of tiny tasks on the shared cluster. The next night the job missed its deadline.

What the Spark UI showed

19
tasks in the UDF stage, down from 400
53 min
UDF stage time, up from 5 minutes
181
cores idle while that stage ran

On the SQL tab, the exchange before the UDF had an AQEShuffleRead node above it reporting the coalesced partition count and roughly 1.2GB of shuffle data. The page showed the plan as AdaptiveSparkPlan isFinalPlan=true, and the initial plan beside it still read hashpartitioning(order_id, 400). The event timeline showed most executors empty for the length of the stage, and the task summary showed durations clustered tightly: no long tail, every task uniformly slow.

The mechanism

With parallelismFirst off, coalescing packs adjacent shuffle partitions together until each reaches spark.sql.adaptive.advisoryPartitionSizeInBytes, 64MB by default. The job wrote roughly 1.2GB of shuffle output, and dividing that by 64MB gives roughly 19 partitions. The tuned 400 had become only the starting count. The flaw is in what the 64MB measures: compressed shuffle bytes, the input to the stage. The expensive work in this stage was the UDF, whose cost follows row count and payload length, and repetitive JSON compresses very well. Each remaining task now carried about 21 times the rows of an old one, so a 2.5 minute task became a 53 minute task, while 181 of the 200 cores waited.

Fixes, and what each one costs

FixEffect on the UDF stageCost
Set `spark.sql.adaptive.coalescePartitions.enabled` to false for this jobBack to 400 tasks, about 5 minutesEvery other shuffle in the job loses coalescing, and its cheap stages return to 400 tiny tasks
Lower the advisory size to 8MB for this jobAbout 154 tasks in a single wave, roughly 6.5 minutesEvery shuffle in the job is sized finer, including the cheap ones
`repartition(400)` just before the UDF400 tasks, since AQE leaves an explicit partition count aloneAn extra shuffle of the aggregated rows and a further stage
parsed = (
orders.join(customers, "customer_id")
.groupBy("order_id")
.agg(F.first("payload").alias("payload"))
.repartition(400)
.withColumn("fields", parse_payload(F.col("payload")))
)

The team chose the repartition(400). It confined the fix to the single expensive step and kept coalescing for the cheap shuffles elsewhere in the job. The extra stage wrote less than the 1.2GB the aggregation had shuffled, a small price next to a 48 minute regression. They rejected turning coalescing off, because that fixes this stage and silently regresses every small shuffle in the job.

Other backfires with the same shape

BackfireMechanismWhat you would see
Runtime broadcast exhausts memoryThe conversion reads serialized shuffle size; the hash table built from those rows in memory can be several times largerDriver or executor memory errors around a broadcast exchange that the initial plan did not have
Skew split multiplies readsA skewed partition cut into N pieces re-reads the other side's matching partition N timesThe join stage's task count jumps and its shuffle read exceeds the shuffle written into it

Telling it

Open with the change: an upgrade, a config, a shift in the data. Backfires follow a change.
Open with the change: an upgrade, a config, a shift in the data. Backfires follow a change.
Give before and after numbers for the slow stage, in tasks and minutes.
Give before and after numbers for the slow stage, in tasks and minutes.
Name the UI evidence: the AQEShuffleRead partition count and idle executors on the timeline.
Name the UI evidence: the AQEShuffleRead partition count and idle executors on the timeline.
State the mechanism in a sentence: coalescing sizes tasks by compressed input bytes.
State the mechanism in a sentence: coalescing sizes tasks by compressed input bytes.
Name the fix you chose, the one you rejected, and why.
Name the fix you chose, the one you rejected, and why.
Close with what you now check when reviewing a job after an upgrade.
Close with what you now check when reviewing a job after an upgrade.
TIP
If you have no backfire of your own, reproduce this one: a JSON-parsing UDF after a groupBy, coalescing on with parallelismFirst off, then compare task counts in the stages tab. A story from a reproduction you ran is checkable, and saying it was a reproduction keeps it honest.
❯❯❯PUTTING IT ALL TOGETHER

> A food delivery marketplace runs an hourly Spark job that joins order events to restaurants, keeps orders flagged for refund review (under 1% of rows), aggregates refunds per restaurant, and calls a fraud-scoring Python UDF on each restaurant's order history. After a cluster upgrade, the job runs twice as long in some hours and faster in others.

The refund-review filter is the selectivity blind spot: estimated at full size, it leads the static plan to shuffle both sides where a broadcast would fit.
Restaurants grow every week, so stored statistics go stale and can turn the restaurants side into an oversized broadcast that AQE won't demote.
A national chain concentrates orders on a single key, which AQE's skew handling splits in the join and leaves alone in the per-restaurant aggregation.
The fraud UDF repeats the backfire: coalescing sizes that stage by compressed input and starves the expensive step of tasks.
The interview answer walks this one job: the plan, each blind spot where it touches the query, and the Spark UI view that confirms it.
KEY TAKEAWAYS
Spark commits to a physical plan on the driver from estimates; filter selectivity, key distribution and shuffle sizes are the 3 facts it cannot see.
Without column statistics a filter is estimated at its input size; with them, equality uses 1 over the distinct count and AND multiplies selectivities.
Underestimates are the dangerous direction: they produce broadcasts that exhaust memory, while overestimates cost a shuffle you didn't need.
Adaptive execution re-plans only at shuffle boundaries, from compressed bytes, and neither demotes a static broadcast nor fixes a skewed aggregation.
A credible backfire story names the change, the before and after numbers, the UI evidence, the mechanism and what the fix cost.

Spark picks its plan before reading data. Name where that bet loses, and tell the time the fix lost too.

Category
Spark
Difficulty
advanced
Duration
16 minutes
Challenges
3 hands-on challenges

Topics covered: Where the Model Breaks, Frame the Static-Plan Limit, Volunteer Each Blind Spot, A Tour With No Failure Modes, A Time AQE Made It Worse

Lesson Sections

  1. Where the Model Breaks

    At this depth the interviewer takes for granted that you can trace a query from the driver to the executors. What they want to know is whether you understand that the trace describes decisions made once, on the driver, before any input has been read. Every choice in the physical plan, from the join strategy to the reducer count to which side becomes the hash table, is a bet placed on estimates. A recital scores at the points where those bets lose. What the planner does read 3 facts the planner c

  2. Frame the Static-Plan Limit

    The opening sentence of a strong recital names the limit before the interviewer has to dig for it: Catalyst produces a single physical plan from what it can estimate at compile time, and adaptive execution can revise that plan only at shuffle boundaries, using sizes measured after a stage finishes. Every later claim in the answer hangs off that frame, so it pays to rehearse it on a concrete query. The query on the table Statistics move the guess, and the guess stays a guess What adaptive executi

  3. Volunteer Each Blind Spot (concepts: Catalyst Optimizer)

    An interviewer probing a recital is searching for the edge of what you know, asking 'and when does that go wrong?' after each layer until the answers run out. Volunteering each layer's blind spot before that question arrives changes the conversation. It shows you have watched the layer fail on real data, and it moves the interviewer from testing recall to discussing a decision, which is the conversation they came to have. Each layer and the thing it cannot see The cost model's 3 assumptions Stal

  4. A Tour With No Failure Modes

    The most common answer to 'walk me through Spark's execution' at this depth is a polished tour: every phase named in order, every component described accurately, and no case where any of it produces a bad plan. Interviewers hear it often enough that it signals preparation more than experience. A correct tour scores as the baseline, and the follow-up questions are built to get past it. The same tour, with the failure modes left in What the follow-ups are looking for After a tour, the interviewer'

  5. A Time AQE Made It Worse (concepts: Adaptive Query Execution, Configuration Tuning)

    Interviewers ask for a backfire story because a story is hard to fake: the details either connect or they do not. A credible one has 4 parts in order: what changed, what the Spark UI showed, the mechanism that explains it, and the fix together with what the fix cost. Here is one built on a mechanism you can reproduce, with numbers that hold together. The job What the Spark UI showed The mechanism Fixes, and what each one costs Other backfires with the same shape Telling it