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()
)
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
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
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.
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
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)
withColumn — Create or Replace Columns
from pyspark.sql.functions import col
df = df.withColumn(
"difference_in_pulse",
col("Maxpulse") - col("Pulse")
)
df.show()
withColumn creates a new column or replaces an existing one. Use col() for column references.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|
+----------+---------+
GROUP BY reduces multiple rows into aggregated results. PARTITION BY in a window function does NOT reduce rows.Sorting Data
# Ascending
df.orderBy(col("age").asc())
# Descending
df.orderBy(col("age").desc())
Removing Duplicates
# Remove complete duplicate rows
new_df = df.dropDuplicates()
# Remove duplicates based on specific column
new_df = df.dropDuplicates(["customerid"])
dropDuplicates() removes exact row duplicates. Use column-specific deduplication for business key deduplication.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")
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()
partitionBy creates independent groups, orderBy defines the ranking order.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()
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")
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()
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
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()
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
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.
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/")
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"
)
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()
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
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)
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
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.
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.
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
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.
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.
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.
Spark Configuration — Interview Discussion
For a hypothetical workload of around 5 GB, walk through the configuration process:
Input data
↓
Estimate partitions
↓
Estimate parallelism
↓
Choose executor cores
↓
Choose executor memory
↓
Run
↓
Inspect Spark UI
↓
Tune
A common starting-point calculation for partitions:
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.
Example Spark Submit
A typical spark-submit command with common configuration flags:
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.