🔍 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.