advanced24 min studyLesson 287 in Engineering

Predicate pushdown and late materialization

Reduce intermediate rows and delay wide-value reconstruction while preserving predicate semantics, identity, multiplicity, and visibility.

On this page

What you’ll understand

  • Distinguish fewer downstream rows from fewer storage bytes read.
  • Calculate an explicit late-fetch byte model at a declared boundary.
  • Explain why position identity, snapshots, and expression semantics constrain pushdown.

A selective condition is valuable when it prevents unnecessary work. Predicate pushdown moves eligible testing nearer its input, while late materialization delays reconstructing wide output rows until their identities are known to survive. The two ideas complement one another but do not imply that every pushed filter avoids storage reads or that collecting row positions is always cheaper than carrying values.

Push a predicate only across a legal boundary

A filter near a scan may reduce rows reaching a join or aggregate even if the scan still reads every relevant page. An index condition or storage-level pruning can avoid different work: searching qualifying keys, skipping a partition, or rejecting an entire block using metadata. A residual filter applied after retrieval is different again. Read the plan's condition placement and the storage format before reporting an I/O reduction. The same logical selectivity can lead to very different bytes moved when qualifying rows are packed together versus scattered across many pages.

The semantic restrictions from relational rewrites remain in force. An outer join can preserve rows that a moved right-side predicate would handle differently; a LIMIT or window calculation can change which rows qualify when evaluation order changes. Volatile expressions and expressions that can error also prevent simple total-function reasoning. Security barriers impose another boundary: evaluating a user expression before authorization can expose information through its behavior. PostgreSQL documents special treatment for leakproof functions in row-security evaluation. A seemingly faster placement is not valid if it changes permitted observation or the required result.

N^downstream=sN,0≤s≤1\widehat N_{\rm downstream}=sN,\qquad 0\le s\le1
N counts input rows and s is the fraction satisfying an eligible predicate. This row-count reduction says nothing by itself about pages read, expression evaluation cost, or whether moving the predicate across a particular operator is semantically valid.↔ Scroll wide equations horizontally.

Keep identities until wide columns are needed

A column-oriented execution path can scan a narrow predicate column, retain the positions of qualifying records, and fetch wide output columns for those positions later. That avoids reconstructing full tuples that will soon be rejected. A position list, range set, or bitmap must identify the same logical rows across all participating columns. Updates, compaction, deleted rows, and snapshot visibility make that identity a storage-engine contract, not an assumption that the nth physical value always belongs to the nth current row.

Late materialization can trade sequential value movement for gathering scattered values. If most rows survive, positional bookkeeping and repeated column access may cost more than early reconstruction. Compressed representations and vector operators can sometimes keep positions cheap; variable-length values and repeated joins can make reconstruction harder. The Abadi materialization paper examines these design choices in column stores, rather than claiming one universal best point. PostgreSQL index-only access illustrates a related separation between available index values and heap visibility, but it should not be described as the same late-materializing column engine.

Dlate=Nwf+sN(wi+wp),Dearly=N(wf+wp)D_{\rm late}=Nw_f+sN(w_i+w_p),\qquad D_{\rm early}=N(w_f+w_p)
At a declared idealized transfer boundary, w_f is predicate bytes scanned per row, w_i is one written survivor-ID width, and w_p is fetched payload bytes per survivor. The model counts that ID write once, excludes its later reads, page granularity, compression, output traffic, and metadata, and uses 0≤s≤1. D values are bytes.↔ Scroll wide equations horizontally.

Measure the boundary where work disappears

State whether an estimate concerns storage-device reads, memory-controller traffic, decoded values, operator payload, or final network bytes. Those quantities can differ sharply. Skipping payload decoding may save CPU without reducing already-prefetched device bytes; avoiding tuple reconstruction may reduce allocations while leaving output volume unchanged. Compare the same final rows and ordering under a stable snapshot. Include position-buffer lifetime and the effect of selectivity on locality. A smaller intermediate row count is an explanation to test, not an automatic statement about elapsed time.

The cache lab isolates one access-pattern consideration without modeling a column store. Reset, set both C=4 and s=4, predict twelve hits in the initial sixteen requests, then Play or Step. Separately Reset, set both C=4 and s=1, predict no initial hits, then Play or Step. The fixed modulo-sixteen stride changes the address cycle; it does not generate positions from a SQL predicate or map logical rows to disk pages. Explain why actual late-fetch traffic needs a real survivor-position distribution rather than substituting the toy stride for a query's selectivity.

Count narrow scans and survivor payload separately

An ideal packed-column model has 1,000,000 rows, a four-byte predicate value per row, a 128-byte output payload, and one percent survivors. Count one four-byte ID write per survivor and no ID rereads or page overhead.

  1. An early path touching predicate and payload for every row transfers 1,000,000×(4+128)=132,000,000 bytes at the declared model boundary.

  2. The late path scans 4,000,000 predicate bytes. Its 10,000 survivors fetch 1,280,000 payload bytes and write 40,000 ID bytes, totaling 5,320,000 bytes.

  3. The difference is 126,680,000 modeled bytes, about a 24.81-fold ratio of early to late traffic. This does not predict device I/O or runtime because page granularity, compression, ID rereads, gathering, and output traffic were deliberately excluded.

The result

The saving is specific to the stated transfer accounting and selective workload. It is not a claim that an arbitrary pushed filter reads twenty-five times less disk data.

Explore the model

Interactive lab

Interactive lab · cache

Late fetch still needs an access-pattern argument

Reset, set both C=4 and s=4, predict twelve hits in the initial sixteen requests, then Play or Step. Separately Reset, set both C=4 and s=1, predict zero, then Play or Step. This fixed LRU stream has no SQL filter, survivor positions, disk layout, or column reconstruction.

Late fetch still needs an access-pattern argumentA fully associative LRU cache starts empty and requests address (i·s) mod 16. Hits move an address to the newest position; misses evict the oldest when full. Playback starts with 16 requests and adds two per second, capped at 4096 explicitly simulated requests. This toy stream is not a processor-cache benchmark. Current readings: Hits / requests: 0 / 16; Hit rate: 0%; Address-stream period: 16 addresses. All parameters are adjustable with the labeled controls below.Fully associative LRU cache after 16 deterministic requests: 0 hits and 16 misses.Most recent 16 requestsaddress = i × 3 mod 160M3M6M9M12M15M2M5M8M11M14M1M4M7M10M13MCurrent cache order · oldest → newest471013Latest address 13 · miss: inserted as newestToy address stream · period 16 · starts empty
Cached addressMiss / evictionMost recent request
Hits / requests
0 / 16
Hit rate
0%
Address-stream period
16 addresses
t = 0 s

A fully associative LRU cache starts empty and requests address (i·s) mod 16. Hits move an address to the newest position; misses evict the oldest when full. Playback starts with 16 requests and adds two per second, capped at 4096 explicitly simulated requests. This toy stream is not a processor-cache benchmark. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.

Look forFewer fetched logical rows can still have poor locality; measure the actual boundary and survivor-position pattern.

Check your understanding

A scan-level residual filter rejects ninety-nine percent of rows after their pages have already been read. Which claim is directly justified?

Try it yourself

Under the same ideal boundary model, let predicate width be eight bytes, payload width forty bytes, ID width four bytes, and survivor fraction one. Compare late and early bytes per input row. What does this teach?

Take a moment to make a prediction or work it out on paper. Open a hint when you need one.

Give me a hint

Substitute s=1 before comparing the two formulas.

Compare with the solution

Late accounting gives 8+1×(4+40)=52 bytes per row, while early accounting gives 8+40=48. With every row surviving, this model adds an ID-write cost without eliminating payload fetches. Real compression, gathering, masks, and reconstruction costs can change the outcome; late materialization is not universally superior.

Take it with you

  • Pushdown must respect outer joins, expression behavior, and security boundaries.
  • Late materialization depends on stable row identity and selective value access.
  • Declare the transfer boundary before converting fewer rows into a byte-saving claim.

Read further

Original sources and deeper explanations to follow at your own pace.

Optional progress, saved in this browser when available. Revisit any lesson at any time.

Lesson not marked complete.