1.1 Complex PySpark Transformations & DataFrame API

Key Takeaways

  • Pandas UDFs use PyArrow for zero-copy serialization, executing operations on Arrow batches which provides 10-100x performance improvements over standard row-by-row PySpark UDFs.
  • Window functions like row_number(), rank(), and dense_rank() require an orderBy clause in the window specification, whereas aggregate functions over windows do not.
  • The rowsBetween specification defines a frame relative to the current row by position, while rangeBetween defines a logical boundary based on the order column's actual values.
  • When handling complex nested types, explode flattens an ArrayType into separate rows, while posexplode generates an additional column indicating the original array position.
  • Higher-order functions like transform and filter process ArrayType elements natively in the JVM without the serialization overhead of UDFs.
Last updated: July 2026

1.1 Complex PySpark Transformations & DataFrame API

The PySpark DataFrame API is the primary interface for data manipulation in Databricks. For the Databricks Certified Data Engineer Professional exam, candidates must demonstrate a profound understanding of complex transformations, advanced windowing logic, nested data manipulation, and performance-optimized user-defined functions. The exam itself consists of 59 scored questions (and possibly some unscored ones) over a 120 minutes time limit. The standard $200 fee applies for registration. This section provides an in-depth exploration of these core topics to ensure you are thoroughly prepared for the rigorous questions on the exam.

Understanding Window Functions in Depth

Window functions perform calculations across a set of rows that are related to the current row. Unlike aggregate functions (like sum or count) which collapse rows, window functions retain the original rows and append the calculated result as a new column. This is incredibly powerful for time-series analysis, deduplication, and ranking.

Anatomy of a Window Specification

A Window specification typically consists of three components:

  1. partitionBy: Divides the data into logical groups. The window function is applied independently within each partition. If omitted, the window spans the entire dataset, which can lead to severe performance issues due to all data being shuffled to a single partition.
  2. orderBy: Defines the sorting order within each partition. This is mandatory for ranking functions and defines the logical sequence of rows.
  3. Frame Specification: Defines the subset of rows relative to the current row to be included in the calculation.
from pyspark.sql.window import Window
from pyspark.sql.functions import row_number, col

# Example of a window specification
window_spec = Window.partitionBy("department").orderBy(col("salary").desc())
df.withColumn("rank", row_number().over(window_spec))

Ranking and Analytic Functions

Ranking functions such as row_number(), rank(), and dense_rank() are heavily tested on the exam.

  • row_number(): Assigns a unique, sequential integer to each row within a partition.
  • rank(): Assigns a rank, but leaves gaps in the ranking if there are ties (e.g., 1, 2, 2, 4).
  • dense_rank(): Similar to rank, but leaves no gaps (e.g., 1, 2, 2, 3).
  • percent_rank(): Evaluates the relative rank of a row within a window partition.

Crucially, all ranking functions require an explicit orderBy clause in the Window specification. If omitted, Spark will throw an AnalysisException.

Analytic functions like lead() and lag() allow you to access data from subsequent or previous rows without a self-join. This is essential for calculating month-over-month changes or detecting session boundaries.

Window Frames: rowsBetween vs rangeBetween

Understanding the difference between rowsBetween and rangeBetween is a common exam topic.

  • rowsBetween: This frame boundary is based strictly on the physical row position relative to the current row. For example, Window.rowsBetween(Window.unboundedPreceding, Window.currentRow) aggregates from the start of the partition up to the current row.
  • rangeBetween: This frame boundary is logical and based on the actual values of the orderBy column. For example, if you order by a numeric timestamp, Window.rangeBetween(-3600, 0) includes all rows where the timestamp is within the last hour (3600 seconds) of the current row's timestamp, regardless of how many rows that actually is.

Handling Nested Structs and Arrays

Modern data lakes frequently store semi-structured data in JSON or Parquet formats. In Spark, these translate to StructType and ArrayType.

Flattening Arrays: explode and posexplode

When you have an array column and want to create a separate row for each element, you use the explode function. A critical detail is that explode drops rows where the array is empty or null. If you must retain these rows, you should use explode_outer, which populates the element column with null.

The posexplode function is a variation that returns two columns: the position (index) of the element in the array and the element's value. This is useful when the order of items in an array carries semantic meaning. If you need to keep outer rows with position, use posexplode_outer.

Managing Structs

StructTypes act like dictionaries or nested rows. You can extract fields using dot notation (e.g., col('address.city')). If you have an array of structs, the inline function is extremely efficient: it both explodes the array and expands the struct fields into top-level columns simultaneously.

from pyspark.sql.functions import explode, col, inline

# Exploding an array of structs
df.select(explode(col("items")).alias("item"))

# Inlining an array of structs
df.selectExpr("inline(items)")

Higher-Order Functions

Historically, manipulating arrays in Spark required exploding the array, applying transformations, and then grouping back together. This causes massive shuffle overhead. Higher-order functions execute natively in the JVM and process arrays in-place.

  • transform: Applies a function to every element of an array, returning a new array.
  • filter: Returns an array containing only elements that evaluate to true for a given predicate.
  • aggregate: Reduces an array to a single value by applying a binary operator to a running state and each element.
  • exists: Checks if at least one element in the array satisfies a predicate.

Using higher-order functions over UDFs is a best practice for both performance and code cleanliness. These native functions avoid the costly serialization steps required by Python UDFs.

PySpark UDFs vs Vectorized Pandas UDFs

User-Defined Functions (UDFs) allow you to apply custom Python logic to a DataFrame. However, traditional PySpark UDFs are notorious for performance bottlenecks.

The Problem with Standard PySpark UDFs

When a standard Python UDF is invoked, Spark must serialize the data row-by-row from the JVM, send it via Py4J to a Python worker process, evaluate the function, and serialize the result back to the JVM. This serialization overhead dominates the execution time.

Pandas UDFs to the Rescue

Pandas UDFs (also known as Vectorized UDFs) solve this problem by leveraging Apache Arrow. PyArrow provides zero-copy serialization, allowing Spark to transfer data in large batches rather than row-by-row. The Python worker then processes these batches using highly optimized C-backed libraries like Pandas and NumPy.

This results in performance improvements ranging from 10x to 100x over standard UDFs.

Types of Pandas UDFs

  1. Series to Series: Takes one or more pandas Series as input and returns a pandas Series of the same length. This is ideal for scalar operations.
  2. Iterator of Series to Iterator of Series: Allows you to initialize expensive state (like loading an ML model or opening a database connection) just once per partition, rather than once per row or batch.
  3. Grouped Map: Used in conjunction with groupBy().applyInPandas(). It passes each group as a pandas DataFrame and returns a new pandas DataFrame. This is useful for complex grouped operations, like training a distinct machine learning model for each user.
  4. Grouped Aggregate: Computes an aggregate over a grouped pandas DataFrame.

When defining a Pandas UDF that returns a complex structure, you must explicitly define the output schema using a StructType. Understanding when and how to implement these optimizations is critical for passing the Databricks Professional exam.

import pandas as pd
from pyspark.sql.functions import pandas_udf
from pyspark.sql.types import DoubleType

@pandas_udf(DoubleType())
def vectorized_math(s: pd.Series) -> pd.Series:
    return s * 1.5 + 2.0

df.withColumn("new_val", vectorized_math(col("val")))

By leveraging these advanced techniques, you can ensure that your PySpark applications are highly scalable, maintainable, and cost-efficient.

Test Your Knowledge

Which window frame specification is based on the actual values of the ORDER BY column rather than row position?

A
B
C
D
Test Your Knowledge

What underlying technology gives Pandas UDFs a significant performance advantage over standard PySpark UDFs?

A
B
C
D
Test Your Knowledge

Which PySpark function flattens an array column into multiple rows while also providing a separate column for the original element index?

A
B
C
D
Test Your Knowledge

When using standard PySpark UDFs versus Pandas UDFs, what happens when a standard PySpark UDF is invoked?

A
B
C
D