Skip to content
About
Main content

Pushdown: negotiation and the ladder

When is pushdown wrong?

Pushdown capability negotiation: the interface is the limit

This item belongs at the top of the article, because it governs everything below it. A data source must be asked what filters it accepts. The contract must also say what it did not handle, so the engine can keep a and apply it itself. Spark's SupportsPushDownFilters.pushFilters() makes this contract explicit: it returns the subset of filters that the source could not handle. That returned set is the interface's vocabulary in machine-readable form.

Different sources speak different languages:

  • a JDBC source (a relational database reached over the network) accepts SQL;
  • a REST API accepts whatever its query parameters can express;
  • a file format accepts comparisons against its stored .

The 's reach ends exactly where that vocabulary ends. No amount of planning cleverness can extend it.

Here is a small pseudo-code of the contract:

accepted, residual = source.push_filters([f1, f2, f3])
# 'accepted' filters are applied inside the source.
# 'residual' filters MUST still be applied by the engine above the scan.

If an engine forgot the residual part, a source that ignores a filter would return wrong results. So "pushed" really means "the source promised to apply it".

In one sentence: the vocabulary of the interface below is the ceiling on this whole article, and a correct contract always returns the filters that were not handled.


Predicate pushdown: a ladder, not a switch

Pushdown is usually described as yes or no. In fact it is a ladder, and each rung saves a different thing:

DepthWhere the predicate is evaluatedWhat it saves
0A filter operator above the scanOnly the work of operators above it
1Inside the scan operatorBuilding row tuples that will be thrown away
2On encoded data (dictionary codes, run-length runs)Decoding CPU
3Against page or row-group statisticsI/O
4Against partition values or manifestsI/O and listing of metadata
5Sent to a remote systemNetwork transfer and remote compute

Depth 2 is the least visible and often the largest CPU win. Two terms: with dictionary encoding, a column stores each distinct string once, and the rows store small integer codes. With run-length encoding (RLE), a repeated value is stored as (value, count). Under , c = 'US' becomes one dictionary lookup that returns a code, and then comparisons of integers. Under RLE the comparison runs over runs, not rows: a column with 10⁶ rows and 200 runs is filtered with 200 comparisons. The predicate never touches a string. This is the mechanism behind most of the folklore that "columnar engines are fast". It is a pushdown effect, not an execution effect.

Four cases where pushdown is wrong

The folklore says pushdown always helps. It does not.

1. Expensive predicates. Pushing a user-defined function (UDF) below a join evaluates it on more rows than the join would have produced. A derivable rule: to minimize the expected cost of a set of AND-ed conditions, evaluate condition i before condition j if and only if

the fraction of rows that pass
the cost to evaluate it once

where s is the fraction of rows that pass () and c is the cost to evaluate it once. This ranks conditions by "rows removed per unit of cost". It is the same ordering structure as scheduling jobs by value over work.

2. Outer joins. A predicate on the null-supplying side of an outer join (the side that gets for unmatched rows) cannot be pushed below the join. The null-extended rows have not been produced yet, so the meaning would change. This is the property from the planning articles, used in the opposite direction.

3. Aggregation. A predicate on a grouping key can be pushed below the aggregate. A predicate on an aggregate result cannot. That is the whole mechanical meaning of WHERE versus HAVING: one clause is the pushable half and the other is the residual. SQL makes you name them because the optimizer could not always tell.

4. Federated sources. If you push a predicate to a remote system that has no index, your cheap filter becomes a full scan on their side, billed to them, on their latency.

In one sentence: pushdown is a ladder whose rungs save decode, I/O, listing and network in turn; the rung you reach is set by the interface's vocabulary; and the order of conditions follows selectivity-gain per unit cost.