41. Sample changes on every run
A training sample is built with this Spark code. Two runs on the same data give different rows. Why?
sample = (spark.read.parquet("s3://data/events/")
.filter("country = 'IN'")
.limit(100000))- A.Spark always randomises rows unless cached
- B.filter() changes results based on cluster size
- C.limit without orderBy returns whichever rows arrive first
- D.Spark cannot read Parquet deterministically
Show answer
Answer: C. limit without orderBy returns whichever rows arrive first
Without an explicit ordering, the rows returned by limit depend on partition and task timing. For reproducible samples, sort by a stable key first, or use sample() with a fixed seed on a fixed input version.
42. Which late event is kept
A Spark Structured Streaming job counts events in 5-minute event-time windows with withWatermark("event_time", "10 minutes"). The maximum event time seen so far is 12:31. Two late events now arrive: one at 12:18 and one at 12:23. Which is guaranteed to be counted?
- A.Both events
- B.Neither event
- C.Only the 12:23 event
- D.Only the 12:18 event
Show answer
Answer: C. Only the 12:23 event
The watermark is max event time minus the delay: 12:31 - 10 minutes = 12:21. The 12:23 event is after the watermark, so its window [12:20, 12:25) is still open and it is counted. The 12:18 event belongs to window [12:15, 12:20), which ends before the watermark, so Spark may drop it; data older than the watermark is not guaranteed to be processed.
43. A slow Python UDF in PySpark
A PySpark job applies a row-by-row Python UDF to clean phone numbers and is 10 times slower than the rest of the pipeline. What is the best first fix?
- A.Rewrite it with built-in Spark SQL functions (such as regexp_replace), or a vectorised pandas UDF if logic is complex
- B.Call collect() and clean the data on the driver
- C.Add cache() after the UDF
- D.Increase the number of retries
Show answer
Answer: A. Rewrite it with built-in Spark SQL functions (such as regexp_replace), or a vectorised pandas UDF if logic is complex
Plain Python UDFs move every row between the JVM and Python workers and are opaque to the Catalyst optimiser. Built-in functions run inside the JVM and are optimised. When Python is needed, Arrow-based pandas UDFs process whole batches and cut serialisation overhead.
44. Overwrite deleted the whole table
A backfill job writes one day with df.write.mode("overwrite").partitionBy("dt").parquet(path) on a path-based table. Afterwards only that day exists and all other dates are gone. Why?
- A.With the default static partitionOverwriteMode, overwrite replaces the whole output path; dynamic mode would replace only the partitions present in df
- B.Parquet files expire after one day
- C.partitionBy always deletes other partitions
- D.The job ran out of memory and lost the data
Show answer
Answer: A. With the default static partitionOverwriteMode, overwrite replaces the whole output path; dynamic mode would replace only the partitions present in df
In static mode, which is the Spark default, an overwrite clears all existing partitions under the target. Setting spark.sql.sources.partitionOverwriteMode=dynamic replaces only the partitions being written. Table formats offer safer options such as Delta replaceWhere or Iceberg overwrite by filter.
45. Broadcast hint on the wrong side
A large orders table is left-outer-joined to a small cities table, but the hint is put on orders: broadcast(orders).join(cities, "city_id", "left"). Spark ignores it. Why?
- A.Broadcast hints work only in Spark SQL, not in Python
- B.Spark never broadcasts a table that has a city_id key
- C.Hints are ignored whenever AQE is turned on
- D.A left outer join can only broadcast the right side
Show answer
Answer: D. A left outer join can only broadcast the right side
For a left outer join, every row from the left side must appear even without a match, so Spark streams the left side and can only build a hash table from the right side. A hint on the left side cannot be honoured. Put the broadcast on cities instead, which is also the small table here.
46. JDBC read with lowerBound and upperBound
A JDBC read uses partitionColumn="id", lowerBound=1, upperBound=1000000, numPartitions=10. The table has ids up to 5 million. What happens?
- A.Only rows with ids from 1 to 1,000,000 are read
- B.All rows are read; ids above 1,000,000 go into the last partition
- C.The read fails because ids exceed upperBound
- D.Spark splits 5 million ids evenly into 10 partitions
Show answer
Answer: B. All rows are read; ids above 1,000,000 go into the last partition
lowerBound and upperBound only decide the stride of the ranges; they do not filter. The first and last partitions are open-ended, so every id above 1,000,000 lands in the last partition and makes it huge. Set the bounds from the real MIN and MAX of the column.
47. Unified memory for a 10 GB heap
An executor has a 10 GB heap (10,240 MB). Spark reserves 300 MB, and spark.memory.fraction is 0.6. About how much unified memory (execution plus storage) does it get?
- A.About 6,144 MB
- B.About 3,072 MB
- C.About 5,964 MB
- D.About 9,940 MB
Show answer
Answer: C. About 5,964 MB
Unified memory = (heap - 300 MB reserved) x 0.6 = (10,240 - 300) x 0.6 = 9,940 x 0.6 = 5,964 MB. Execution and storage share this pool and can borrow from each other. The remaining 40% is user memory for your own objects and Spark internals.
48. Container killed for exceeding memory
A PySpark job on YARN fails with "Container killed by YARN for exceeding memory limits". The JVM heap usage in the UI looks fine. What should you raise first?
- A.spark.sql.shuffle.partitions
- B.spark.driver.maxResultSize
- C.spark.memory.storageFraction
- D.spark.executor.memoryOverhead
Show answer
Answer: D. spark.executor.memoryOverhead
YARN kills the whole container when total process memory goes over its limit, including off-heap memory, network buffers and Python worker processes, which all live outside the JVM heap. That non-heap space is set by memoryOverhead (default 10% or 384 MB). Raising heap does not help if the overhead is what overflows.
49. Adding 300 columns with withColumn
Code adds 300 derived columns with a for loop that calls df = df.withColumn(...) each time. The job spends minutes before any task starts. What is the better pattern?
- A.Cache the DataFrame after every withColumn call
- B.Use more shuffle partitions for the job
- C.Build all expressions and add them in one select()
- D.Run the loop on the executors with mapPartitions
Show answer
Answer: C. Build all expressions and add them in one select()
Each withColumn creates a new projection in the plan, so 300 calls create a deep plan that the analyzer and optimizer must walk again and again. Building a list of column expressions and calling select once, or withColumns with a dict in Spark 3.3+, creates one projection.
50. startingOffsets changed but ignored
A Structured Streaming query reads Kafka with a checkpoint. You change startingOffsets from "latest" to "earliest" and restart to reprocess history. Nothing old is read. Why?
- A.Kafka deletes old data whenever a consumer restarts
- B.On restart, offsets come from the checkpoint, not startingOffsets
- C.earliest is only allowed for batch reads
- D.Spark needs the topic to be recreated first
Show answer
Answer: B. On restart, offsets come from the checkpoint, not startingOffsets
startingOffsets applies only when a query starts with no checkpoint. After that, the offsets saved in the checkpoint decide where to resume, which is what gives exactly-once recovery. To reprocess, start with a new checkpoint location, and make sure the sink can handle the replay.
51. Stream-stream join running out of memory
A streaming join of ad impressions to clicks runs for days and its state store keeps growing until executors fail. What is missing?
- A.A larger trigger interval such as 10 minutes
- B.A cache() call on both input streams
- C.Watermarks plus a time bound in the join condition
- D.Complete output mode for the join query
Show answer
Answer: C. Watermarks plus a time bound in the join condition
Without a watermark and an event-time range in the join condition, Spark must keep every past row in case a future match arrives, so state grows forever. With them, for example clicks within 1 hour of an impression, Spark can drop old state. This bounds memory at the cost of ignoring very late events.
52. Pruning a fact table through a join
A fact table partitioned by date_key is joined to a date dimension filtered to one quarter. The query has no direct filter on the fact table, yet Spark reads only about 90 date partitions. What did this?
- A.Static partition pruning from the WHERE clause
- B.Dynamic partition pruning using the dimension filter
- C.Column pruning in the Parquet reader
- D.AQE coalescing the shuffle partitions
Show answer
Answer: B. Dynamic partition pruning using the dimension filter
With dynamic partition pruning, Spark first runs the filtered dimension side and uses its join keys as a runtime filter on the fact table scan. Only matching partitions are read. It works best when the dimension side is small enough to broadcast.
53. A broadcast hint on 8 GB
Someone adds broadcast() to an 8 GB table to speed up a join. The job now fails with driver out-of-memory or broadcast timeout errors. Why?
- A.Broadcast data is written to S3 and read back slowly
- B.Spark refuses to broadcast tables with string columns
- C.The table is gathered on the driver, then sent to every executor
- D.The hint forces the other table to be broadcast as well
Show answer
Answer: C. The table is gathered on the driver, then sent to every executor
A broadcast join builds the small side on the driver and ships a full copy to each executor, which must hold it in memory. That only works for tables up to a few hundred MB, and Spark caps broadcasts at 8 GB. For two large tables, use sort-merge join, bucketing or pre-aggregation.
54. Half-written files in a stream output
A streaming file sink writes Parquet to a folder. A Presto job reading the folder directly sometimes sees files from a failed batch. Spark readers do not. Why?
- A.Presto reads files faster than Spark writes them
- B.The failed batch was written by the driver
- C.Spark reads the _spark_metadata log; Presto just lists files
- D.Spark hides any file smaller than one MB
Show answer
Answer: C. Spark reads the _spark_metadata log; Presto just lists files
The file sink records each committed batch in a _spark_metadata log, and Spark readers use it to see only committed files, which gives exactly-once output. Other engines just list the folder and can see leftover files from failed attempts. Writing to Delta or Iceberg gives every engine a proper commit log.
55. Using spot instances for Spark
To cut cloud cost by about 60%, a team plans to move a nightly Spark cluster to spot instances. Which set-up is the usual safe choice?
- A.Driver on spot, executors on on-demand
- B.Driver on on-demand, executors mostly on spot
- C.Everything on spot with speculation turned off
- D.Everything on spot with one large executor
Show answer
Answer: B. Driver on on-demand, executors mostly on spot
Losing the driver kills the whole application, so it should run on stable on-demand capacity. Losing a spot executor only costs recomputing its tasks and shuffle output, which Spark handles automatically. Graceful decommissioning can also migrate shuffle and cache blocks when spot nodes get a warning.
56. Which partition AQE treats as skewed
AQE skew join is on with default settings (skewedPartitionFactor 5, skewedPartitionThresholdInBytes 256 MB). The median shuffle partition is 40 MB. Which partition will AQE split?
- A.A 180 MB partition
- B.A 240 MB partition
- C.A 300 MB partition
- D.None of these sizes
Show answer
Answer: C. A 300 MB partition
A partition counts as skewed only if it is larger than 5 × median (200 MB) AND larger than 256 MB. 180 MB fails both tests and 240 MB passes the first but not the second. 300 MB passes both, so AQE splits it into smaller tasks.
57. Cost of salting a skewed join
One merchant_id holds 40% of a 2 billion row fact table. You salt the fact side with a random salt 0–7 and join to a 2 million row merchant table. How many rows must the merchant side have after salting?
- A.2 million, unchanged
- B.16 million
- C.250,000
- D.8 million
Show answer
Answer: B. 16 million
Each fact row gets one of 8 salts, so every merchant row must exist once per salt to still match. The merchant side is exploded 8 times: 2 million × 8 = 16 million rows. This is the price of salting, so salt only the hot keys when the small side is large.
58. Rows produced by cube on two columns
Sales data has 3 cities and 4 channels, and every city-channel pair has sales. How many rows does df.cube("city", "channel").sum("amount") return?
- A.12
- B.16
- C.19
- D.20
Show answer
Answer: D. 20
cube produces every grouping set: (city, channel) gives 12 rows, (city) gives 3, (channel) gives 4, and the grand total gives 1. That is 12 + 3 + 4 + 1 = 20. rollup would skip the channel-only level and return 16.
59. Time travel fails after VACUUM
A Delta table is updated daily. After VACUUM runs with the default retention, a query using VERSION AS OF a 30-day-old version fails. Why?
- A.VACUUM deleted data files only that old version used
- B.VACUUM deletes the transaction log for all versions
- C.Time travel only works for the last 3 versions
- D.VERSION AS OF needs the table to be cached
Show answer
Answer: A. VACUUM deleted data files only that old version used
VACUUM removes data files no longer referenced by the current table and older than the retention period (7 days by default). The log still lists the old version, but its files are gone, so reading it fails. Keep retention long enough for any time travel or audit you need.
60. Streaming state causing long GC pauses
A Structured Streaming dedup query keeps state for 50 million keys. Executors show long GC pauses and batches slow down. Which change targets the cause?
- A.Switch the output mode from append to complete
- B.Use the RocksDB state store provider
- C.Lower spark.sql.shuffle.partitions to 10
- D.Remove the watermark so state is never dropped
Show answer
Answer: B. Use the RocksDB state store provider
The default state store keeps state as JVM objects on the heap, so huge state means heavy garbage collection. The RocksDB provider keeps state in native memory and local disk, outside the JVM heap. A watermark is still needed so old keys are eventually dropped.