All posts

200 Apache Spark interview questions, with answers

Two hundred questions from first principles to production scenarios, each with an answer you can defend. Grouped by topic, collapsed so you can drill one section at a time, and written against the latest Spark release, 4.2.0 as of now.

63 min read Spark

TL;DR

  • 200 questions in 11 sections, from what an RDD is to why a job that took 20 minutes now takes 2 hours. Click any question to open the answer.
  • The last 15 are scenario questions: a symptom, and how to reason to the cause. These are what separate people who have run Spark in production.
  • Every config default and class name was read from the v4.2.0 tag or the docs for it, not recalled. Where a default surprises people, the number is given.
  • The answers say when the textbook answer is wrong: that AQE skew handling needs a partition over 256 MB before it fires, that input partitions do not come from file count, that accumulators double-count outside actions.
  • No speedup ratios. Where performance matters the answer names the mechanism and tells you to measure on your own data.

Interview lists tend to collect questions without checking the answers, so they repeat things that were true in Spark 1.6 and quote defaults that have since changed. This one is built the other way round: the answers came first, from source and documentation at v4.2.0, and anything that could not be verified was left out.

Use it two ways. Read a section end to end to find the gaps in your own model, or open a single question when you want the defensible version of an answer you already half know. The sections go roughly from basics to advanced, and the scenario section at the end is the one worth rehearsing out loud.

A note on what is deliberately absent: there are no benchmark numbers or speed multipliers here. Where performance is the point, the answer explains the mechanism that makes one path cheaper and leaves the measuring to you, on your data and your cluster.

How this is organised

Section Questions
Spark fundamentals 1 to 20
RDDs 21 to 40
DataFrames, Datasets and Spark SQL 41 to 62
Architecture and execution 63 to 84
Partitioning and shuffle 85 to 104
Memory, caching and persistence 105 to 122
Joins, Catalyst and adaptive execution 123 to 142
Performance tuning and troubleshooting 143 to 160
Structured Streaming 161 to 175
Deployment, cluster managers and operations 176 to 185
Scenario-based questions 186 to 200

Spark fundamentals

Start here. These come up in the first ten minutes of almost every interview.

1. What is Apache Spark?

A distributed engine for large-scale data processing. You express a computation over a dataset, and Spark splits both the data and the work across a cluster, coordinating it from a single driver process. It provides APIs in Scala, Java, Python and R, plus libraries for SQL, streaming, machine learning and graphs.

2. Why use Spark instead of a single machine?

Because the data no longer fits the memory, CPU or disk of one machine, or because the work finishes sooner spread across many. Spark divides a dataset into partitions and processes them in parallel. The trade is coordination cost, which is why a small dataset often runs faster on one machine than on a cluster.

3. What is the difference between Spark and Hadoop MapReduce?

MapReduce writes intermediate results to disk between every map and reduce. Spark keeps them in memory where it can, and expresses a whole chain of operations as one plan before running any of it. That matters most for iterative work, where MapReduce re-reads the same data from disk on every pass.

The honest framing for an interview: Spark’s advantage is in-memory intermediate data plus whole-plan optimisation, not a single fixed speed multiplier. Quote a measured number only if you measured it on your own data.

4. What is lazy evaluation?

Transformations do not compute anything when you call them. They record what to do and return a new dataset. Nothing runs until an action asks for a result. The RDD programming guide states it directly: “All transformations in Spark are lazy, in that they do not compute their results right away.”

This is what makes optimisation possible: Catalyst sees the whole chain before any of it has run.

5. What is the difference between a transformation and an action?

A transformation creates a new dataset from an existing one and runs nothing. An action returns a value to the driver or writes to storage, and submits a job.

  Transformation Action
Examples map, filter, select, groupBy, join count, collect, show, take, write
Returns A new dataset A value, or a write
Runs Nothing yet Submits a job
6. How can you prove that transformations are lazy?

Count the jobs. Chaining transformations submits none; the action submits one.

rdd = spark.sparkContext.parallelize(range(4), 2)
doubled = rdd.map(lambda x: x * 2).filter(lambda x: x > 2)
print(len(spark.sparkContext.statusTracker().getJobIdsForGroup() or []))   # 0
doubled.count()
print(len(spark.sparkContext.statusTracker().getJobIdsForGroup() or []))   # 1
7. What is a partition?

The unit of parallelism. A dataset is split into partitions, and a stage runs one task per partition. Almost every Spark performance question reduces to how many partitions you have and how evenly the data is spread across them.

8. What is `SparkSession` and how does it relate to `SparkContext`?

SparkSession is the entry point for the DataFrame and SQL APIs, and it is what you create in modern code. SparkContext is the lower-level entry point for RDDs and cluster coordination, reachable as spark.sparkContext.

from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("trips").getOrCreate()
sc = spark.sparkContext

One SparkSession wraps one SparkContext.

9. What languages does Spark support, and does the choice affect performance?

Scala, Java, Python and R. For DataFrame and SQL work the language barely matters, because the operations compile to the same plan and run in the JVM. It matters for RDDs and Python UDFs, where data must cross into a Python process and back, which blocks whole-stage codegen and predicate pushdown across that point.

10. What is a job, a stage and a task?
Term Definition Created by
Job One action The action call
Stage A group of tasks with no shuffle between them DAGScheduler, cut at shuffle boundaries
Task One partition of one stage TaskSchedulerImpl

A job has one or more stages, a stage has one task per partition.

11. What is a DAG in Spark?

A directed acyclic graph of dependencies between datasets. Directed because each edge points from input to output, acyclic because nothing depends on itself. DAGScheduler walks it to cut stages and to work out what to recompute if something is lost.

12. What is the driver?

The process that holds the SparkSession, builds and optimises the plan, cuts it into stages, schedules every task, and tracks the results. It is the coordinator and it does not process your data. If the driver dies, the application dies, because the plan and the scheduler state die with it.

13. What is an executor?

A JVM launched for one application that runs tasks and stores cached and shuffle blocks. The source describes it in a line: a “Spark executor, backed by a threadpool to run tasks”. It never makes a scheduling decision and never sees the query plan.

14. What is a cluster manager, and which ones does Spark support?

A separate service that grants containers. At Spark 4.x the options are Standalone, YARN and Kubernetes. Mesos support was removed in the 4.x line: resource-managers/ in the source tree holds only kubernetes and yarn.

The key division is that the cluster manager grants resources but does not schedule your tasks. The driver does that.

15. Is Spark a master-slave architecture?

In the general sense yes, one coordinator and many workers, but Spark’s own vocabulary matters and the word “master” is overloaded.

Term What it is
Driver The coordinator. Never called the master in Spark
--master A URL naming the cluster manager
Standalone Master A daemon, standalone mode only
Worker A machine hosting executors
Executor A JVM doing the work

Spark also moved away from the older terminology: at v4.2.0 there is sbin/start-workers.sh and conf/workers.template, while start-slaves.sh and slaves.template are gone.

16. What is `spark-submit`?

The script that launches an application against any supported cluster manager through one interface.

spark-submit \
  --class org.apache.spark.examples.SparkPi \
  --master yarn \
  --deploy-mode cluster \
  --executor-memory 4g \
  --executor-cores 4 \
  examples/jars/spark-examples.jar 1000

Every Spark-facing option must come before the application jar. Anything after it is passed to your application instead.

17. What is the difference between client and cluster deploy mode?

Where the driver runs. In client mode it runs in the submitting process; in cluster mode the cluster manager launches it inside the cluster.

  Client Cluster
Driver runs On the submitting machine In the cluster
Console output Attached In the driver’s log
Suited to Shells, notebooks, debugging Production jobs

One consequence catches people: in client mode the driver JVM already exists by the time your code runs, so spark.driver.extraJavaOptions set in SparkConf arrives too late. Use --driver-java-options.

18. What is the default parallelism, and what sets it?

spark.default.parallelism governs RDD shuffle operations. The documented default depends on the cluster manager: for distributed shuffles it is the largest number of partitions in the parent RDD, and for operations like parallelize with no parent it follows the cluster manager, which on local mode is the number of cores. For DataFrames, the relevant knob is spark.sql.shuffle.partitions, which defaults to 200.

19. What is `spark.sql.shuffle.partitions` and why does the default cause trouble?

The number of partitions produced by a DataFrame shuffle. It defaults to 200 regardless of your data size or cluster, which is why it is wrong so often: 200 tasks over a few megabytes is scheduling overhead, and 200 tasks over a terabyte is spill. Adaptive query execution coalesces it down at runtime, which is why leaving the default is more tolerable in modern Spark than it used to be.

20. Which Spark version introduced Log4j 2, and why does it matter?

Spark 3.3.0. The file became log4j2.properties and the flag -Dlog4j.configurationFile=. At v4.2.0 the only template shipped is conf/log4j2.properties.template; the Log4j 1 template is gone. Guides that tell you to edit log4j.properties with log4j.rootLogger= predate that change and the file will be ignored.

RDDs

The original API. Interviewers still ask, because the concepts underneath the DataFrame API are these.

21. What is an RDD?

A Resilient Distributed Dataset: an immutable, partitioned collection that can be operated on in parallel, with enough information recorded to rebuild any lost partition. Resilient because of lineage, distributed because of partitions.

22. What are the five properties of an RDD?

From RDD.scala, every RDD is defined by:

  1. A list of partitions.
  2. A function for computing each partition.
  3. A list of dependencies on other RDDs.
  4. Optionally, a partitioner for key-value RDDs.
  5. Optionally, preferred locations for each partition.

Properties 4 and 5 are the ones that explain shuffle avoidance and data locality.

23. How does Spark achieve fault tolerance for RDDs?

Through lineage. Spark records how each partition was derived rather than only the partition itself, so a lost partition is recomputed from its parents on a surviving executor.

lineage = (spark.sparkContext.parallelize(range(10), 2)
    .map(lambda x: x + 1)
    .filter(lambda x: x % 2 == 0))
print(lineage.toDebugString().decode())

Recovery is per partition, not per job.

24. What is the difference between a narrow and a wide dependency?

In a narrow dependency each parent partition feeds at most one child partition, so the work pipelines inside one task. In a wide dependency a child partition draws from many parents, which requires a shuffle and therefore a stage boundary.

  Narrow Wide
Classes OneToOneDependency, RangeDependency ShuffleDependency
Examples map, filter, mapPartitions groupByKey, reduceByKey, join, repartition
Shuffle No Yes
Stage Same stage Creates a boundary
25. Where exactly does Spark cut a stage?

At every ShuffleDependency in the lineage, and nowhere else. DAGScheduler walks the graph backwards from the action and starts a new stage each time it crosses one. Everything narrow pipelines into the stage it is already in, which is why a long chain of map and filter costs no extra stages.

26. What is the difference between `reduceByKey` and `groupByKey`?

reduceByKey combines values on the map side before the shuffle, so far less data crosses the network. groupByKey shuffles every value and combines afterwards.

pairs.reduceByKey(lambda a, b: a + b)      # prefer this
pairs.groupByKey().mapValues(sum)          # shuffles everything

The subtlety worth knowing: a map-side combine disqualifies Spark’s two faster shuffle writers, so reduceByKey shuffles less data through a more expensive writer. It is still usually the right choice, because less network traffic dominates.

27. What is the difference between `map` and `mapPartitions`?

map applies a function per element; mapPartitions applies it once per partition, receiving an iterator. Use mapPartitions when there is per-partition setup worth amortising, such as opening a database connection, and be careful not to materialise the whole partition in memory.

28. What is the difference between `map` and `flatMap`?

map returns one output per input. flatMap returns zero or more per input and flattens the result, which is how you both expand and filter in one pass.

29. What is the difference between `repartition` and `coalesce`?

repartition shuffles to reach any target number of partitions and balances them. coalesce merges partitions without a full shuffle, so it can only reduce the count and may leave them uneven.

Use coalesce to reduce partitions before a write; use repartition when you need balance or more partitions. One trap: coalesce(1) before a wide transformation can shrink the parallelism of the whole upstream stage, because it removes the shuffle boundary that would have isolated it.

30. What is the difference between `cache` and `persist`?

cache() is persist() with the default storage level. persist(level) lets you choose. The default differs by API, which surprises people:

API Default level
rdd.cache() Memory only
df.cache() Disk and memory, deserialized

So an RDD cache that does not fit simply drops partitions and recomputes them, while a DataFrame cache spills to disk.

31. What are the RDD storage levels?

MEMORY_ONLY, MEMORY_ONLY_SER, MEMORY_AND_DISK, MEMORY_AND_DISK_SER, DISK_ONLY, their _2 replicated variants, and OFF_HEAP. The _SER variants store a serialized form, which is more compact and more CPU to read. The _2 variants replicate to a second node so a lost executor does not force recomputation.

32. What are broadcast variables?

A read-only value cached once per executor rather than shipped with every task. The guide puts it as keeping “a read-only variable cached on each machine rather than shipping a copy of it with tasks”.

cities = sc.broadcast({"sf": "San Francisco", "nyc": "New York"})
resolved = sc.parallelize(["sf", "nyc"]).map(lambda c: cities.value[c])

Use them for lookup tables. Without one, a large captured object is serialized per task.

33. What are accumulators, and what is the catch?

Write-only counters that tasks add to and the driver reads. The catch is the guarantee: exactly-once applies only to updates made inside actions. The guide says that in transformations “each task’s update may be applied more than once if tasks or job stages are re-executed”.

acc = sc.accumulator(0)
counted = sc.parallelize(range(100), 4).map(lambda x: (acc.add(1), x)[1])
counted.count(); print(acc.value)   # 100
counted.count(); print(acc.value)   # 200, same 100 rows

The uncached dataset is recomputed, so the accumulator increments again. Add .cache() and it stays at 100. Use accumulators for diagnostics, never for a number the output depends on.

34. What is a partitioner, and when does it help?

The function mapping a key to a partition, HashPartitioner or RangePartitioner. It helps when two datasets share a partitioner on the join key, because the join then needs no shuffle. Persist a partitioned RDD if you will reuse it, or the partitioning is recomputed.

35. What does `toDebugString` tell you?

The lineage of an RDD as an indented tree, with each level showing the RDD type and the number of partitions. Indentation changes mark shuffle boundaries, so it is the quickest way to see how many stages a computation will take.

36. When should you still use RDDs?

When you need control the structured APIs do not expose: custom partitioning, per-partition resource handling, or operations on unstructured data with no schema. For anything with a schema, the DataFrame API goes through Catalyst and will usually beat hand-written RDD code.

37. What is the difference between `collect` and `take`?

collect() brings every row to the driver, bounded only by spark.driver.maxResultSize, which defaults to 1g. take(n) returns n rows and starts by scanning one partition, adding more only if needed. Prefer take for inspection; collect on a large result is the classic way to kill a driver.

38. What is the difference between `reduce` and `fold`?

Both aggregate to a single value. fold takes a zero value and so is defined on an empty dataset, where reduce throws. The zero value must be a true identity, because it is applied per partition as well as across them.

39. Why are RDD operations on key-value pairs special?

Because only they can be partitioned by key, which is what enables shuffle-free joins, reduceByKey-style map-side combines, and lookup. In Scala they are available through implicit conversion on RDD[(K, V)]; in Python any RDD of 2-tuples qualifies.

40. What happens if a task fails?

It is retried, up to spark.task.maxFailures, which defaults to 4, before the stage fails. If instead a completed stage’s shuffle output was lost with its executor, DAGScheduler resubmits that whole stage, because the next stage has nothing to fetch. That is a stage rerun rather than a task retry, and it is why a job can appear to go backwards in the UI.

DataFrames, Datasets and Spark SQL

The API you should reach for by default, and the questions that separate people who use it from people who understand it.

41. What is a DataFrame?

A distributed collection of rows organised into named columns, with a schema. Operations on it go through Catalyst, so Spark can reorder filters, prune columns and choose join strategies. It is the API to default to.

42. What is a Dataset, and how does it differ from a DataFrame?

A Dataset is a typed collection that still runs through Spark SQL’s optimised engine. In Scala and Java it gives compile-time type safety and encoders. In Python there is no typed Dataset API, because the language has no compile-time types to check, so DataFrame is what you get.

43. Compare RDD, DataFrame and Dataset.
  RDD DataFrame Dataset
Level Low Structured Typed structured
Schema None Yes Yes
Catalyst No Yes Yes
Type safety Compile time (Scala) Runtime Compile time (Scala, Java)
Python Yes Yes No typed API
Reach for it when Custom partitioning, no schema Almost always Typed Scala or Java code
44. What is Catalyst?

Spark SQL’s query optimiser. It takes a parsed query through resolution, rule-based logical optimisation, physical planning with cost-based choices, and then code generation. The important idea is that it optimises a whole plan, which is only possible because transformations are lazy.

45. What are Catalyst's phases?
  1. Parse into an unresolved logical plan.
  2. Analyse, resolving tables, columns, functions and types against the catalog.
  3. Optimise the logical plan with rules: predicate pushdown, column pruning, constant folding, filter reordering.
  4. Plan physically, choosing join strategies and producing one or more physical plans.
  5. Generate code, compiling operators into JVM bytecode via whole-stage codegen.
46. What is predicate pushdown?

Moving a filter as close to the data source as possible, so fewer rows are read. With Parquet, Spark can push it into the file format, which skips row groups by their footer statistics. You can see it in the plan as PushedFilters.

FileScan parquet [fare_amount,city_id]
  PushedFilters: [IsNotNull(fare_amount), GreaterThan(fare_amount,50.0)]
47. What is column pruning?

Reading only the columns the query needs. It is a large win on columnar formats such as Parquet and ORC, where unread columns are never touched on disk. Selecting explicitly rather than using select("*") is what lets it happen.

48. What is whole-stage code generation?

Catalyst fuses the operators in a stage into a single generated Java method, so intermediate rows never become objects and the loop is tight. In explain output, fused operators are marked with a * and a codegen stage id. Python UDFs break the fusion, which is a large part of their cost.

49. How do you read a query plan?
df.explain("formatted")     # the readable one
df.explain("extended")      # parsed, analysed, optimised, physical
df.explain("cost")          # with statistics, if available

Read the physical plan bottom up: scans at the bottom, then filters and projections, then exchanges, then the final operator. Each Exchange is a shuffle and therefore a stage boundary.

50. What is the difference between `select` and `withColumn`?

select projects a set of columns; withColumn adds or replaces one, keeping the rest. Chaining many withColumn calls builds a deep plan and is slower to analyse than one select with all the expressions, which matters when you are adding dozens of columns.

51. What is the difference between a temporary view and a global temporary view?

createOrReplaceTempView is scoped to the SparkSession that created it. createGlobalTempView is visible to every session in the application, under the global_temp database, and lives until the application ends.

52. What is a UDF, and why avoid it?

A user-defined function. Avoid it when a built-in exists, because a UDF is a black box: Catalyst cannot see inside it, so it cannot push filters through it or fuse it into generated code. In PySpark a plain Python UDF also serialises rows out to a Python process and back.

53. What is a pandas UDF and when does it help?

A vectorised UDF that operates on Arrow batches rather than one row at a time, so the interpreter overhead is amortised across the batch. Use it when you genuinely need Python logic on columns. It still breaks codegen, but the per-row cost is far lower than a plain Python UDF.

54. How do you handle nulls correctly?

Use isNull and isNotNull rather than equality, because col = NULL is never true in SQL. na.fill, na.drop and coalesce cover most cases. Be aware that from Spark 4.0 spark.sql.ansi.enabled is on by default, which changes how invalid casts and overflows behave: they raise instead of returning null.

55. What changed with ANSI mode in Spark 4?

The migration guide is explicit: “Since Spark 4.0, spark.sql.ansi.enabled is on by default.” Invalid casts, numeric overflows and division by zero now raise errors instead of yielding null. Set it to false to restore the old behaviour, but treat a failure on upgrade as a finding: it is usually pointing at data that was silently becoming null before.

56. What is the difference between `union` and `unionByName`?

union matches columns by position; unionByName matches by name. Positional matching on two DataFrames whose columns are in different orders silently produces wrong data, which makes unionByName the safer default. It also takes allowMissingColumns.

57. How do you write partitioned output, and what is the trap?
(df.write
   .partitionBy("city_id")
   .mode("overwrite")
   .parquet("s3a://lakehouse-prod/warehouse/trips"))

The trap is file count. With 200 shuffle partitions and 50 cities you can get 10,000 small files. Repartition by the same column first so each partition value is written by few tasks.

58. What is the difference between `partitionBy` and `bucketBy`?

partitionBy writes a directory per value, which prunes at scan time. bucketBy hashes rows into a fixed number of files per partition and records it in the metastore, which lets a join on the bucket key skip the shuffle. Bucketing requires a metastore table, so it only applies to saveAsTable.

59. What are the save modes?

append, overwrite, ignore and error (the default, also spelled errorifexists). With partitioned output, spark.sql.sources.partitionOverwriteMode set to dynamic makes overwrite replace only the partitions the data touches rather than the whole table.

60. Why is Parquet usually the right format?

It is columnar, so column pruning skips unread columns on disk; it carries per-row-group statistics, so pushed-down predicates skip data; and it compresses well because a column holds like-typed values. spark.sql.parquet.filterPushdown defaults to true.

61. How do you read from JDBC in parallel?

Give Spark a numeric, date or timestamp column to split on, plus bounds.

trips = (spark.read.format("jdbc")
    .option("url", "jdbc:postgresql://db.internal:5432/rides")
    .option("dbtable", "public.trips")
    .option("partitionColumn", "trip_seq")
    .option("lowerBound", "1")
    .option("upperBound", "40000000")
    .option("numPartitions", "16")
    .load())

Without partitionColumn the whole table is read by a single task. The column must be numeric, date or timestamp: a string key will not do.

62. What is the difference between `cache` on a DataFrame and a temporary view?

Caching stores the computed result; a view stores the query. CACHE TABLE in SQL caches eagerly by default, whereas df.cache() is lazy and populates on the next action.

Architecture and execution

How a line of code becomes work on a cluster. The strongest signal of depth in an interview.

63. Walk through what happens when you submit a Spark application.
  1. spark-submit hands the application to the cluster manager.
  2. The driver starts and creates the SparkSession.
  3. The driver asks the cluster manager for executors.
  4. Executors start and register back with the driver.
  5. Your code builds a plan; nothing runs yet.
  6. An action submits a job.
  7. Catalyst optimises; DAGScheduler cuts stages at shuffle boundaries.
  8. TaskSchedulerImpl places tasks on executors by locality.
  9. Executors run tasks and write shuffle output for the next stage.
  10. Results return to the driver, or executors write them to storage.
64. What does the driver actually do, step by step?

Creates the SparkSession; requests executors through SchedulerBackend; turns code into a logical then physical plan; cuts stages in DAGScheduler; places tasks via TaskSchedulerImpl and TaskSetManager; tracks attempts and retries; keeps the block index in BlockManagerMaster; receives results; and serves the UI on port 4040.

65. What does an executor do, step by step?

Registers with the driver by sending RegisterExecutor and waiting for RegisteredExecutor; accepts LaunchTask messages and runs each in a pool thread; reads input and writes shuffle output; reports StatusUpdate per task plus a heartbeat every spark.executor.heartbeatInterval, default 10s; and stores and serves blocks through its BlockManager.

Until that registration round trip completes, a granted container is doing nothing.

66. What does the cluster manager do?

Decides who gets containers, and nothing else. Standalone uses a Master tracking Worker daemons; YARN uses the ResourceManager, asked by Spark’s ApplicationMaster through YarnAllocator; on Kubernetes the driver’s own ExecutorPodsAllocator creates pods, so there is no Spark cluster-manager daemon at all.

67. What is `DAGScheduler` responsible for?

Turning a job into a DAG of stages by cutting at every ShuffleDependency, submitting each stage as a TaskSet, and resubmitting stages whose shuffle output has been lost. It is the stage-level scheduler.

68. What is `TaskSchedulerImpl` responsible for?

Placing individual tasks from a TaskSet onto executors, tracking attempts, handling retries and speculation. It is the task-level scheduler and it works with TaskSetManager per stage.

69. What is `SchedulerBackend`?

The one component that talks to the cluster manager, with an implementation per manager. It requests and releases executors and relays their registration to the scheduler.

70. What is a `ShuffleMapStage` versus a `ResultStage`?

Every stage but the last is a ShuffleMapStage, whose job is to produce shuffle output for the next stage. The last is a ResultStage, which computes the answer the action asked for. That is why the UI shows a final stage with a different shape.

71. What are the task locality levels?

PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY. The scheduler prefers the best available and waits briefly for it, governed by spark.locality.wait. If your data is remote object storage, locality mostly does not apply and lowering the wait can help.

72. Where does the input partition count come from?

Not from the number of files, which is the assumption that costs most people time. FilePartition.maxSplitBytes decides the split size:

Math.min(defaultMaxSplitBytes, Math.max(openCostInBytes, bytesPerCore))

spark.sql.files.maxPartitionBytes defaults to 128MB and spark.sql.files.openCostInBytes to 4MB, charged per file so small files are packed together. The consequence: the same data written as 1, 8 or 16 Parquet files can read back as 1, 2 and 2 partitions. Read the number with getNumPartitions() rather than predicting it.

73. How many tasks will a stage have?

One per partition of that stage. For the first stage that is the input partition count; after a shuffle it is spark.sql.shuffle.partitions, or whatever adaptive execution coalesced it to. It is not related to the number of executors.

74. What happens when an executor is lost?

Its running tasks are retried elsewhere and the partitions it held are rebuilt from lineage. Cached blocks are recomputed unless the storage level replicated them. If a completed stage’s shuffle output died with it, that stage is resubmitted.

75. Why does losing the driver kill the application, but losing an executor not?

Because the driver’s state, the plan, the scheduler and the block index, exists nowhere else. Executors hold only data that can be recomputed or refetched, so they are disposable by design.

76. What is the external shuffle service, and why use it?

A daemon on each node that serves shuffle blocks independently of the executors that wrote them, so an executor can exit without destroying its shuffle output. spark.shuffle.service.enabled defaults to false. It matters for dynamic allocation, which otherwise cannot release an executor still holding shuffle data.

77. What is dynamic allocation?

Adding and removing executors according to pending work. spark.dynamicAllocation.enabled defaults to false, so absent a decision your application holds its executors from first request to exit. Without an external shuffle service it needs spark.dynamicAllocation.shuffleTracking.enabled, which defaults to true, to avoid discarding executors that still hold shuffle blocks.

78. What is speculative execution?

Re-running tasks that are running much slower than their peers and taking whichever finishes first. spark.speculation defaults to false. It helps with a slow node; it hurts when the task is slow because of skew, since the duplicate is equally slow and wastes a slot.

79. What is the default scheduler mode within an application?

spark.scheduler.mode defaults to FIFO, so jobs run in submission order and a large job can starve later small ones. FAIR shares slots between jobs and is worth setting when one application serves many concurrent queries, such as a notebook or a JDBC server.

80. How do you set JVM options for the driver and executors?

spark.driver.extraJavaOptions and spark.executor.extraJavaOptions. In client mode the driver JVM is already running by the time SparkConf executes, so pass driver options with --driver-java-options or a properties file instead. Both configs are the route for logging configs, heap dump flags, stack size and proxy settings.

81. What is `spark.driver.maxResultSize` and why does it exist?

A cap on the total size of results returned to the driver, default 1g. It exists because collect() on a large dataset would otherwise exhaust the driver heap. Raising it treats the symptom; the fix is usually not to collect.

82. What port does the Spark UI use, and what happens after the application ends?

spark.ui.port defaults to 4040. When the application ends the UI goes with it, which is why you enable event logging and run a history server to replay it afterwards.

83. How do broadcast variables and accumulators relate to the driver and executors?

Broadcast variables go outward, cached once per executor rather than per task. Accumulators come back, aggregated on the driver. They exist because a closure’s captured variables are serialized to the executor and updates to them never return.

84. What is Spark Connect?

A client-server split where the client sends an unresolved logical plan over gRPC and the server runs it. The practical consequence is that the client no longer needs to be a JVM colocated with the driver, so a thin client can drive a remote cluster and a crash in user code cannot take out the driver.

Partitioning and shuffle

The single biggest source of Spark performance problems, and therefore of interview questions.

85. What is a shuffle?

A redistribution of data across partitions so that rows which must be processed together end up together. The map side writes partitioned, sorted output to local disk; the reduce side fetches its slice from every map output over the network. It is the most expensive thing Spark does.

86. What triggers a shuffle?

Any wide dependency: groupBy, join without a broadcast, distinct, repartition, orderBy, reduceByKey, window functions without an existing matching partitioning. In the physical plan every shuffle appears as an Exchange.

87. Why is a shuffle expensive?

Because it involves serialization, disk writes on the map side, network transfer, and often sorting or merging on the reduce side. It also creates a stage boundary, so the downstream stage cannot start until the upstream one has finished writing.

88. What are Spark's shuffle writers?

SortShuffleManager picks between three, cheapest first:

Writer Chosen when
Bypass merge sort No map-side combine, partitions below a threshold
Unsafe (serialized) No map-side combine, serializer supports relocation
Sort Everything else, the most general and most expensive

A map-side combine, which is exactly what reduceByKey does, disqualifies the two fast paths immediately.

89. What is data skew?

An uneven distribution of rows across partitions, usually because one key dominates. The symptom is a stage where most tasks finish quickly and one or two run far longer, since a task cannot be split.

90. How do you detect skew?

In the Spark UI’s stage page, compare the max task duration and shuffle read size against the median. A large gap is skew. In the data, count rows per key and look at the top of the distribution.

91. How do you fix skew?

In order of preference:

  1. Enable adaptive skew join handling, which splits skewed partitions automatically.
  2. Broadcast the small side if the join allows it, removing the shuffle.
  3. Salt the hot key: add a random suffix to spread it, join on the salted key, then aggregate away the salt.
  4. Process the hot keys separately and union the results.

Salting is the general answer when the hot key is genuinely large.

92. How does salting work in practice?
from pyspark.sql import functions as F

N = 16
facts = trips.withColumn("salt", (F.rand() * N).cast("int"))
dims  = (cities
    .withColumn("salt", F.explode(F.array([F.lit(i) for i in range(N)]))))

joined = facts.join(dims, ["city_id", "salt"]).drop("salt")

The fact side gets a random salt; the dimension side is replicated once per salt value so every match still exists. The cost is N times the dimension rows, which is why N stays small.

93. What are the adaptive skew join settings and their defaults?
Config Default
spark.sql.adaptive.enabled true
spark.sql.adaptive.skewJoin.enabled true
spark.sql.adaptive.skewJoin.skewedPartitionFactor 5.0
spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes 256MB

Both conditions must hold: a partition is treated as skewed only when it is larger than 5 times the median and larger than 256 MB. On a job whose partitions are all under 256 MB, skew handling never fires no matter how uneven they are.

94. What is adaptive query execution?

Re-planning parts of a query at runtime using statistics from completed stages. It coalesces shuffle partitions, converts sort-merge joins to broadcast joins when a side turns out small, and splits skewed partitions. It is on by default since Spark 3.2.

95. What does AQE partition coalescing do?

After a shuffle, it merges small output partitions into fewer, larger tasks, targeting spark.sql.adaptive.advisoryPartitionSizeInBytes, default 64MB, with a floor of spark.sql.adaptive.coalescePartitions.minPartitionSize, default 1MB. This is what makes the static 200 default tolerable.

96. How do you choose the number of shuffle partitions?

Aim for partitions in the low hundreds of megabytes, and for a total that is a small multiple of your total cores so every core gets several tasks. With AQE enabled, set the advisory size rather than the partition count, and let coalescing do the arithmetic.

97. What is the small files problem, and how do you avoid it?

Many tiny output files make later reads slow, because planning cost and per-file open cost dominate. It comes from writing with high parallelism or over-partitioning. Fix it by coalesce or repartition before the write, by partitioning output on a low-cardinality column, or by setting spark.sql.files.maxRecordsPerFile, which defaults to 0 meaning unlimited.

98. What is the difference between `repartition(n)` and `repartition(col)`?

repartition(n) distributes rows round-robin into n partitions. repartition(col) hash-partitions by that column, so all rows with the same value land together. The second is what you want before writing partitioned output or before a join on that key.

99. Does `orderBy` shuffle, and how?

Yes. A global sort uses a RangePartitioner, which first samples the data to find range boundaries and then shuffles rows into the right range. That sampling is an extra job, which is why a global sort is more expensive than it looks. sortWithinPartitions avoids it when you only need local ordering.

100. How can you join without a shuffle?

Three ways: broadcast the small side; join on a column both tables are already bucketed by, with matching bucket counts; or, with RDDs, join two datasets that already share a partitioner.

101. What is the shuffle fetch side, and which knobs matter?

The reduce side pulling blocks from map outputs. spark.reducer.maxSizeInFlight bounds how much data is in flight per reduce task, and spark.shuffle.io.maxRetries with spark.shuffle.io.retryWait govern retries. FetchFailedException almost always means an executor that held shuffle output died, not a network misconfiguration.

102. What causes `FetchFailedException`?

The executor holding the shuffle blocks is gone, was killed by the resource manager for exceeding memory, or is too busy to serve. Look at why that executor died rather than at fetch settings. An external shuffle service makes shuffle output survive the executor.

103. What is shuffle spill, and what does it tell you?

Data written to disk because the shuffle could not hold it in execution memory. The UI reports spill in memory and on disk. Spill means the partitions are too large for the memory available, so raise parallelism or lower the advisory partition size rather than only raising memory.

104. Why can `coalesce(1)` be dangerous?

Because it removes a shuffle boundary, so the reduced parallelism propagates upstream: the whole computation feeding it may run in one task. If you need exactly one output file, repartition(1) pays for a shuffle but keeps the upstream stage parallel.

Memory, caching and persistence

Where the errors are most alarming and the fixes least obvious.

105. How is executor memory divided?

UnifiedMemoryManager subtracts a fixed 300 MB of reserved memory, then takes spark.memory.fraction of what remains, default 0.6, as the unified pool for execution and storage. The rest is user memory for your own data structures. Off-heap is separate and off by default: spark.memory.offHeap.enabled defaults to false.

106. What is the difference between execution and storage memory?

Execution memory is for shuffles, joins, sorts and aggregations. Storage memory is for cached blocks and broadcast data. They draw from one unified pool, and spark.memory.storageFraction, default 0.5, sets the floor below which cached blocks will not be evicted.

107. Can execution and storage evict each other?

The eviction is one-way. Execution can evict cached blocks down to the storage floor; storage can never evict execution. So caching a large DataFrame cannot fail a shuffle, but it can make one spill, which is how adding a cache() makes a job slower.

108. What is the difference between heap and off-heap memory?

On-heap memory is managed by the JVM and subject to garbage collection. Off-heap is allocated outside the heap, bounded by spark.memory.offHeap.size, and avoids GC pressure at the cost of explicit management. Tungsten’s binary formats are what make off-heap useful.

109. What is executor memory overhead?

Memory requested from the cluster manager beyond the JVM heap, for off-heap allocations, the Python worker, and native libraries. spark.executor.memoryOverhead defaults to a fraction of executor memory with a minimum floor. Containers killed for exceeding memory limits are usually an overhead problem, not a heap problem.

110. What causes a driver `OutOfMemoryError`?

Usually collect() or toPandas() on a large result, a broadcast of something that is not small, or a very large number of tasks whose metadata the driver must track. spark.driver.memory defaults to 1g, which is fine for a coordinator and not for anything pulling data back.

111. What causes an executor `OutOfMemoryError`?

A partition too large to process, a skewed key concentrating rows in one task, a wide aggregation with high cardinality, or a UDF materialising too much. Raising memory is the last resort: raising the partition count is usually the fix, because it makes each task’s share smaller.

112. When should you cache?

When a DataFrame is used more than once and producing it was expensive. Caching something read once makes the job slower, because you pay the write and give up memory that execution wanted.

113. When should you not cache?

When the dataset is used once, when it is cheap to recompute, or when memory is already tight. Also avoid caching immediately before a single wide shuffle, where the cache competes with the shuffle for the same pool.

114. What are the DataFrame storage levels worth knowing?

MEMORY_AND_DISK is the default for df.cache(). MEMORY_AND_DISK_SER trades CPU for a smaller footprint. DISK_ONLY is for when recomputation is more expensive than reading from disk. MEMORY_ONLY is the RDD default and will silently drop partitions that do not fit.

115. How do you uncache, and why does it matter?

df.unpersist(), and spark.catalog.clearCache() for everything. It matters because cached blocks hold storage memory for the life of the application otherwise, squeezing execution memory for every later stage.

116. Is `cache()` an action?

No. It marks the dataset to be cached and populates on the next action. CACHE TABLE in SQL is eager by default, which is the exception.

117. What is checkpointing, and how does it differ from caching?

Checkpointing writes the dataset to reliable storage and truncates the lineage, so recovery does not replay the whole chain. Caching keeps lineage intact and stores a copy for reuse. Use checkpointing for very long lineages, such as iterative algorithms, where recomputation would be prohibitive.

localCheckpoint() truncates lineage using executor storage: faster, but lost if the executor is lost.

118. Why would a long lineage be a problem even if the data fits?

Because the plan itself grows, analysis and planning take longer, and a single lost partition triggers a long recomputation. A StackOverflowError from a deep plan is the extreme version, and checkpointing is the standard remedy.

119. What is the default serializer, and should you change it?

spark.serializer defaults to org.apache.spark.serializer.JavaSerializer. Kryo is more compact and faster, and is worth setting for RDD-heavy or shuffle-heavy workloads; register your classes for the full benefit. DataFrame operations mostly use Tungsten’s own encoders, so the effect there is smaller.

120. What is Tungsten?

The execution backend that stores rows in a compact binary format off-heap, operates on them without deserialising into JVM objects, and works with whole-stage codegen. It is why the DataFrame API beats hand-written RDD code on structured data.

121. How do you diagnose GC pressure?

Look at the GC time column in the executor tab of the UI. Consistently high GC time relative to task time points at too many small objects, too much data per executor, or a heap sized so large that full collections are slow. Serialized storage levels and fewer, smaller partitions per executor both help.

122. What does the storage tab of the UI tell you?

Which datasets are cached, the fraction actually held in memory, and how much spilled to disk. A cached dataset showing well below 100% in memory is one that is being partly recomputed or read from disk on every use, which usually means the cache is not paying for itself.

Joins, Catalyst and adaptive execution

Join strategy is where most real tuning happens.

123. What join strategies does Spark have?
Strategy How it works Good when
Broadcast hash join Ship the small side to every executor One side is small
Shuffle hash join Shuffle both, build a hash table per partition One side is much smaller but not broadcastable
Sort-merge join Shuffle both, sort, merge Both sides large
Broadcast nested loop Cartesian with a broadcast Tiny input, or no join key
Cartesian Full cross product Explicit cross joins only
124. What is a broadcast hash join and when does Spark choose it?

Spark ships the smaller side to every executor so the join needs no shuffle. It chooses this automatically when the estimated size is below spark.sql.autoBroadcastJoinThreshold, default 10485760, which is 10 MB. Set it to -1 to disable.

125. How do you force or prevent a broadcast?
from pyspark.sql.functions import broadcast
joined = trips.join(broadcast(cities), "city_id")

Or with a hint in SQL: SELECT /*+ BROADCAST(c) */ .... Available hints include BROADCAST, MERGE, SHUFFLE_HASH and SHUFFLE_REPLICATE_NL. Forcing a broadcast of something too large moves the failure to the driver, which must collect it first.

126. What is `spark.sql.broadcastTimeout`?

How long a broadcast may take to build before the query fails, default 300 seconds. Hitting it usually means the broadcast side is not actually small, so raise the estimate quality or change strategy rather than the timeout.

127. Why is sort-merge join the default for large joins?

Because it needs only a sort per partition and a single merge pass, with memory proportional to the merge buffers rather than to a whole hash table. spark.sql.join.preferSortMergeJoin defaults to true.

128. What is a shuffle hash join, and when is it better than sort-merge?

Both sides are shuffled, then a hash table is built from the smaller side per partition. It avoids the sort, so it wins when one side is small enough to hash per partition but too large to broadcast. Spark chooses it less often because it is more memory-hungry per task.

129. What is the difference between a bucketed join and a broadcast join?

A bucketed join avoids the shuffle because both tables were written hashed into the same number of buckets on the join key, recorded in the metastore. A broadcast avoids it by copying the small side everywhere. Bucketing works for large-to-large joins; broadcasting does not.

130. Why might a broadcast join not happen even though the table is small?

Because Spark’s size estimate is based on statistics that may be missing or stale, especially for a non-catalog source or after a chain of transformations. Run ANALYZE TABLE ... COMPUTE STATISTICS, or add a hint. Adaptive execution helps here, because it can switch to a broadcast at runtime once the real size is known.

131. What is cost-based optimisation and is it on?

Join reordering and strategy selection driven by table and column statistics. spark.sql.cbo.enabled defaults to false, and it also needs statistics to have been computed. Adaptive execution has taken over much of what CBO was meant to do, using real runtime numbers instead of estimates.

132. What does AQE do about join strategy?

After a shuffle stage completes, it knows the actual size of each side, so it can demote a planned sort-merge join to a broadcast hash join. This is the single most useful thing AQE does, because planning-time estimates on derived data are so often wrong.

133. What is a local shuffle reader?

An AQE optimisation: when a sort-merge join becomes a broadcast join at runtime, the shuffle that was already written can be read locally instead of over the network. spark.sql.adaptive.localShuffleReader.enabled defaults to true.

134. How do you read whether AQE actually changed the plan?

Compare df.explain() before execution with the UI’s SQL tab afterwards. The plan node is AdaptiveSparkPlan with isFinalPlan=false before running and true after, and the final plan shows the strategies AQE settled on.

135. What is the difference between a semi join and an anti join?

A left semi join returns rows from the left that have a match, with no columns from the right. A left anti join returns rows from the left with no match. Both are cheaper than a full join followed by a filter, because neither needs to carry right-side columns.

136. How do you handle a join key with nulls?

Nulls never match, so they end up on the outer side of an outer join and are dropped by an inner join. If a null should mean “unknown but joinable”, handle it explicitly with coalesce to a sentinel, and be aware that a large number of null keys concentrates into one partition, which is skew.

137. What causes a Cartesian product by accident?

A join condition that Spark cannot use as an equality, for example a join on an inequality or a condition written in a where after a cross join. The plan shows CartesianProduct or BroadcastNestedLoopJoin. Check the plan whenever a join is unexpectedly slow.

138. What is dynamic partition pruning?

Using the result of a dimension-side filter to prune partitions of the fact table at runtime, rather than scanning all of them. It applies when the fact table is partitioned on the join key and the dimension side is filtered, which is the classic star-schema shape.

139. How do you join a large table to a large table efficiently?

Reduce before joining: filter and project both sides first. Then either bucket both on the join key so the shuffle disappears, or accept a sort-merge join and make sure the shuffle partitions are sized sensibly and skew is handled. Broadcasting is not an option at that scale.

140. What is the `Exchange` node in a plan?

A shuffle. Its description names the partitioning, for example hashpartitioning(city_id, 200), which tells you both the key and the target partition count. Counting Exchange nodes is the fastest way to count the shuffles in a query.

141. What is `ReusedExchange`?

A shuffle whose output is reused by more than one branch of the plan rather than recomputed. Seeing it is good: it means Catalyst recognised a common subtree. Its absence where you expected reuse often means the two branches differ in a way you did not intend.

142. How do you reduce the number of shuffles in a query?

Aggregate and join on the same keys where possible so one shuffle serves both; repartition once by the shared key rather than letting each operator shuffle; replace distinct followed by join with a semi join; and avoid orderBy unless the output genuinely needs global ordering.

Performance tuning and troubleshooting

What an interviewer really wants to know: can you find the bottleneck rather than guess at knobs.

143. How do you approach a slow Spark job?

Find the bottleneck before changing anything. Open the SQL tab and the stage page, and identify the slowest stage. Then ask which of four things it is: skew, too much shuffle, spill, or too few or too many tasks. Only then change a config, and change one at a time.

144. What does the Spark UI tell you, tab by tab?
Tab Use it for
Jobs Which action is slow, and how many stages it took
Stages Task duration distribution, shuffle read and write, spill
SQL The physical plan and per-operator row counts and time
Storage What is cached and how much is actually in memory
Executors Per-executor task counts, GC time, failures
Environment The configs actually in effect
145. A few tasks take far longer than the median. What is it?

Skew. The data is unevenly distributed across partitions, and a task cannot be split. Confirm it on the stage page by comparing max to median shuffle read, then handle it with adaptive skew join, a broadcast, or salting.

146. The stage shows large spill. What do you change?

Raise parallelism so each task handles less, or lower the advisory partition size so AQE makes more partitions. Raising executor memory helps too, but it treats the symptom: the partitions are too large for the memory each task gets.

147. Planning takes longer than execution. What is wrong?

Usually too many small files, so the file listing and split computation dominate, or a plan grown deep by hundreds of chained withColumn calls. Check the number of input files first, then the plan size.

148. There are thousands of tasks each lasting milliseconds. What is wrong?

Too many partitions, so scheduling overhead dominates the work. Lower spark.sql.shuffle.partitions or let AQE coalesce, and coalesce before writing. The rule of thumb is partitions in the low hundreds of megabytes, not kilobytes.

149. How do you size executors?

Favour several medium executors over a few huge ones or many tiny ones. Around 4 to 5 concurrent tasks per executor is a common starting point, because tasks contend for the same HDFS client and because a very large heap makes full garbage collections slow. Then size memory so each task’s share covers your largest partition, and leave headroom for overhead.

150. How do you decide `--num-executors` versus dynamic allocation?

Pick one. Setting both --num-executors and dynamic allocation is contradictory, and the static value only acts as an initial size. Use static sizing for predictable batch jobs, and dynamic allocation for bursty or interactive workloads, remembering that it needs shuffle tracking or an external shuffle service.

151. What does a container killed for exceeding memory limits mean?

The cluster manager killed the JVM for using more than its requested total, which is heap plus overhead. It is usually overhead: off-heap buffers, the Python worker, or native libraries. Raise spark.executor.memoryOverhead rather than heap, and check whether one skewed partition is the real cause.

152. What is `spark.network.timeout` and when do you raise it?

The default timeout for network interactions, 120s. Raise it only when you have evidence of legitimately slow operations, such as very large shuffle fetches; otherwise a timeout is reporting a dead or overwhelmed executor and raising it just delays the error.

153. How do you debug a `NoClassDefFoundError` or a version conflict?

Add -verbose:class through spark.driver.extraJavaOptions or spark.executor.extraJavaOptions. It prints which JAR every class was loaded from, which turns a guess about shading into a direct answer. Then fix it with spark.jars, a shaded assembly, or spark.driver.userClassPathFirst where appropriate.

154. What causes a `java.lang.StackOverflowError` in Spark?

Usually a plan deep enough that a recursive walk over it exhausts the thread stack: hundreds of columns, deeply nested structs, or a long iterative lineage. Raising -Xss on the side that threw resolves many cases; checkpointing to truncate lineage is the structural fix.

155. How do you use custom logging to see what a job did?

Write a log4j2.properties, ship it with --files, and point both sides at it:

spark-submit \
  --files /etc/spark/conf/log4j2.properties \
  --conf "spark.driver.extraJavaOptions=-Dlog4j.configurationFile=log4j2.properties" \
  --conf "spark.executor.extraJavaOptions=-Dlog4j.configurationFile=log4j2.properties" \
  app.jar

Because --files puts it in each working directory, the value is a bare filename, not a path.

156. How do you confirm a config actually took effect?

The Environment tab of the UI lists the configs in force, which is the authoritative answer. spark.conf.get("key") works for SQL configs at runtime. A config set after the relevant component started, such as a driver JVM option in client mode, will silently not apply.

157. What is the difference between `spark.sql.files.maxPartitionBytes` and `spark.sql.shuffle.partitions`?

The first governs how input files are split into partitions on read, default 128MB. The second governs how many partitions a shuffle produces, default 200. Tuning read parallelism and shuffle parallelism are separate problems and people often reach for the wrong one.

158. How do you speed up a job that writes many partitions?

repartition by the same columns you partitionBy, so each output partition is written by one or a few tasks instead of all of them. That converts thousands of small files into a handful of properly sized ones, which also makes every later read faster.

159. What are the highest-value things to check before tuning configs?

Are you reading more columns or rows than you need; is there an accidental Cartesian or a missed broadcast in the plan; are there small files; is there skew; is something cached that should not be. Each of those outweighs most config changes.

160. How do you benchmark a change honestly?

Change one thing, run on representative data at representative scale, and compare the stage that you expected to change rather than total wall clock on a warm cache. Timings from a laptop or a demo dataset are dominated by JVM warm-up and page cache, so they do not transfer.

Structured Streaming

Enough to show you understand the model, not just the API.

161. What is Structured Streaming?

A stream processing model where a stream is treated as an unbounded table and a query over it is re-run incrementally as data arrives. You write the same DataFrame operations as for batch, and the engine maintains the incremental state.

162. What is the difference between Structured Streaming and the old DStream API?

DStreams are RDD-based micro-batches with their own API. Structured Streaming is DataFrame-based, goes through Catalyst, supports event-time processing and watermarks, and provides end-to-end exactly-once with replayable sources and idempotent sinks. New work should use Structured Streaming.

163. What are the output modes?
Mode Writes
append Only new rows, for queries where existing rows never change
update Rows whose value changed since the last trigger
complete The whole result table every trigger

complete is only valid with aggregations, and it rewrites everything each trigger, so it does not scale to large result tables.

164. What is a trigger, and what are the options?

It controls when a micro-batch runs. The choices are the default, which starts the next batch as soon as the previous finishes; a fixed processing-time interval; availableNow, which processes all available data and stops; and continuous processing, which is experimental and limited.

165. What is event time versus processing time?

Event time is when the record says the event happened; processing time is when the engine saw it. Event time is what correctness depends on for windowed aggregation, and it is why a record arriving late must still be attributed to its original window.

166. What is a watermark and what does it actually do?

It is a threshold on event time that tells the engine how late data may be. It does two things: it finalises windows so their results can be emitted in append mode, and it lets the engine drop the state for windows that can no longer change. Without one, a stateful query’s state grows without bound.

(events
  .withWatermark("started_at", "10 minutes")
  .groupBy(window(col("started_at"), "5 minutes"), col("city_id"))
  .agg(count("*")))
167. Does a watermark do anything on a non-aggregating stream?

Essentially nothing. withWatermark on a simple pass-through that keeps no state has nothing to finalise and no state to drop. Seeing one there is a sign the author copied it from an aggregation example.

168. What is checkpointing in Structured Streaming?

A required location where the engine records offsets processed and the state it holds, so a restarted query resumes exactly where it stopped. Every streaming query needs checkpointLocation, and you cannot share one between two queries.

169. How does Structured Streaming achieve exactly-once?

By combining a replayable source that can be re-read from a recorded offset with an idempotent sink that will not duplicate on retry, plus checkpointed offsets. It is a property of the whole pipeline, not of the engine alone: an at-least-once sink gives at-least-once end to end.

170. What is `foreachBatch` and why is it useful?

A sink that hands you each micro-batch as a normal DataFrame, so you can use batch-only operations such as MERGE into a lakehouse table, or write to several destinations. It is the standard way to drive an upsert from a stream.

171. What happens if you change a streaming query's code and restart it?

It depends on the change. Adding a column or changing a filter is usually safe. Changing the aggregation keys, the output mode, or the nature of the state is not, because the checkpointed state no longer matches the query, and the restart fails or must start from a fresh checkpoint.

172. How do you handle late data?

Set the watermark to the lateness you are willing to tolerate. Data later than that is dropped from stateful aggregations. If you need it, capture it separately, for example by writing the raw stream as well and reconciling in batch.

173. What are stateful operations, and which are they?

Any operation whose result depends on data across triggers: windowed aggregation, deduplication, stream-stream joins, and arbitrary state via flatMapGroupsWithState or the newer transform-with-state API. All of them need a watermark to bound state.

174. What is special about a stream-stream join?

Both sides must buffer rows waiting for a match, so both need watermarks and the join needs a time constraint, or state grows without bound. Outer joins additionally cannot emit a non-match until the watermark proves no match can arrive.

175. How do you monitor a streaming query?

query.lastProgress and query.status give per-batch metrics, including input rows per second, processing rate, and state size. The two numbers that matter are whether processing rate keeps up with input rate, and whether state size is growing without bound, which is the signature of a missing or too-generous watermark.

Deployment, cluster managers and operations

The practical half that people often cannot answer.

176. How do you run Spark on YARN?

--master yarn with --deploy-mode client or cluster. Spark starts an ApplicationMaster which requests containers through YarnAllocator, and each container becomes an executor. HADOOP_CONF_DIR or YARN_CONF_DIR must point at the cluster config so Spark can find the ResourceManager.

177. How do you run Spark on Kubernetes?

--master k8s://https://host:port. The driver runs in a pod and its own ExecutorPodsAllocator creates executor pods through the API server, so there is no Spark-specific cluster-manager daemon. You supply a container image and a service account with permission to create pods.

178. What is the difference between Standalone, YARN and Kubernetes for Spark?
  Standalone YARN Kubernetes
Provided by Spark itself Hadoop Kubernetes
Grants containers Master daemon ResourceManager API server
Good for Simple dedicated clusters Existing Hadoop estates Cloud-native, mixed workloads
179. How do you share a cluster between Spark applications?

Across applications, the cluster manager arbitrates, using queues on YARN or namespaces and quotas on Kubernetes. Within one application, spark.scheduler.mode set to FAIR with pools shares slots between concurrent jobs, which is what you want for a shared notebook or JDBC server.

180. How do you pass configuration to a Spark application, and what is the precedence?

Highest to lowest: values set on SparkConf in code, then flags to spark-submit, then spark-defaults.conf. A properties file can be named with --properties-file, and --load-spark-defaults makes Spark read spark-defaults.conf as well.

181. How do you add third-party libraries?

--packages for Maven coordinates with transitive resolution, --jars for local jars on driver and executor classpaths, and --py-files for Python .zip, .egg or .py. For repeatability in production, bake dependencies into an assembly jar or a container image rather than resolving at submit time.

182. What is the history server and why do you need it?

The application UI dies with the application. Enable event logging with spark.eventLog.enabled and a log directory, and the history server replays those logs so you can investigate a job after it finished. Without it, post-mortem analysis is guesswork.

183. How do you secure a Spark cluster?

Authentication between components with spark.authenticate and a shared secret, encryption in transit for both RPC and shuffle, Kerberos for Hadoop delegation tokens, and cluster-manager-level isolation. Avoid secrets in extraJavaOptions, since the Environment tab of the UI displays configs.

184. How do you route Spark traffic through an HTTP proxy?

JVM proxy properties on both sides, for example -Dhttp.proxyHost, -Dhttp.proxyPort and -Dhttp.nonProxyHosts, passed through spark.driver.extraJavaOptions and spark.executor.extraJavaOptions. It is the same delivery mechanism as any other JVM flag, which is why learning one teaches the rest.

185. What should you monitor in production?

Stage-level task duration skew, shuffle spill, GC time per executor, executor loss and its cause, and for streaming the gap between input and processing rate plus state size. Alert on the structural signals rather than on total job duration, which moves for many unrelated reasons.

Scenario-based questions

These are the ones that separate people who have run Spark in production from people who have read about it. Each is a symptom; the answer is how to reason about it.

186. A job that took 20 minutes now takes 2 hours, and nothing was deployed. How do you investigate?

Nothing changed in the code, so something changed in the data or the cluster.

  1. Compare the physical plan with a previous run. A join that used to broadcast and now does not is the most common cause, because the dimension table grew past the threshold.
  2. Check input size and file count. Ten times the files with the same bytes changes planning cost and partition count.
  3. Check skew: max versus median task time on the slowest stage. A new hot key concentrates work.
  4. Check the executors tab for loss and retries, and for GC time.
  5. Only then look at configs, which did not change.

The usual answer to this scenario is a lost broadcast or a newly skewed key.

187. A stage has 200 tasks; 198 finish in seconds and 2 run for an hour. What do you do?

Classic skew. Confirm on the stage page that the long tasks also have far larger shuffle read. Then:

  • Check whether AQE skew handling could fire. It needs a partition larger than 5 times the median and larger than 256 MB, so on a smaller job it never triggers even with terrible skew.
  • If one side is small, broadcast it and remove the shuffle entirely.
  • Otherwise salt the hot key, or split the hot keys out and union the results.

Do not enable speculation for this: the duplicate task is equally slow and just wastes a slot.

188. Your job writes 50,000 tiny Parquet files. Why, and how do you fix it?

Because output parallelism times partition count equals file count: 200 shuffle partitions writing 250 partition values gives 50,000 files. Fix it by repartitioning on the same columns you partition by, so each value is written by one task:

(df.repartition("city_id")
   .write.partitionBy("city_id")
   .mode("overwrite").parquet(path))

Also consider whether the partition column is too high-cardinality for partitioning at all, and cap file size with spark.sql.files.maxRecordsPerFile.

189. The driver runs out of memory on a job that only aggregates. What happened?

Something is pulling data back. Look for collect(), toPandas(), a broadcast of a side that is not small, or show() on a very wide result. Also consider task count: a job with hundreds of thousands of tasks makes the driver track a lot of metadata. spark.driver.memory defaults to 1g, which is a coordinator’s budget, not a data-processing budget.

190. Executors keep being killed for exceeding memory limits. What is your sequence?
  1. Confirm it is the container total, not the heap: the message comes from the cluster manager, not a JVM OutOfMemoryError.
  2. Raise spark.executor.memoryOverhead first, because off-heap buffers, the Python worker and native libraries live there.
  3. Check for skew, since one huge partition in one task is a common root cause that looks like a memory shortage.
  4. Raise partition count so each task’s share is smaller.
  5. Only then raise executor memory.

Raising memory first often just moves the failure later.

191. A `FetchFailedException` keeps failing your job. What is the real problem?

Almost never the fetch settings. The executor that held those shuffle blocks is gone, was killed for memory, or is too busy to serve. Investigate why it died. An external shuffle service makes shuffle output outlive the executor, which is also what dynamic allocation needs to release executors safely.

192. You have a 2 TB fact table and a 3 GB dimension table to join. How?

3 GB is far past the 10 MB broadcast threshold, so a broadcast is out unless you can reduce it. In order:

  1. Filter and project the dimension side first. If the query only needs two columns and a subset of rows, 3 GB may become broadcastable.
  2. If not, accept a sort-merge join and size the shuffle sensibly.
  3. If this join runs repeatedly, bucket both tables on the join key with the same bucket count, which removes the shuffle permanently.
  4. Handle skew on the join key separately.

Forcing a broadcast hint on 3 GB moves the failure to the driver, which must collect it first.

193. Your streaming query's state grows until the job dies. Why?

A stateful operation without an effective watermark, so nothing is ever old enough to drop. Check that the watermark column is event time from the payload and not the source’s ingest timestamp, that the watermark is actually attached before the aggregation, and that the allowed lateness is not enormous. Confirm with lastProgress, where state rows should plateau rather than climb.

194. A nightly job silently produced half the expected rows. How do you find out why?

Silent wrong results point at semantics, not performance.

  • An inner join dropping non-matching rows, including rows whose key is null.
  • union matching columns by position where the two sides differ in order. Use unionByName.
  • A filter written against a column that is now sometimes null, since col = value is never true for null.
  • On an upgrade to Spark 4, ANSI mode now raising or changing results where invalid casts previously produced null.

Compare row counts at each stage of the pipeline to find where they fall off.

195. You must deduplicate a CDC feed to the latest row per key. How?

A window function is the idiomatic way:

from pyspark.sql import Window
from pyspark.sql.functions import row_number, desc, col

latest = (changes
    .withColumn("rn", row_number().over(
        Window.partitionBy("trip_id").orderBy(desc("updated_at"))))
    .where(col("rn") == 1).drop("rn"))

It shuffles once by key. If the key is skewed, that single partition is your bottleneck, so check the distribution first. For a stream, dropDuplicates with a watermark is the bounded-state equivalent.

196. A query works on a sample but fails at full scale with spill and no progress. What now?

The shape that works at one scale rarely fails at another for a config reason; it fails because partition sizes scaled with the data.

  1. Check partitions: total bytes divided by partition count should land in the low hundreds of megabytes.
  2. Lower spark.sql.adaptive.advisoryPartitionSizeInBytes or raise the shuffle partition count so tasks are smaller.
  3. Look for an aggregation with far higher cardinality at scale than in the sample.
  4. Check whether something cached at sample size no longer fits and is now evicting execution memory.
197. You need to read 10 TB but only one day of it. The job reads everything. Why?

The filter is not pruning. Either the table is not partitioned on the date column, or the predicate is written so it cannot be pushed down, for example by wrapping the column in a function such as to_date(ts) = '...' instead of comparing the partition column directly. Check the plan for PartitionFilters and PushedFilters: if the date predicate appears in neither, nothing is being skipped.

198. Two jobs writing the same table produce corrupt or missing data. What is wrong?

Plain Parquet directories have no commit protocol, so two concurrent writers can interleave and a failure can leave partial output. Either serialise the writers, or move to a table format with atomic commits: Hudi, Iceberg or Delta. That is the structural fix, and it is why lakehouse formats exist.

199. A `groupBy` on a high-cardinality column is unbearably slow. Options?

Ask first whether the aggregation is needed at that cardinality.

  • If it feeds a join, aggregate after joining on a coarser key.
  • If it feeds an approximate answer, use approx_count_distinct rather than exact countDistinct, which is a second shuffle.
  • If output is written partitioned by that column, the cardinality is also a small-files problem.
  • Make sure the shuffle partition count matches the cardinality’s scale so partitions are neither tiny nor huge.
200. Your team wants to move a Spark job's source table from Hudi to Iceberg. What do you consider?

Separate the engine from the table format. Spark is the compute; Hudi and Iceberg are table formats with different strengths, so the questions are about write pattern and read reach:

  • Are the writes keyed upserts at high frequency? Hudi has a record index for that; Iceberg plans a join.
  • Which engines must read it? Iceberg has the widest warehouse support.
  • Who operates the table services, compaction and cleaning, in each case?

If the answer is “both, for different readers”, Apache XTable converts metadata in place without copying data.

How to use this in an actual interview

Three habits matter more than recall.

Say what it depends on. Most Spark questions have a “depends” in them, and naming the dependency is the answer. “Should you cache?” depends on whether the dataset is reused and whether producing it was expensive. An answer that names the condition beats one that picks a side.

Reach for the mechanism, not the knob. “Raise the memory” is a weak answer to a spill question; “the partitions are too large for the memory each task gets, so I would raise parallelism first and check for skew” is a strong one. The knob comes last, after the cause.

Refuse to invent numbers. If you do not know a default, say that you would check the Environment tab or the configuration reference. Interviewers notice confident wrong defaults far more than an honest “I would look it up”, and one invented number makes everything else you said suspect.

Conclusion

Two hundred questions is a lot of surface, but they collapse into a handful of ideas. Work is lazy until an action asks for a result, which is what allows the whole plan to be optimised. Partitions are the unit of parallelism, so nearly every performance question is really about how many you have and how evenly the data sits across them. Shuffles are the expensive thing, and stage boundaries fall exactly where they occur. The driver holds all the state that cannot be recomputed, which is why it is the one process with no redundancy.

If you can derive an answer from those four, you will handle questions this list does not contain, which is the actual goal. The scenario section is where that shows: nobody can memorise the answer to “it was 20 minutes and now it is 2 hours”, because the answer is a way of looking rather than a fact.

References

Trademarks

Apache Spark, Apache Hadoop, Apache Hive, Apache Parquet, Apache Kafka, Apache Hudi, Apache Iceberg, Apache XTable (incubating), Apache Log4j and Apache are either registered trademarks or trademarks of The Apache Software Foundation in the United States and other countries. Delta Lake is a trademark of the Linux Foundation.

Found this useful?

These posts and tools are free. If one saved you an afternoon, you can buy me a coffee.

Buy me a coffee