Skip to content

expire reads a column's zone maps in one catalog scan, not one per row group #1310

Closed

Description

@jdatcmd

Follow-up to #1290 (PR #1307), whose phase 0 measured a no-op pgcolumnar.expire at one zone_map catalog scan per row group: 10 scans and 0.05 ms at 10 groups, 1000 scans and 2.7 ms at 1000 groups (PG17). With the daemon running expire every pass, a billion-row table (about 1M groups) would cost about 3 s and a million catalog scans per pass.

#1304 added PgColumnarReadWholeChunkZoneMapsForColumn(storageId, columnIndex, snapshot), one scan keyed on storage_id. expire can read the retention column's maxima through it instead of calling PgColumnarReadZoneMapForColumn per group.

Note: that scan reads every zone-map row of the table and filters in C. OffgridwithJD measured 1006 planning buffers against 120 at 100M rows for the planner's use (#1292). For expire, which already reads every group, that trade is still N scans to 1. Measure both before choosing, and consider option 5's per-column extent (#1292) if it lands first.

Done when: an arm counts expire's zone_map scans (pg_stat_get_xact_numscans on zone_map plus zone_map_pkey) at 50 groups and asserts 1, red before the change. ttl_expire and autovacuum_expire stay green.

🤖 Generated with Claude Code

Activity

  1. jdatcmd commented on Oct 12, 2026

    @jdatcmd
    CollaboratorAuthor

    Measured: neither variant is worth shipping. Waiting on option 5 instead.

    The setup: PG17, a no-op expire (a 3650-day TTL), and a 10-column table. A reads the column's zone maps in one scan through PgColumnarReadWholeChunkZoneMapsForColumn, then bsearches by group. B keeps the per-group probes, with one shared PgColumnarZoneMapSession. Time is the mean of 50 calls; buffers are from EXPLAIN (ANALYZE, BUFFERS) of one call.

    geometry variant zone_map scans buffers ms per call
    1000-row groups, 1M rows (1000 groups, 20 zone-map rows each) main 1000 3230 2.12
    A: one scan 1 824 1.77
    B: one session 1000 3230 2.35
    default geometry, 10M rows (67 groups, 159 zone-map rows each) main 67 416 0.22
    A: one scan 1 646 0.89

    A wins only with unusually small row groups. On default geometry it's 4× slower. The scan keys on storage_id alone, so it reads every column's whole-chunk and per-vector rows and discards all but one column's. That's the same cost OffgridwithJD measured for the planner on #1304. B changes nothing: the per-probe cost is the index descent, not opening the catalog.

    What would actually help: most daemon passes find nothing to retire, so the gain is an early exit for the no-op pass. If the retention column's minimum over the whole table is already past the cutoff, nothing can expire, and expire can return 0 without a single per-group read. That needs a per-column extent kept at flush time, which is exactly #1292's option 5 (OffgridwithJD is building it). The bound also has to stay sound after retire and rewrite, as I asked there; for this use, a stale-but-wide extent only means no early exit, which is safe.

    Plan: this waits on option 5. Then:

    • Arm 1: a no-op expire reads zero zone-map rows when the extent's minimum is past the cutoff.
    • Arm 2: expire still retires correctly when the extent says it might.
  2. added 2 commits that reference this issue on Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions