Spark SQL can change a physical plan after shuffle and join statistics are available, which matters when a query plan does not match the data shape seen at runtime. Adaptive Query Execution, or AQE, lets Spark re-optimize SQL and DataFrame work while the job runs instead of relying only on the first compiled plan.
AQE is controlled by Spark SQL configuration, with spark.sql.adaptive.enabled as the umbrella switch. Apache Spark enables it by default in Spark 3.2.0 and later, but setting it explicitly is useful when a cluster profile, notebook session, or application wrapper may have changed the default.
Set AQE at the layer that starts the application, then verify it inside the same session that runs the query. The local PySpark check uses a small shuffle-heavy query so the active settings, adaptive plan marker, and final row result are visible from one command.
Related: How to set Spark SQL shuffle partitions
Related: How to explain a Spark DataFrame query plan
Related: How to configure Spark defaults
Steps to configure Spark adaptive query execution:
- Choose the configuration surface that owns the Spark application.
Use the SparkSession builder when application code creates the session, spark-submit --conf when an operator starts the job, or spark-defaults.conf when every job from the same client should inherit the setting.
- Add the AQE settings before the SparkSession is created.
- aqe-check.py
from pyspark.sql import SparkSession from pyspark.sql.functions import col spark = ( SparkSession.builder.appName("sg-aqe-check") .config("spark.sql.adaptive.enabled", "true") .config("spark.sql.adaptive.coalescePartitions.enabled", "true") .config("spark.sql.shuffle.partitions", "8") .getOrCreate() ) spark.sparkContext.setLogLevel("ERROR") orders = spark.range(0, 1000).select( (col("id") % 8).alias("customer_id"), (col("id") * 10).alias("amount"), ) customers = spark.range(0, 8).select(col("id").alias("customer_id")) result = ( orders.join(customers, "customer_id") .groupBy("customer_id") .count() .orderBy("customer_id") ) rows = result.collect() print(f"spark.sql.adaptive.enabled = {spark.conf.get('spark.sql.adaptive.enabled')}") print( "spark.sql.adaptive.coalescePartitions.enabled = " f"{spark.conf.get('spark.sql.adaptive.coalescePartitions.enabled')}" ) print(f"spark.sql.shuffle.partitions = {spark.conf.get('spark.sql.shuffle.partitions')}") result.explain() print(f"row_count = {len(rows)}") print("first_rows = " + ", ".join(f"{row.customer_id}:{row['count']}" for row in rows[:3])) spark.stop()
Keep spark.sql.shuffle.partitions=8 only for this local check. Use a workload-sized shuffle partition value for production jobs.
- Run the AQE check script.
$ python aqe-check.py spark.sql.adaptive.enabled = true spark.sql.adaptive.coalescePartitions.enabled = true spark.sql.shuffle.partitions = 8 == Physical Plan == AdaptiveSparkPlan isFinalPlan=true +- == Final Plan == ResultQueryStage 3 +- *(4) Sort [customer_id#1L ASC NULLS FIRST], true, 0 +- AQEShuffleRead coalesced ##### snipped ##### row_count = 8 first_rows = 0:125, 1:125, 2:125 - Confirm the explain output shows an adaptive final plan after the action runs.
AdaptiveSparkPlan isFinalPlan=true means Spark printed the final adaptive physical plan after collect() materialized the result. AQEShuffleRead coalesced shows AQE coalesced shuffle reads for this small check. If an ordinary physical plan appears instead, set spark.sql.adaptive.enabled before creating the SparkSession and rerun the query action before calling explain().
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.