Data Engineering glossary
What is Predicate Pushdown?
Predicate pushdown is a query optimization that moves filter conditions down to the data source, so rows are discarded while the data is being read instead of afterwards. With Parquet, partitions and table formats, it lets the engine skip whole folders, files or row groups whose statistics show they cannot match the filter.
How it works
Without pushdown, an engine reads every row and then filters. With pushdown, the optimizer hands the filter to the scan, which uses metadata to avoid reading data at all. There are several layers, from cheapest to finest:
- Partition pruning: a filter on a partition column (
country = 'IN') skips whole folders. Spark lists these asPartitionFilters. - File skipping: table formats such as Delta Lake and Iceberg keep min/max statistics per file and skip files that cannot match. Snowflake does the same per micro-partition.
- Row-group skipping: inside each Parquet file, min/max statistics per row group let the reader skip blocks. Spark lists these as
PushedFilters. - Column pruning (projection pushdown): only the selected columns are read, shown as
ReadSchema.
Pushdown only works when the engine can translate the filter. A plain comparison on a column pushes down; a filter wrapped in a Python UDF, a cast on the column, or a function such as upper(col) = 'X' often cannot. And statistics skip only when values are clustered: if every file holds the full range of a column, min/max can never exclude anything, which is why sorting, Z-ordering and clustering matter.
Example
A table partitioned by country. The plan shows the country filter pruning partitions, the amount filter pushed into the Parquet reader, and only two columns read (PySpark 4.2.0).
import re, tempfile
from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.master("local[2]").appName("glossary-pushdown").getOrCreate()
spark.sparkContext.setLogLevel("ERROR")
path = tempfile.mkdtemp() + "/orders"
(spark.range(1_000_000)
.withColumn("country", F.element_at(F.array(*map(F.lit, ["IN", "US", "UK", "DE"])), (F.col("id") % 4 + 1).cast("int")))
.withColumn("amount", F.col("id") % 500)
.write.partitionBy("country").parquet(path))
q = (spark.read.parquet(path)
.filter((F.col("country") == "IN") & (F.col("amount") > 450))
.select("id", "amount"))
scan = [l for l in q._jdf.queryExecution().executedPlan().toString().splitlines() if "FileScan" in l][0]
for part in ["PartitionFilters", "PushedFilters", "ReadSchema"]:
print(re.search(part + r": (\[.*?\]|\S+)", scan).group(0))
print("rows:", q.count())
spark.stop()
PartitionFilters: [isnotnull(country#5), (country#5 = IN)]
PushedFilters: [IsNotNull(amount), GreaterThan(amount,450)]
ReadSchema: struct<id:bigint,amount:bigint>
rows: 24000
Common mistakes
- Wrapping a filter column in a function or cast (
to_date(ts) = '2026-10-01'), which can stop it from being pushed down. - Filtering after a Python UDF or a
collect, when the filter could have run in the scan. - Expecting skipping on a column whose values are spread randomly across files.
- Reading CSV or JSON and expecting the same savings; those formats have no statistics to skip with.

