layout: default title: Zone-Map-Only Fast Paths

Spec: Zone-Map-Only Fast Paths

Status: first metadata-only helper implemented Risk tier: CAUTION Primary goal: define the honest boundary between current zone-map pruning and future heap-fetch-avoiding optimizations.

Problem

The shorthand “index-only scan equivalent using zone map” is misleading for the current storage format.

PostgreSQL Index Only Scan can return indexed column values without fetching a heap tuple when visibility checks allow it. The current sorted_heap zone map does not store tuple values or tuple identifiers. It stores page-level min/max for the first two primary-key columns:

page -> (col1_min, col1_max, col2_min, col2_max)

The SortedHeapScan custom scan uses this metadata to compute block ranges and then reads heap tuples with table_scan_getnextslot(...). Executor quals remain the final correctness filter.

Therefore, the current zone map can skip heap pages, but it cannot by itself return result rows.

Current Contract

For ordinary SELECT queries:

  • zone maps are a page-pruning accelerator;
  • heap tuple fetch remains required;
  • MVCC visibility remains checked through the normal heap scan path;
  • executor quals and projection remain authoritative;
  • EXPLAIN should keep showing Custom Scan (SortedHeapScan), not an index-only path.

This is the right correctness boundary for 0.14.

Non-Goals

  • Do not present SortedHeapScan as a PostgreSQL Index Only Scan.
  • Do not return rows directly from page-level min/max metadata.
  • Do not bypass heap visibility checks.
  • Do not make exact-count claims from zone maps unless the table state carries enough exact row-count metadata and MVCC semantics are specified.

Valid Future Paths

Path A: Zone-map-only metadata answers

This path is only for queries whose answer can be proven from page metadata.

Candidate examples:

  • empty-result proof for predicates whose zone-map overlap set is empty;
  • planner/executor stats and observability helpers;
  • future approximate planning estimates;
  • future exact page-range summaries if the storage format adds exact tuple counts with a clear MVCC validity contract.

This path does not return user rows. It can only answer metadata-level questions or prove no heap page can match.

Implemented first helper:

  • sorted_heap_zonemap_may_match_int8(regclass, bigint, bigint) returns false only when valid first-key zone-map metadata proves that an int8 range cannot match.
  • The helper fail-opens to true on stale zone maps, unsupported key shapes, or possible overlap.
  • The helper is intentionally a building block for application/planner logic, not a row-returning executor path.

Path B: Covering sidecar / value-bearing index

This is the closest real equivalent to index-only behavior.

Required pieces:

  • a value-bearing sidecar or proper Index AM path that stores the needed projected columns or a covering subset;
  • tuple identity and visibility semantics compatible with PostgreSQL MVCC;
  • invalidation/rebuild rules across compact, merge, online rewrite, restore, and partition attach/detach;
  • planner rules that engage only when the target list and quals are covered;
  • regression tests comparing results against normal heap execution.

This is a larger feature than the current zone map. It should be designed as a new contract, not as an extension of page min/max pruning.

Acceptance Tests For Any Implementation

Z1. Empty-result pruning remains heap-safe

Create a compacted sorted_heap table and query a PK range outside all zone-map entries.

Expected:

  • sorted_heap_zonemap_may_match_int8(...) returns false for a range outside all valid first-key zone-map entries;
  • it returns true for overlapping ranges;
  • stale or unsupported metadata returns true, preserving normal heap execution.

Z2. Covered-row path proves coverage

For any future covering sidecar:

  • query target list contains only covered columns;
  • quals contain only supported covered predicates;
  • result equals the heap-backed plan for visible rows;
  • disabling the sidecar produces the same rows.

Z3. Non-covered query falls back

For a query that requests a non-covered column:

  • planner does not choose the covering path;
  • normal heap-backed execution is used.

Z4. Rewrite lifecycle invalidates or rebuilds correctly

After compact, merge, online compact/merge, pg_restore, and partition attach/detach:

  • stale sidecar state is not used;
  • rebuild/refresh path is explicit;
  • query results match the heap-backed plan.

Adversary Notes

  • Page min/max metadata is lossy for rows inside a page. It can eliminate pages, not identify matching tuples.
  • UUID and text/varchar zone-map keys can be lossy. They are acceptable for pruning because executor quals verify rows, but not for row-returning identity.
  • MVCC visibility is the hardest part of any index-only-like path. A sidecar that stores values still needs a visibility story; otherwise it can return dead or uncommitted rows.
  • Partitioned tables multiply lifecycle rules: every leaf needs independent sidecar validity plus parent-level planning/fanout rules.

Decision

For 0.14, treat “index-only scan equivalent” as resolved into this spec: zone-map-only row return is not a valid feature on the current storage format. Future work should choose explicitly between metadata-only fast paths and a covering value-bearing sidecar.