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
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.
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.
$ 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
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().