🔍 Ctrl+K
🟢 Easy

SparkSession — Entry Point

SparkSession is the main entry point for working with Spark DataFrames, SQL, and all Spark functionality. It replaces the older SparkContext and SQLContext.

from pyspark.sql import SparkSession

spark = (
    SparkSession.builder
    .appName("Practice")
    .getOrCreate()
)
💡 Interview Tip
SparkSession is the single entry point for all Spark operations. It encapsulates SparkContext, SQLContext, and StreamingContext.
🟢 Easy

RDD — Resilient Distributed Dataset

RDD is a distributed, fault-tolerant, and immutable collection of objects that can be processed in parallel across a Spark cluster.

rdd = spark.sparkContext.parallelize([1, 2, 3, 4, 5])
print(rdd.collect())  # [1, 2, 3, 4, 5]

Key concepts:

  • RDD is distributed across partitions
  • RDDs are immutable — transformations create new RDDs
  • Actions trigger execution
  • Fault tolerance through lineage
💡 Interview Tip
RDD is the fundamental data structure of Spark. Understanding it helps you grasp how Spark works internally.
🟢 Easy

Creating a DataFrame

data = [
    (1, "Praveen"),
    (2, "Ajay"),
    (3, "James")
]
columns = ["id", "name"]

df = spark.createDataFrame(data, columns)
df.show()
df.printSchema()

Output:

+---+-----+
| id| name|
+---+-----+
|  1|Praveen|
|  2| Ajay|
|  3|James|
+---+-----+

Schema:

root
 |-- id: long
 |-- name: string
🟢 Easy

Reading CSV Files

df = spark.read.csv(
    "/content/data.csv",
    header=True,
    inferSchema=True
)
df.show()
df.printSchema()

header=True — The first row is treated as column names. Without it, Spark generates _c0, _c1, etc.

inferSchema=True — Spark attempts to determine appropriate data types from the data.

💡 Interview Tip
For production pipelines, explicitly defining the schema is often preferable to relying on schema inference.
🟡 Intermediate

Defining Schema Manually

from pyspark.sql.types import (
    StructType, StructField, IntegerType, StringType
)

customer_schema = StructType([
    StructField("customer_id", IntegerType(), True),
    StructField("customer_name", StringType(), True),
    StructField("city", StringType(), True),
    StructField("age", IntegerType(), True),
    StructField("salary", IntegerType(), True)
])

df = spark.createDataFrame(data, customer_schema)

Why use an explicit schema?

  • Better data quality
  • Predictable data types
  • Avoid incorrect schema inference
  • Better production reliability
🟢 Easy

Filtering Data

newdf = df.filter(df["Duration"] > 60)
newdf.show()

# Alternative using col()
from pyspark.sql.functions import col
newdf = df.filter(col("Duration") > 60)

Pandas vs PySpark:

# Pandas
df[df["age"] > 18]

# PySpark
df.filter(col("age") > 18)
🟢 Easy

withColumn — Create or Replace Columns

from pyspark.sql.functions import col

df = df.withColumn(
    "difference_in_pulse",
    col("Maxpulse") - col("Pulse")
)
df.show()
💡 Interview Tip
withColumn creates a new column or replaces an existing one. Use col() for column references.
🟡 Intermediate

GroupBy Aggregations

from pyspark.sql.functions import avg

result = (
    employee_df
    .groupBy("department")
    .agg(avg("salary").alias("avg_salary"))
)
result.show()

Output:

+----------+---------+
|department|avg_salary|
+----------+---------+
|HR        |50000.0  |
|Engineering|65000.0|
+----------+---------+
💡 Interview Tip
GROUP BY reduces multiple rows into aggregated results. PARTITION BY in a window function does NOT reduce rows.
🟢 Easy

Sorting Data

# Ascending
df.orderBy(col("age").asc())

# Descending
df.orderBy(col("age").desc())
🟢 Easy

Removing Duplicates

# Remove complete duplicate rows
new_df = df.dropDuplicates()

# Remove duplicates based on specific column
new_df = df.dropDuplicates(["customerid"])
💡 Interview Tip
dropDuplicates() removes exact row duplicates. Use column-specific deduplication for business key deduplication.
🟡 Intermediate

Joining DataFrames

Example: Orders and Customers

merged_df = orders_df.join(
    customer_df,
    on="customerid",
    how="inner"
)

new_df = merged_df.select("customerid", "amount", "age")
new_df.show()

Pandas equivalent:

pd.merge(df1, df2, on="customerid", how="inner")
🔴 Advanced

Window Functions

Window functions are extremely important for PySpark interviews. Common functions: row_number(), rank(), dense_rank(), lag(), lead().

Second Highest Salary Per Department

Step 1 — Create Window Specification:

from pyspark.sql.window import Window
from pyspark.sql.functions import dense_rank, col

window_spec = (
    Window
    .partitionBy("department")
    .orderBy(col("salary").desc())
)

Step 2 — Apply dense_rank():

ranked_df = employee_df.withColumn(
    "rank",
    dense_rank().over(window_spec)
)

Step 3 — Filter Rank = 2:

second_highest = ranked_df.filter(col("rank") == 2)
second_highest.show()
💡 Interview Tip
Window functions perform calculations across rows related to the current row without reducing the result set. partitionBy creates independent groups, orderBy defines the ranking order.
🟡 Intermediate

Spark SQL

A DataFrame can be registered as a temporary SQL view, then queried using SQL.

employee_df.createOrReplaceTempView("employee")

spark.sql("""
    SELECT * FROM employee
    WHERE department = 'HR'
""").show()

spark.sql("""
    SELECT department, AVG(salary) AS avg_salary
    FROM employee
    GROUP BY department
""").show()

Second Highest Salary Using SQL

spark.sql("""
    SELECT department, employee_name, salary
    FROM (
        SELECT department, employee_name, salary,
            DENSE_RANK() OVER (
                PARTITION BY department
                ORDER BY salary DESC
            ) AS rank
        FROM employee
    )
    WHERE rank = 2
""").show()
🔴 Advanced

User Defined Functions (UDF)

A UDF allows you to apply custom Python logic to Spark columns.

from pyspark.sql.functions import udf
from pyspark.sql.types import StringType

def to_uppercase(name):
    return name.upper()

uppercase_udf = udf(to_uppercase, StringType())

new_df = df.withColumn("uppercase_name", uppercase_udf(col("name")))

UDF vs Built-in Functions

Prefer built-in Spark functions whenever possible instead of Python UDFs because Python UDFs can introduce serialization and execution overhead.

# Instead of:
profit_udf(col("revenue"), col("expenses"))

# Prefer:
col("revenue") - col("expenses")
🟡 Intermediate

Handling Null Values

# Fill nulls with default values
df = df.fillna({"age": 30, "salary": 50000})

# Drop rows containing nulls
df = df.dropna()

# Count nulls by column
from pyspark.sql.functions import col, sum

null_counts = df.select([
    sum(col(c).isNull().cast("int")).alias(c)
    for c in df.columns
])
null_counts.show()
🟡 Intermediate

Creating Conditional Columns

from pyspark.sql.functions import when, col

df = df.withColumn(
    "salary_category",
    when(col("salary") < 50000, "Low")
    .when((col("salary") >= 50000) & (col("salary") < 60000), "Medium")
    .otherwise("High")
)

Logic: salary < 50000 → Low, 50000–60000 → Medium, 60000+ → High

🟡 Intermediate

Cache and Persist

# Cache (MEMORY_ONLY by default)
df.cache()
df.filter(col("age") > 30).show()
df.groupBy("city").count().show()

# Persist with specific storage level
from pyspark import StorageLevel
df.persist(StorageLevel.MEMORY_AND_DISK)

# Remove cached data
df.unpersist()
💡 Interview Tip
Cache is a convenient way of persisting a DataFrame (defaults to MEMORY_ONLY). Persist allows specifying a storage level like MEMORY_AND_DISK.
🟡 Intermediate

Repartition vs Coalesce

# Repartition — full shuffle, can increase or decrease
df = df.repartition(20)

# Coalesce — no shuffle, only reduce partitions
df = df.coalesce(5)

Simple rule: repartition → increase/decrease + shuffle, coalesce → reduce + less shuffle

🔴 Advanced

Broadcast Join in Practice

from pyspark.sql.functions import broadcast

result = orders_df.join(
    broadcast(products_df),
    "product_id",
    "inner"
)

When to use: When one side of the join is small enough to fit in executor memory. This avoids shuffling the large dataset.

🔴 Advanced

ETL Pipeline — Most Important Interview Program

Pipeline: Source → Read → Transform → Validate → Write

from pyspark.sql import SparkSession
from pyspark.sql.functions import lit

spark = SparkSession.builder.appName("ETL").getOrCreate()

# Read
df = spark.read.csv("s3://bucket/raw/data.csv", header=True, inferSchema=True)

# Transform
df = df.dropDuplicates()
df = df.withColumn("city", lit("Chennai"))

# Write
df.write.mode("overwrite").parquet("s3://bucket/transformed/")
💡 Interview Tip
ETL pipelines are the most common pattern in data engineering. Practice this end-to-end flow: Read → Clean → Transform → Validate → Write.
🟡 Intermediate

JDBC — Database Integration

jdbc_url = "jdbc:mysql://localhost:3306/database"
properties = {
    "user": "root",
    "password": "password",
    "driver": "com.mysql.cj.jdbc.Driver"
}

employee_df = spark.read.jdbc(
    url=jdbc_url,
    table="employee",
    properties=properties
)

department_df = spark.read.jdbc(
    url=jdbc_url,
    table="department",
    properties=properties
)

merged_df = employee_df.join(
    department_df,
    employee_df.departmentid == department_df.departmentid,
    "inner"
)
🟡 Intermediate

S3 → Spark → Transformation → S3

source = "s3://my-bucket/raw/data.csv"
destination = "s3://my-bucket/transformed/"

df = spark.read.csv(source, header=True, inferSchema=True)
new_df = df.dropDuplicates()
new_df.write.mode("overwrite").parquet(destination)
spark.stop()
🔴 Advanced

Data Skew — Detection and Solutions

Detect skew:

from pyspark.sql.functions import desc

df.groupBy("key") \
  .count() \
  .orderBy(desc("count")) \
  .show()

Salting technique: Add a random prefix to skewed keys to distribute data evenly.

Original: key=1 → 1,000,000 records

After salting:
1_0 → Partition 1
1_1 → Partition 2
1_2 → Partition 3
1_3 → Partition 4
💡 Interview Tip
I first identify skew by checking the distribution of records by join key. If skewed, I use salting, broadcast joins, or AQE skew handling.
🔴 Advanced

Accumulators

Accumulators allow executors to contribute values that can be read by the driver. Useful for counting bad records during processing.

bad_records = spark.sparkContext.longAccumulator("bad_records")

def validate(row):
    if row.age < 18:
        bad_records.add(1)

print(bad_records.value)
🔴 Advanced

Catalyst Optimizer

Catalyst is Spark SQL's query optimization framework.

Spark Code / SQL
       ↓
Parsed Logical Plan
       ↓
Analyzed Logical Plan
       ↓
Optimized Logical Plan
       ↓
Physical Plan
       ↓
Execution

Key optimizations:

  • Predicate Pushdown — Apply filters as early as possible
  • Column Pruning — Read only required columns
🔴 Advanced

Adaptive Query Execution (AQE)

AQE allows Spark to adapt parts of query execution using runtime information.

Initial Plan
     ↓
Execute
     ↓
Runtime Statistics
     ↓
AQE
     ↓
Adjusted Execution

Key features: coalescing shuffle partitions, changing join strategies, handling skewed joins.

🟡 Intermediate

Using explain() — Interview Essential

# Basic plan
df.explain()

# Detailed plan with all stages
df.explain(True)

Use explain() to inspect whether Spark is using BroadcastHashJoin, SortMergeJoin, Exchange/shuffle, filtering, or projection.

🟡 Intermediate

Interview Coding Checklist

Basic

  • Create SparkSession
  • Create RDD
  • Create DataFrame
  • Read CSV / JSON / Parquet
  • Create manual schema

Transformations

  • filter, select, withColumn
  • groupBy, orderBy, join
  • dropDuplicates, unionByName

Functions

  • col, lit, when
  • sum, avg, max, min, count

Window Functions

  • row_number, rank, dense_rank
  • lag, lead, partitionBy

Advanced

  • UDF, Broadcast Join
  • Cache, Persist, Repartition
  • Data Skew, Salting, AQE
  • explain(), Accumulators

Data Engineering

  • JDBC, S3, ETL pipeline
  • Data validation, Parquet output
  • SCD Type 2, Incremental loading
🔴 Advanced

Production Interview Scenarios

🚨 Scenario 1 — One Join Key Has Millions of Records

Detect:

df.groupBy("key").count().orderBy(col("count").desc()).show()

Solutions: Broadcast join, Salting, AQE skew handling, Better partitioning

🚨 Scenario 2 — 100,000 Small Files

Solutions: Compact files, use appropriate target file size, optimize partitioning, use Parquet/ORC format

🚨 Scenario 3 — Incremental Load

Solutions: Timestamp filtering, File arrival metadata, Glue Job Bookmarks, CDC, S3 events, Ingestion tracking table

🚨 Scenario 4 — Out Of Memory

Possible causes: Large partitions, Data skew, Large broadcast table, Too few partitions, Expensive joins

Solutions: Increase parallelism, Handle skew, Avoid collect(), Review broadcast, Unpersist unused DataFrames, Increase executor memory

🚨 Scenario 5 — 5 GB CSV Output

Avoid: df.coalesce(1).write.csv(...)

Prefer:

df.write.option("maxRecordsPerFile", 1000000).mode("overwrite").csv("s3://bucket/output/")

🚨 Scenario 6 — SCD Type 2

Track historical changes using start_date, end_date, and is_current columns. Old record gets end_date populated and is_current=false. New record gets start_date=current and is_current=true.

🚨 Scenario 7 — Large Fact + Small Dimension

Question: A large fact table needs to be joined with a small dimension table. What join would you consider?

Answer: Broadcast Join — the smaller dataset is broadcast to executor nodes so Spark can avoid shuffling the large dataset.

Python
from pyspark.sql.functions import broadcast

fact_df.join(
    broadcast(dimension_df),
    "customer_id"
)

Provided the dimension is sufficiently small for executor memory.

🚨 Scenario 8 — CSV and JSON in the Same Folder

If the files have different schemas/formats, don't blindly union them. Read each format separately, normalize the schema, then combine.

Python
csv_df = spark.read.csv("path/to/csv_files", header=True)
json_df = spark.read.json("path/to/json_files")

final_df = csv_df.unionByName(
    json_df,
    allowMissingColumns=True
)

Provided the datasets represent compatible business data.

🚨 Scenario 9 — 500 GB Data

Question: We have approximately 500 GB of data. Two large files arrive in S3. How would you optimize processing?

Consider: File format (Parquet/ORC), Partitioning strategy, Parallelism, Join strategy, Shuffle size, Data skew, Bucketing where appropriate, AQE, Predicate pushdown, Column pruning.

For repeated large joins on stable join keys, bucketing can sometimes be useful, but it should not automatically be treated as the solution for every large dataset.

🔴 Advanced

Spark Configuration — Interview Discussion

For a hypothetical workload of around 5 GB, walk through the configuration process:

Text
Input data
   ↓
Estimate partitions
   ↓
Estimate parallelism
   ↓
Choose executor cores
   ↓
Choose executor memory
   ↓
Run
   ↓
Inspect Spark UI
   ↓
Tune

A common starting-point calculation for partitions:

Text
number of partitions ≈ data size / target partition size

But the appropriate partition size depends on: file format, compression, transformation, shuffle, cluster size, workload, and Spark configuration.

Interview Tip
Do not present "5 GB = exactly 40 partitions" as a fixed rule. The right partition count depends on multiple factors.
🔴 Advanced

Example Spark Submit

A typical spark-submit command with common configuration flags:

Bash
spark-submit \
  --num-executors 10 \
  --executor-cores 4 \
  --executor-memory 10g \
  --driver-memory 6g \
  myfile.py

Flag explanations:

  • --num-executors — Number of executor processes.
  • --executor-cores — Number of CPU cores available per executor.
  • --executor-memory — Memory available to each executor.
  • --driver-memory — Memory available to the driver.