Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Engineering  Module 9 – Batch processing with Apache Spark

Spark joins, shuffles and partitions

What a Spark shuffle moves, how the planner picks a broadcast, sort-merge or shuffled hash join, and where every partition count in a job comes from.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and pyspark 4.2.0
  • By MySmartCoPilot
Free core, Pro depth: 7 of 25 minutes free
Free: What a shuffle moves, and why it sets the cost · Three ways to join two tables, and who chooses · Where each partition count comes from · Practise. With a pass: 3 in-depth parts and 3 interview answers, about 18 minutes.

What you will learn

  • Explain what a shuffle moves between tasks and why it dominates a job's cost
  • Distinguish broadcast, sort-merge and shuffled hash joins by what each one moves
  • Read a plan's Exchange and join operators to name the strategy Spark chose
  • Work out how many partitions a shuffle declares and how many tasks really run
  • Choose a number of shuffle partitions for a stage from the volume it moves

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain what a shuffle moves between tasks and why it dominates a job's cost
  • Distinguish broadcast, sort-merge and shuffled hash joins by what each one moves
  • Read a plan's Exchange and join operators to name the strategy Spark chose
  • Work out how many partitions a shuffle declares and how many tasks really run
  • Choose a number of shuffle partitions for a stage from the volume it moves

Before you start

On this page

When Spark joins 1,200,000 delivery events to a table of 8 city zones, it can move all 1,200,000 rows across the network, or none of them, and return the same rows either way. One comparison against a size threshold decides which, and the plan records the answer in an operator name. The job’s cost also turns on a number few people set on purpose: how many partitions each of its shuffles declares.

The numbers below come from four recorded PySpark 4.2.0 runs in local mode that report counts, not durations: rows, partitions, stages and plan operators. By the end you can read a plan and say what moves, why that strategy was chosen and how many tasks a stage runs.

What a shuffle moves, and why it sets the cost

The first recorded run asks three questions of the same events and prints what each plan holds. Keeping the events whose minutes are 40 or more returns 266,666 rows of 1,200,000 and plans no Exchange operator: The operator in a Spark physical plan that stands for one shuffle: rows leave the partitions they were computed in and are re-distributed, usually by a hash of the key.. Counting the events per zone returns 8 rows and plans one. The join to the zones table returns all 1,200,000 rows and plans none, with a BroadcastExchange beside it.

Three questions, three plans, on 1,200,000 delivery events

join_plan_session.py

"""Session driver: run join_plan.py on PySpark 4.2.0 in local mode, inside a private lab folder.

Spark needs a Java runtime and the pyspark package. The driver points at the pinned ones (Java 21 and PySpark
4.2.0), gives Spark a home and a temporary folder inside the lab, binds it to 127.0.0.1 and prints only what the
program prints: configuration values, row counts and operator names, never Spark's own log lines or paths.
"""

import os
import pwd
import subprocess
import sys
import tempfile
from pathlib import Path

# Where mise installs the pinned Java 21. The home folder comes from the account database, so the path holds even
# when HOME points at a scratch folder.
REAL_HOME = pwd.getpwuid(os.getuid()).pw_dir
JAVA_HOME = os.path.join(REAL_HOME, ".local", "share", "mise", "installs", "java", "temurin-21.0.12+101.0.LTS")
HERE = Path(__file__).resolve().parent
PROGRAM = "join_plan.py"


def pinned_pyspark():
    """The folder that holds the pinned pyspark 4.2.0 package, or None to use the one installed for this Python."""
    for folder in [Path.cwd(), *Path.cwd().parents, *HERE.parents]:
        for found in folder.glob("*/toolchains/pyspark-4.2.0/python"):
            return str(found)
    return None


with tempfile.TemporaryDirectory(prefix="jsp-plan-") as lab:
    for name in ("home", "tmp", "out"):
        os.makedirs(os.path.join(lab, name))
    java_home = JAVA_HOME if os.path.isdir(JAVA_HOME) else os.environ.get("JAVA_HOME", "")
    env = {
        "PATH": os.pathsep.join([os.path.join(java_home, "bin"), "/usr/bin", "/bin"]),
        "JAVA_HOME": java_home,
        "HOME": os.path.join(lab, "home"),
        "TMPDIR": os.path.join(lab, "tmp"),
        "WORK": os.path.join(lab, "out"),
        "SPARK_LOCAL_IP": "127.0.0.1",
        "PYSPARK_PYTHON": sys.executable,
        "TZ": "UTC",
        "LANG": "en_US.UTF-8",
    }
    package = pinned_pyspark()
    if package:
        env["PYTHONPATH"] = package
    done = subprocess.run([sys.executable, str(HERE / PROGRAM)], cwd=lab, env=env,
                          capture_output=True, text=True, check=False)
    print(done.stdout.replace(lab, "<lab>").rstrip())
    if done.returncode != 0:
        last = [line for line in done.stderr.splitlines() if line.strip()][-1:] or ["no message"]
        print(PROGRAM, "stopped with exit status", done.returncode, "-", last[0].replace(lab, "<lab>"))
        sys.exit(done.returncode)

Output

PySpark 4.2.0, three settings this session left alone:
  spark.sql.shuffle.partitions = 200
  spark.sql.adaptive.enabled = true
  spark.sql.autoBroadcastJoinThreshold = 10485760b

events rows = 1200000, partitions = 2
zones rows = 8, partitions = 2

filter and select: every row stays where it is
  rows out = 266666
  Exchange = 0, BroadcastExchange = 0, Sort = 0
  operators: Project | Filter | Range

count per zone: equal zones must meet
  rows out = 8
  Exchange = 1, BroadcastExchange = 0, Sort = 0
  operators: HashAggregate | Exchange hashpartitioning ENSURE_REQUIREMENTS | HashAggregate | Project | Range

join to the 8 zones: the planner decides how
  rows out = 1200000
  Exchange = 0, BroadcastExchange = 1, Sort = 0
  operators: Project | BroadcastHashJoin BuildRight Inner | Project | Filter | Range | BroadcastExchange | Project | Range

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 join_plan_session.py

join_plan.py

"""PySpark 4.2.0, local mode: which steps shuffle, and which join the planner chose on its own.

Three questions over the same 1,200,000 delivery events of a food-delivery company: a filter that keeps every row
where it already sits, a count per zone that must gather equal zones together, and a join to the 8-row zones table.
The program prints the three settings it did not set, so they are Spark's own defaults, then the operators of each
plan with its shuffle, broadcast and sort counts. Plans are printed through plan_ops.py, which keeps operator names
only: a raw plan carries estimated sizes and ids that may differ between runs.
"""

from pyspark.sql import SparkSession

from plan_ops import operators, shape

spark = (SparkSession.builder
         # learn-lint-allow inclusive: PySpark 4.2.0 names the builder method that sets the cluster URL master()
         .master("local[2]")
         .appName("joins-shuffles-defaults")
         .config("spark.ui.enabled", "false")
         .config("spark.driver.bindAddress", "127.0.0.1")
         .config("spark.driver.host", "127.0.0.1")
         .config("spark.driver.memory", "1g")
         .getOrCreate())
spark.sparkContext.setLogLevel("ERROR")

print(f"PySpark {spark.version}, three settings this session left alone:")
for key in ("spark.sql.shuffle.partitions", "spark.sql.adaptive.enabled", "spark.sql.autoBroadcastJoinThreshold"):
    print(f"  {key} = {spark.conf.get(key)}")

events = (spark.range(0, 1200000)
          .selectExpr("id as event_id",
                      "cast(pmod(hash(id), 8) as int) as zone_id",
                      "cast(12 + pmod(hash(id, 7), 36) as int) as minutes"))
zones = (spark.range(0, 8)
         .selectExpr("cast(id as int) as zone_id", "concat('zone-', cast(id as string)) as zone_name"))

print(f"\nevents rows = {events.count()}, partitions = {events.rdd.getNumPartitions()}")
print(f"zones rows = {zones.count()}, partitions = {zones.rdd.getNumPartitions()}")


def report(label, df, rows_out):
    print(f"\n{label}")
    print(f"  rows out = {rows_out}")
    print(f"  {shape(df)}")
    print(f"  operators: {' | '.join(operators(df))}")


slow_deliveries = events.filter("minutes >= 40").select("event_id", "zone_id")
report("filter and select: every row stays where it is", slow_deliveries, slow_deliveries.count())

per_zone = events.groupBy("zone_id").count()
report("count per zone: equal zones must meet", per_zone, per_zone.count())

with_names = events.join(zones, "zone_id").select("event_id", "zone_name")
report("join to the 8 zones: the planner decides how", with_names, with_names.count())

spark.stop()

plan_ops.py

"""Read a Spark physical plan as a list of operator names, with no sizes, ids or paths in it.

The plan string Spark prints carries estimated sizes, expression ids and plan ids, all of which may differ between
runs and between machines, so nothing here prints a plan as it stands. operators() keeps the operator names this
lesson talks about, each with the few keywords on its line that say how it works, and counts() counts them.
"""

import re

WATCHED = (
    "AQEShuffleRead",
    "BroadcastExchange",
    "BroadcastHashJoin",
    "Exchange",
    "Filter",
    "HashAggregate",
    "Project",
    "Range",
    "ShuffledHashJoin",
    "Sort",
    "SortMergeJoin",
)

KEYWORDS = (
    "BuildLeft",
    "BuildRight",
    "Inner",
    "coalesced",
    "hashpartitioning",
    "local",
    "SinglePartition",
    "RoundRobinPartitioning",
    "ENSURE_REQUIREMENTS",
    "REPARTITION_BY_NUM",
)


def plan_text(df):
    """The physical plan Spark executed, or will execute, for this DataFrame.

    An adaptive plan prints its final plan and then the initial plan it started from; only the first half describes
    the operators that ran, so the second half is cut off here and never counted twice.
    """
    printed = df._jdf.queryExecution().executedPlan().toString()
    return printed.split("== Initial Plan ==")[0]


def operators(df):
    """The watched operators of the plan, root first, each followed by the keywords its line holds."""
    found = []
    for line in plan_text(df).splitlines():
        bare = re.sub(r"^[\s+:*()\d-]*", "", line)
        head = re.match(r"[A-Za-z]+", bare)
        if head is None or head.group(0) not in WATCHED:
            continue
        words = [word for word in KEYWORDS if re.search(r"\b" + word + r"\b", line)]
        found.append(" ".join([head.group(0), *words]))
    return found


def counts(df):
    """How many of each watched operator the plan holds: a dict of operator name to count."""
    tally = {}
    for entry in operators(df):
        name = entry.split(" ")[0]
        tally[name] = tally.get(name, 0) + 1
    return tally


def shape(df):
    """One line for a plan: the shuffles, the broadcasts and the sorts it holds."""
    tally = counts(df)
    return (f"Exchange = {tally.get('Exchange', 0)}, "
            f"BroadcastExchange = {tally.get('BroadcastExchange', 0)}, "
            f"Sort = {tally.get('Sort', 0)}")

The first two differ in the one way that decides everything else. A filter decides each row where it already sits, so the task holding it finishes the work. A count per zone cannot: both tasks held rows of all 8 zones, and a count needs every row of one zone in one place. A shuffle: Spark re-distributing rows so that equal keys end up in one partition: each task of a stage writes its rows into one bucket per target partition, and the next stage's tasks fetch the buckets they need. puts them there, and Spark’s guide names its cost as disk writes, serialisation and network transfers:

Interactive · stage by stage What one shuffle does to a count per zone Follow the rows of one count per zone through a shuffle, and watch where they stop being local.
What one shuffle does to a count per zone, step 6 of 6. Showing Read the events, Write by key (buckets written to disk), Fetch the buckets (stage 1 ends, stage 2 starts), Count per zone. In focus: Count per zone.Read the events2 partitions, 2 tasksWrite by keybuckets written to diskFetch the bucketsstage 1 ends, stage 2 startsCount per zone8 rows out

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Two tasks, no sharing. The read gives 1,200,000 events in 2 partitions, one task each. A filter or a projection would finish here: every row is already where it needs to be, and nothing crosses between the two tasks.
  2. Equal zones must meet. A count per zone cannot finish locally, because both tasks hold rows of all 8 zones. Each task partly counts what it holds, then writes its rows into one bucket per target partition.
  3. The buckets land on local disk. Spark's own guide describes the map side: results are held in memory while they fit, then sorted by their target partition and written to a file, which the reading tasks later read as sorted blocks.
  4. Readers pull from every writer. Each of the 8 reducing tasks asks every writing task for its bucket, so a shuffle of W writers and R readers opens W times R transfers. This fetch is the step that costs the network and the disk.
  5. The shuffle starts a new stage. Spark cuts the job in two at the shuffle: the writing tasks are one stage, the reading tasks the next, and the second cannot start until the first has written every bucket.
  6. Eight rows out of 1,200,000. Each reading task now holds every row of its zones and finishes the count, which returns 8 rows. Almost all the work went into moving rows that the answer threw away.

Key idea

Count the plain Exchange operators in a plan and you have counted the times your data crosses the cluster; everything else in the plan runs on rows where they already are.

Checkpoint

Check: What a shuffle moves

  1. Question 1 of 2 In the recorded run, the filter and projection planned 0 Exchange operators while the count per zone planned 1. Which of these steps need rows to move between partitions? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • Ranking the events of each zone by minute
    • Counting the events of each zone

    Steps that look at one row at a time stay inside their task. Steps that must compare or combine rows of the same key need a shuffle, and the plan shows one Exchange for each.

  2. Question 2 of 2 A job reads 1,200,000 events in 2 partitions and counts them per zone. Why can the counting tasks not start before every reading task has finished writing?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Because a counting task needs every bucket of its zones, and a reading task that has not finished may still write one

    A shuffle cuts a job into stages, and the reading stage consumes what the writing stage produced, so one slow writing task holds up every reader.

Three ways to join two tables, and who chooses

Spark joins two tables by bringing equal keys together, but only two of its three strategies move the big side. A broadcast join: A join that sends the whole small side to every executor and probes it in memory against the big side's partitions, so no shuffle on the join key is needed. sends one whole side to every executor and probes it in memory, so the other side stays put. A sort-merge join: A join that shuffles both sides on the join key, sorts each partition, and merges them pairwise. The default for two large tables that will not fit a broadcast. shuffles both sides on the key, sorts each partition and merges them. A shuffled hash join: A join that shuffles both sides on the join key and then builds a hash table from one side's partition, instead of sorting both sides and merging them. shuffles both sides too, then builds a hash table from one side’s partition.

The planner chooses by size. The run printed the broadcast join threshold: The size up to which Spark sends a whole table to every executor rather than shuffling both sides of a join, set by spark.sql.autoBroadcastJoinThreshold; the value -1 switches broadcasting off., spark.sql.autoBroadcastJoinThreshold, as 10485760 bytes, the documented 10 MB default; 8 rows sit far below it, so the plan is a BroadcastHashJoin with no shuffle of the events. The threshold at -1 switches broadcasting off whatever the size, and a BROADCAST hint wins even above it.

Strategy What moves What it needs
Broadcast hash join one copy of the small side, to every executor the small side below the threshold, and room for it
Sort-merge join every row of both sides, once nothing but the shuffle; it sorts and spills if it must
Shuffled hash join every row of both sides, once one task’s memory to hold its partition of a side

Two settings and one measurement decide the rest. spark.sql.shuffle.partitions, 200 by default, sets how many shuffle partition: One of the partitions a shuffle writes its rows into, and one task of the stage that reads them. spark.sql.shuffle.partitions sets how many partitions a shuffle makes. each shuffle declares. adaptive query execution: A Spark feature, on by default, that re-plans parts of a query at runtime from the actual rows and bytes it has seen, under an AdaptiveSparkPlan root. may then join small ones together, a step called partition coalescing: Adaptive query execution joining neighbouring shuffle partitions into fewer, larger ones after it has measured a stage's output, so the next stage runs fewer and bigger tasks. with a target size of its own. Row counts per partition say where the work sits.

Checkpoint

Check: The three join strategies

  1. Question 1 of 2 The recorded join of 1,200,000 events to the 8-row zones table planned a BroadcastHashJoin with 1 BroadcastExchange and 0 Exchange operators. What did Spark compare to decide that?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The estimated size of the smaller side against spark.sql.autoBroadcastJoinThreshold

    The planner broadcasts a side whose estimated size is below the threshold, which is 10485760 bytes by default, and the plan records the decision as a BroadcastExchange under the join.

  2. Question 2 of 2 A team sets spark.sql.autoBroadcastJoinThreshold to -1 for a job that joins the same two tables. What should they expect the plan to do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Shuffle both sides on the join key, because broadcasting is switched off

    The threshold is the only thing that made the broadcast possible, so -1 leaves the shuffling strategies, which move every row of both sides once.

Where each partition count comes from

Three rules set the three counts in a job, and mixing them up is why a tuning change often does nothing. A read’s partitions come from the source and the read settings: Spark packs files into partitions of up to spark.sql.files.maxPartitionBytes, 128 MB by default, charging every file an open cost of spark.sql.files.openCostInBytes, 4 MB, so small files share a task, and a generated range splits by the session’s cores, which is why every run here began with 2. A shuffle’s partitions come from spark.sql.shuffle.partitions, not from the stage before it. Files come from neither: a task writes one file in every partition folder it holds rows for, so a write can leave up to tasks times distinct partition values, a ceiling partitioning, clustering and file sizes measures.

Interactive · step by step The three places a partition count is set Step through one job and see which setting decides the task count at each of its three stages.
The three places a partition count is set, step 5 of 5. Showing The source, Read partitions (2 tasks here), Shuffle partitions (fewer if coalesced), Files written (up to tasks x values). In focus: Files written.The sourceRead partitions2 tasks hereShuffle partitionsfewer if coalescedFiles writtenup to tasks x values

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The source sets the first count. How many partitions a read produces comes from the source and the read settings: Spark packs a table's files into partitions of up to 128 MB, charging every file a 4 MB open cost, and splits a generated range by the session's cores.
  2. One task per partition. Each read partition becomes one task of the first stage. The lesson's runs read a generated range on two cores, so every one of them started with 2 partitions and 2 tasks.
  3. The shuffle count is a setting. The next count is not inherited. Every shuffle splits its output into as many partitions as spark.sql.shuffle.partitions says, whatever the stage before it had, and each one becomes a task.
  4. Adaptive execution may lower it. With adaptive query execution on, Spark can join small shuffle partitions together after measuring the stage, so the number of tasks that actually run is often well below the setting.
  5. The write multiplies the count. The last stage's tasks each write their own file in every partition folder they hold rows for, so the files on disk can reach tasks times distinct partition values.

Four things can still slow a stage whose counts are right, and Spark performance: AQE, skew and spills measures them: data skew, one key holding far more rows than the rest; salting, which spreads such a key over several artificial ones; a When a task's sort buffer or hash table outgrows the execution memory it can get, Spark writes part of it to the executor's local disk and reads it back later., a task writing its sort or hash table to disk; and caching a result that several actions reuse.

Checkpoint

Check: Where a partition count comes from

  1. Question 1 of 2 The recorded runs read their events as 2 partitions, and the session's spark.sql.shuffle.partitions was 200. How many partitions does the stage after a grouping declare?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 200, because a shuffle takes its partition count from the setting, not from the stage before it

    Read counts come from the files' sizes and the read settings, shuffle counts from spark.sql.shuffle.partitions, and file counts from the last stage's tasks and the partition values each one holds. Three places, three different rules.

  2. Question 2 of 2 A job's last stage runs 64 tasks and writes a table partitioned by a month column with 12 distinct values. Every task holds rows of every month. How many files does the write leave?

    Type a number.

    A hint

    Each task writes its own file in every partition folder it holds rows for.

    Show the answer: Question 2 of 2

    Answer: 768 files

    64 tasks x 12 month values is 768 files. That is the ceiling, reached here because every task holds rows of every month; tasks that hold fewer months write fewer files.

Practise

The self-check covers the three sections above.

Practice that adapts to you

Practise this lesson

8 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 What does a shuffle do, in one sentence?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: It re-distributes rows between partitions so that rows with the same key end up together

    A shuffle is the re-distribution step: each task writes its rows into buckets by target partition and the next stage's tasks fetch the buckets they need.

  2. Question 2 of 8 You read a plan and count 3 Exchange operators and 1 BroadcastExchange. How many times does the data get re-distributed across the cluster?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: Three times; the BroadcastExchange sends a copy of a small side instead of moving the big side's rows

    Count the plain Exchange operators to count the shuffles. In the lesson's runs the filter planned 0, the grouping 1, and the broadcast join 0 with one BroadcastExchange beside it.

  3. Question 3 of 8 The recorded filter kept the events whose minutes were 40 or more: 266,666 rows of 1,200,000. How many rows did that step move between partitions?

    Type a number.

    A hint

    The plan for that step held no Exchange.

    Show the answer: Question 3 of 8

    Answer: 0 rows

    A filter decides each row where it already sits, so nothing moves. The plan proves it: no Exchange operator, and so one stage.

  4. Question 4 of 8 A job joins two tables of 400 GB each on a customer id. Which strategy should you expect, with the default settings?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: A sort-merge join, because neither side is small enough to copy to every executor

    The default path for two large sides is the sort-merge join, and the plan shows it as two Exchange operators with a Sort under each side.

  5. Question 5 of 8 The documentation says a BROADCAST hint is prioritised even when the statistics put the hinted side above spark.sql.autoBroadcastJoinThreshold. What risk does that create?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: A side that grows later is still broadcast, so every executor must hold a copy that no longer fits

    A hint is a decision made once, about data that keeps changing, so it belongs with a note that says what size it assumed.

  6. Question 6 of 8 What decides how many partitions a file read produces?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: The files and their sizes, packed into partitions of up to spark.sql.files.maxPartitionBytes

    A file read packs files into partitions of up to spark.sql.files.maxPartitionBytes, 128 MB by default, so a thousand small files read as far fewer than a thousand partitions. A generated range splits by the session's cores instead, which is why the recorded runs began with 2.

  7. Question 7 of 8 Your stage after a grouping runs far too many tiny tasks. Which setting is the one that decides how many partitions that shuffle declares?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: spark.sql.shuffle.partitions

    One setting, one job of its own: spark.sql.shuffle.partitions is the declared partition count of every shuffle in the session unless a query repartitions explicitly.

  8. Question 8 of 8 A pipeline reads 1,500 small Parquet files, groups them by day and writes the result partitioned by day. Which statements about its counts are right? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 8 of 8

    Answer:

    • How many partitions the read makes depends on the files' sizes as well as their number
    • The grouping's stage declares the number of partitions the shuffle setting says, whatever the first stage had

    Three counts, three rules: the files' sizes and the read settings for the read, the setting for the shuffle, and up to tasks times partition values for the files on disk.

Broadcast, sort-merge or shuffled hash: the plan that proves the choice Pro: opens with a Pro, Premium or Ultimate pass

One join of one pair of tables can be planned four ways, and the plan says which one happened in operator names that take no interpreting. The run below records all four on the same 1,200,000 events and 8 zones, and every one of them returns the same rows.

In this part:

  • The same join four ways, and the operators each one left
  • What rules each strategy out

1 runnable example (join_strategies.py, join_strategies_session.py) · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

How many partitions a stage really runs, before and after adaptive execution Pro: opens with a Pro, Premium or Ultimate pass

A shuffle declares its partition count before the query runs, and adaptive query execution decides afterwards how many tasks are worth using. Both numbers are readable, and they are usually not the same number.

In this part:

  • Two hundred partitions declared, and what actually ran
  • Sizing the number from the data, with the arithmetic

1 runnable example (shuffle_partitions.py, shuffle_partitions_session.py) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Finding the expensive step from counts, not from guesses Pro: opens with a Pro, Premium or Ultimate pass

A stage is slow for a reason that is countable: one task holds several times the rows of its neighbours, or the query runs more stages than the work needs. Both numbers can be read off a running job, and the run below prints them for four shapes of the same 1,200,000 events.

In this part:

  • Row counts per partition say where the work sits
  • Stage counts follow the shuffles, and the UI fills in the rest
  • Count the shuffles and files a job runs from its plan
    • Results of the sample tests
  • Key takeaways from these parts

1 runnable example (partition_counts.py, partition_counts_session.py) · 1 exercise with sample tests · about 10 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

Warm-up: why is a shuffle the step people blame for a slow Spark job?

Because it is the only step that moves rows between tasks. Filters, projections and casts run where the rows already are. Grouping, ranking and two of the three join strategies need equal keys in one place, so every task writes its rows into buckets, those buckets reach a file once they outgrow memory, and every reading task fetches the ones addressed to it. That costs serialisation, disk and network, and it splits the job into stages that wait for each other.

Three longer questions start where this lesson ends:

  1. Mid to senior

    A nightly job joins 40 million delivery rows to a zones table. It used to plan a broadcast join; this week the plan shows a sort-merge join with two Exchange operators, and the job now misses its window. How do you investigate and what do you change?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    A platform team sets spark.sql.shuffle.partitions to 4000 for every job, because one large job needed it. Small jobs now run thousands of tasks that hold almost nothing. How do you decide the number, per job and as a default?

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A daily job repartitions its delivery events by city, counts them per zone and date, and writes the counts partitioned by date. Its plan holds two Exchange operators, and each run leaves about 6000 files in 30 date folders. Cut the shuffles and the files without changing a row of the output.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why is the shuffle the step people blame for a slow Spark job?

    It is the only step that moves rows between tasks so that equal keys meet. Every task writes its rows into buckets and every reader fetches its own, which costs serialisation, disk and network and splits the job into stages.

  2. Why does a filter plan no Exchange while a count per zone plans one?

    A filter decides each row where it already sits. A count needs every row of one zone in one place, and only a shuffle puts them there.

  3. What are Spark's three join strategies, and which move the big side?

    A broadcast hash join sends one whole side to every executor and leaves the other in place. A sort-merge join and a shuffled hash join both shuffle the two sides on the key; the first sorts and merges, the second builds a hash table per partition.

  4. When does Spark broadcast one side of a join?

    When its estimated size is under spark.sql.autoBroadcastJoinThreshold, 10485760 bytes by default. A value of -1 turns broadcasting off, and a BROADCAST hint wins even above the threshold.

  5. Joining 1,200,000 delivery events to 8 city zones, how many event rows crossed the network?

    None. The 8-row table sat far below the broadcast threshold, so the plan was a BroadcastHashJoin and the events never left their tasks.

  6. Where do the partitions of a file read and of a shuffle come from?

    A read packs files into partitions of up to spark.sql.files.maxPartitionBytes, 128 MB by default, charging each file an open cost of 4 MB. A shuffle declares spark.sql.shuffle.partitions, 200 by default, which adaptive execution may coalesce.

Key takeaways

  • A shuffle re-distributes rows so that equal keys meet, and each one stands in the plan as an Exchange operator.
  • Spark broadcasts a join side whose estimated size is under 10485760 bytes by default, and otherwise shuffles both sides.
  • A file read packs files into partitions by bytes, a shuffle declares spark.sql.shuffle.partitions partitions, and a write leaves up to tasks times partition values in files.

Terms in this lesson

broadcast join threshold
The size up to which Spark sends a whole table to every executor rather than shuffling both sides of a join, set by spark.sql.autoBroadcastJoinThreshold; the value -1 switches broadcasting off.
Exchange operator
The operator in a Spark physical plan that stands for one shuffle: rows leave the partitions they were computed in and are re-distributed, usually by a hash of the key.
partition coalescing
Adaptive query execution joining neighbouring shuffle partitions into fewer, larger ones after it has measured a stage's output, so the next stage runs fewer and bigger tasks.
shuffle
Spark re-distributing rows so that equal keys end up in one partition: each task of a stage writes its rows into one bucket per target partition, and the next stage's tasks fetch the buckets they need.
shuffle partition
One of the partitions a shuffle writes its rows into, and one task of the stage that reads them. spark.sql.shuffle.partitions sets how many partitions a shuffle makes.
shuffled hash join
A join that shuffles both sides on the join key and then builds a hash table from one side's partition, instead of sorting both sides and merging them.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.