DataFrames and the Catalyst Optimizer

Why Spark's DataFrame API can be optimized automatically and its older RDD API mostly can't — the difference comes down to whether Spark can see inside what you asked it to do.

Lazy Evaluation and the DAG established that Spark sees the whole chain of transformations before running any of it. This article covers what Spark actually does with that visibility — and why it can only do it for one of Spark's two APIs.

RDDs: the original, low-level API#

Spark's original abstraction, the RDD (Resilient Distributed Dataset), is a distributed collection with almost no structure attached — no schema, no column names, just partitions of arbitrary Python objects transformed by arbitrary functions, the same data parallelism idea MapReduce popularized before Spark, minus MapReduce's requirement to write intermediate results to disk between every stage:

Python
rdd = sc.textFile("orders.csv")parsed = rdd.map(lambda line: line.split(","))amounts = parsed.map(lambda fields: float(fields[2]))

Every map here takes a Python function — lambda line: line.split(",") — that Spark cannot look inside. It is a black box: Spark knows to call it on every row, and nothing about what the function actually does.

DataFrames: structured, and therefore optimizable#

The DataFrame API expresses the same idea in terms Spark's planner can actually understand — named columns, declared types, and a fixed vocabulary of operations (filter, select, groupBy) instead of arbitrary functions:

Python
df = spark.read.csv("orders.csv", header=True, inferSchema=True)amounts = df.select("amount").filter(df.amount > 0)

filter and select are not opaque Python functions — they are structured operations Spark's query planner can inspect, reorder, and optimize, because it fully understands what each one means.

The Catalyst optimizer: the same pipeline as any query planner#

Spark SQL's Catalyst optimizer processes a DataFrame plan through stages that map directly onto Query Performance Fundamentals' parse-plan-optimize-execute model, covered there for a traditional database:

Text
Parse       -> build an initial logical plan from the DataFrame callsAnalyze     -> resolve column names and types against the actual schemaOptimize    -> logical optimizations: push filters below joins, combine               adjacent operations, eliminate unused columnsPhysical plan -> choose join algorithms and execution strategy               (the same hash/sort-merge/broadcast choice covered in               Join Algorithms)

A filter written after a join in the code can be pushed below the join during the optimize stage — evaluated first, against less data, exactly like Predicate and Projection Pushdown describes for a file scan. This only works because Catalyst understands that a filter call means "keep rows matching this condition" — the same call expressed as an opaque rdd.filter(lambda row: ...) blocks this reordering, because Catalyst cannot prove what an arbitrary Python function does without running it.

A Python UDF has the exact same blind spot as RDD

A user-defined function wrapped around DataFrame logic — df.withColumn("x", my_python_function(df.amount)) — reintroduces the opaque-function problem even inside the DataFrame API. Catalyst can optimize everything around the UDF call, but not through it. PySpark Performance Pitfalls covers this in more depth — it is one of the most common ways a DataFrame-API job quietly loses the optimization this article describes.

Common mistakes#

Dropping into the RDD API out of old habit or a Spark tutorial's example#

Most structured data problems are expressible entirely in the DataFrame API today, even on older Apache Spark tutorials still written against RDDs. Reaching for rdd.map() when df.select() would do the same job gives up Catalyst's optimization for no benefit.

Assuming every DataFrame operation is automatically optimized#

A DataFrame call wrapping a Python UDF is still opaque at exactly that point in the plan — the DataFrame API's optimizability is a property of its structured operations, not a guarantee that applies unconditionally to every line of code that happens to use it.

Not knowing which API a piece of legacy code actually uses#

RDD-based code and DataFrame-based code can look superficially similar at a glance. Confirming which API a slow job actually uses is worth doing before assuming Catalyst is even in a position to help.

Further reading#

See also#