Skip to content
ExponaBLOG
APACHE SPARK & DATA ENGINEERING · DAY 005 / 100

RDDs vs DataFrames vs Datasets: best practices explained

Which Spark API to use.

Expona Admin7 min read

Introduction

When you start working with Apache Spark, one of the first decisions you'll face is which API to use: RDDs, DataFrames, or Datasets. Each has different performance characteristics, type safety guarantees, and use cases. Understanding the trade-offs between RDDs vs DataFrames vs Datasets: best practices explained will help you write faster, more maintainable Spark applications from day one.

Over the past five days, we've covered Spark fundamentals and cluster basics. Today we're diving into the APIs themselves—not just their syntax, but when and why to use each one. This is the kind of knowledge that separates production code from experimental scripts.

RDDs: Low-Level, Rarely Needed Today

RDDs (Resilient Distributed Datasets) were Spark's original abstraction layer. They're immutable, distributed collections of objects that can be processed in parallel. If you're coming from Hadoop or older Spark codebases, you'll recognize them immediately.

RDDs give you the most control. You can apply arbitrary Python or Scala functions to every element, chain transformations, and handle complex data shapes that don't fit into tabular form. They're also the foundation upon which DataFrames and Datasets are built.

However, RDDs come with a significant cost: Spark can't optimize them. The optimizer (Catalyst) can't see inside your lambda functions. There's no way for Spark to push down filters, reorder operations, or use columnar storage. For large datasets, this means RDDs are often 5-10x slower than the equivalent DataFrame operation.

Here's a basic RDD example:

python

# Create RDD from a list
rdd = sc.parallelize([1, 2, 3, 4, 5])

# Transform with map
squared = rdd.map(lambda x: x * x)

# Filter
filtered = squared.filter(lambda x: x > 10)

# Collect results
result = filtered.collect()
print(result)  # [16, 25]

That code works, but imagine doing this on a 10TB dataset split across hundreds of files. Spark has no idea that you're filtering out most rows before squaring. It can't optimize the execution plan. It can't use Parquet's predicate pushdown. It can't leverage columnar caching.

When should you still use RDDs? Rarely. Some scenarios include:

  • Unstructured data where rows have wildly different schemas (though even then, you can often normalize into a schema)
  • Complex custom partitioning logic that DataFrame partitioning can't express
  • Legacy code migration where the cost of rewriting isn't justified
  • Machine learning pipelines using MLlib's RDD-based API (though DataFrame-based ML is now standard)

For 99% of new development, you should reach for DataFrames or Datasets instead. The performance difference is substantial, and the API is just as expressive for most tasks.

DataFrames: The Optimized Default Choice

DataFrames are the workhorse of modern Spark. They're RDDs wrapped with structure—a schema that tells Spark what columns exist, their types, and their order. This structure is everything. It unlocks the Catalyst optimizer.

When you write a DataFrame operation, Spark parses your logic into a logical execution plan, then applies optimization rules. It can eliminate unnecessary columns before reading, push filters down to Parquet readers, combine operations, and generate optimized bytecode. The result is typically 10-50x faster than equivalent RDD code.

Here's the same squaring-and-filtering task as a DataFrame:

python

from pyspark.sql import SparkSession

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

# Create DataFrame from list of tuples
data = [(1,), (2,), (3,), (4,), (5,)]
df = spark.createDataFrame(data, ["value"])

# Transform and filter
result = df.withColumn("squared", df.value * df.value) \
    .filter(df.squared > 10) \
    .select("squared")

result.show()
# +-------+
# |squared|
# +-------+
# |     16|
# |     25|
# +-------+

Spark sees this as a logical plan: create, add column, filter, select. It optimizes by reordering or eliminating steps. If you were reading from Parquet, it would push the filter down so the file reader only materializes matching rows.

DataFrames work in Python, Scala, SQL, and R. They integrate seamlessly with Spark SQL—you can write the same code as a query string or using the fluent API. They support all common operations: joins, aggregations, window functions, and more.

The main trade-off is that DataFrames use row-based encoders by default in Python. You lose some compile-time type safety compared to Datasets in Scala. But this is a minor concern for most teams, especially those using Python.

Datasets: Typed API in Scala and Java

Datasets provide a middle ground: the performance of DataFrames with the type safety of strongly-typed languages. They're only available in Scala and Java—Python doesn't have Datasets because Python's type system isn't expressive enough at compile time.

A Dataset is a DataFrame with an attached schema class. When you define a case class in Scala, Spark can encode and decode rows type-safely. The optimizer still runs. You still get performance benefits. But you also get compile-time guarantees that your column names are correct.

Here's a Scala Dataset example:

python

case class Number(value: Int)

val data = Seq(Number(1), Number(2), Number(3), Number(4), Number(5))
val ds = spark.createDataset(data)

val result = ds
  .map(n => n.value * n.value)
  .filter(_ > 10)

result.show()
// +-----+
// |value|
// +-----+
// |   16|
// |   25|
// +-----+

Notice the map function. In this case, Spark can't optimize through it because it's user code. For that reason, prefer withColumn and filter operations over map when possible—they can still be optimized.

Datasets shine in scenarios like:

  • Scala or Java codebases where type safety matters across organizational code standards
  • Complex domain models where compile-time verification prevents bugs
  • Teams working across both Scala services and Spark pipelines who want consistent typing

If you're on Python or don't have strong typing requirements, DataFrames offer 95% of the benefits with simpler code.

Spark SQL Works on the Same Engine

Here's something that surprises new Spark developers: Spark SQL queries run on the same Catalyst optimizer as DataFrames. When you write this:

python

spark.sql("SELECT squared FROM (SELECT value * value as squared FROM numbers WHERE squared > 10)")

Spark parses it into the same logical plan as the DataFrame equivalent. The performance is identical. SQL and DataFrames are just two syntaxes for the same underlying engine.

This matters because it means you can mix them freely. Read data as a DataFrame, register it as a temp table, run SQL queries, and pipe results back to DataFrames without performance penalties:

python

# Create and register
df = spark.createDataFrame([(1,), (2,), (3,)], ["value"])
df.createOrReplaceTempView("numbers")

# Query with SQL
result_sql = spark.sql("SELECT value * value as squared FROM numbers WHERE (value * value) > 10")

# Continue with DataFrame API
final = result_sql.filter(result_sql.squared < 20)
final.show()

The key insight: choose SQL or DataFrames based on team preference and readability, not performance. Use SQL when the transformation logic is complex and SQL readers are on your team. Use DataFrames when you need to integrate with Python code or want programmatic construction. Use Datasets in Scala when type safety justifies the stricter syntax.

Common Mistakes

Mistake 1: Using RDDs for structured data. If your data has a schema (columns, types), use DataFrames. Period. There's no scenario where RDDs are faster. You're only losing optimizations.

Mistake 2: Calling collect() on large datasets. This applies to all APIs. Pulling 1GB of data into driver memory will crash your job. Use take(), limit(), or write to storage instead.

Mistake 3: Using map() and filter() on Datasets when withColumn() and filter() would work. User-defined map functions can't be optimized through. Use SQL-style operations when possible.

Mistake 4: Ignoring schema definitions. Always explicitly define schemas for DataFrames, especially when reading from loose formats like JSON or CSV. Schema inference is convenient but prone to type mismatches on production data.

Mistake 5: Mixing Python DataFrames with Datasets without understanding the trade-offs. Python doesn't have compile-time types. If you want type safety, use Scala or Java. Don't force it into Python.

Key Takeaways

  • Use DataFrames by default for new Spark applications. They offer the best balance of performance, ease of use, and compatibility across languages.
  • Avoid RDDs unless you're working with genuinely unstructured data or maintaining legacy code. You're sacrificing 10-50x performance for no benefit in 99% of cases.
  • Consider Datasets in Scala or Java when compile-time type safety aligns with your codebase standards and team practices.
  • SQL and DataFrames compile to the same execution plan. Choose based on readability and team expertise, not performance.
  • Schema matters. Define explicit schemas for DataFrames to enable all optimizations and catch type errors early.
  • Optimize through SQL-style operations like withColumn, filter, and join rather than user-defined map functions when possible.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles