Skip to content

fix(server): narrow metric_series_rollups covering index to scalar INCLUDE (JEF-591) - #152

Merged
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-591-narrow-the-metric_series_rollups-covering-index-to-scalar
Jul 28, 2026
Merged

fix(server): narrow metric_series_rollups covering index to scalar INCLUDE (JEF-591)#152
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-591-narrow-the-metric_series_rollups-covering-index-to-scalar

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

Closes JEF-591.

Summary

Narrows metric_series_rollups_name_bucket_covering_idx's INCLUDE list to
scalar-only columns (service, kind, unit, is_monotonic, count, sum, avg, max),
dropping the heavy attrs JSONB and bucket_bounds/bucket_counts arrays --
only the rarer facet/histogram read paths need those; the hot
query_metric_series path only ever selects sum/count. This kills
per-ingest-upsert fat-tuple write amplification on the Pi.

The real work (per the ticket) was making the JEF-580 online-DDL lane detect
the definition change at all.
The lane originally matched a desired index by
name + pg_index.indisvalid only, so just editing DESIRED_INDEXES'
create_sql would have been a silent no-op against the existing wide, valid,
same-named prod index -- it would never narrow.

Drift-detection approach

  • OnlineIndex gains an include_cols: &'static [&'static str] field.
  • reconcile_index's Valid branch now also calls include_columns_drifted,
    which compares the index's live INCLUDE columns against include_cols.
  • Live columns are read via the catalog (pg_index/pg_attribute --
    unnest(indkey) past indnkeyatts, the count of true key columns),
    not pg_get_indexdef's text, which isn't a stable string to diff
    against our own create_sql (omits CONCURRENTLY, may schema-qualify
    names, doesn't guarantee INCLUDE column order).
  • Comparison is order-independent (sorted Vec<String> equality) -- only the
    column set matters.
  • On drift: DROP INDEX CONCURRENTLY + rebuild (same path as the existing
    INVALID-index handling, factored into a shared drop_index_concurrently
    helper). Absent/invalid handling is unchanged.

DESIRED_INDEXES

Updated to the scalar-only INCLUDE def. On deploy, the lane will detect the
wide-vs-narrow drift against the index migration 0017 built by hand in prod
and rebuild it narrow, off the boot path (that's the whole point of the
JEF-580 lane).

Testing

  • drifted_include_columns_are_dropped_and_rebuilt (new): pre-creates a wide
    index on a throwaway table, sets desired = narrow, runs reconcile_index --
    asserts it drops + rebuilds to the narrow INCLUDE set (new oid); runs again
    -- asserts no-op (same oid).
  • seeded_covering_index_narrows_then_is_a_noop (renamed from
    seeded_covering_index_run_is_a_noop): forces the real
    metric_series_rollups index into migration 0017's exact wide state, then
    runs the real lane (run(&url)) -- asserts the first run narrows it (new
    oid, narrow INCLUDE set), and the second run is a no-op.
  • All other existing online_ddl tests updated only for the new
    include_cols field (no behavior change to absent/invalid handling).

Local EXPLAIN verification (seeded ~2.1M-row metric_series_rollups,
~6% matching one metric name, to force a realistic plan choice instead of a
trivial seq scan):

  • query_metric_series-equivalent SQL: Index Only Scan ... Heap Fetches: 0
    with both the wide and the narrow index -- confirms no regression on the
    hot path.
  • metric_facet/metric_histogram-equivalent SQL against the narrow index:
    plain Index Scan (heap fetch for attrs/bucket_bounds/bucket_counts,
    as expected/acceptable per the ticket) -- correctness spot-checked and
    correct (matched the seeded avg/last/histogram-bucket-sum values).

What I could not measure: the real 10.4M-row prod EXPLAIN and prod
index-size delta -- only reachable against the production database, per the
ticket's own note. Local index-size numbers (269MB wide vs 228MB narrow on the
synthetic table) are not representative of prod: my synthetic rows left
attrs/bucket_bounds/bucket_counts mostly NULL/tiny, understating the real
per-tuple savings prod's populated JSONB attrs + histogram arrays would show.

Checks

  • cargo fmt -- clean
  • cargo check -- clean
  • cargo clippy --all-targets -- -D warnings -- clean
  • cargo test --locked -- 68 lib tests + 66 integration tests, all green (run
    twice consecutively against the same persistent local dev DB to confirm
    idempotency, not just from a pristine DB)
  • /soundcheck:pr-review -- 0 Critical/High findings (all new SQL is either
    parameterized or the same pre-existing AssertSqlSafe-on-hardcoded-constant
    pattern the JEF-580 drop path already used)
  • /simplify -- single pass (no Agent-tool fan-out in this context); extracted
    a shared desired_include_cols helper to dedupe a map+sort pattern that
    appeared in both production code and a test

🤖 Generated with Claude Code

…CLUDE (JEF-591)

Narrows metric_series_rollups_name_bucket_covering_idx's INCLUDE list to
scalar-only columns (service, kind, unit, is_monotonic, count, sum, avg,
max), dropping the heavy attrs JSONB and bucket_bounds/bucket_counts
arrays -- only the rarer facet/histogram read paths need those; the hot
query_metric_series path only ever selects sum/count. Narrower INCLUDE
means a smaller index tuple, cutting per-ingest-upsert write
amplification on the Pi.

The JEF-580 online-DDL lane only matched a desired index by name +
pg_index.indisvalid, so editing DESIRED_INDEXES' create_sql alone would
have been a silent no-op against the existing wide, valid, same-named
prod index -- it would never narrow. Added definition-drift detection:
OnlineIndex gains an include_cols field, and reconcile_index now also
compares a *valid* index's live INCLUDE columns (via pg_index/
pg_attribute -- indkey past indnkeyatts -- not pg_get_indexdef's text,
which isn't a stable string to compare against) against the desired
set, dropping + rebuilding on a mismatch. Absent/invalid handling is
unchanged.

Tests (against a real Postgres): a synthetic pre-created wide index with
a narrow desired def is dropped + rebuilt to the narrow INCLUDE set, and
a second run against the now-matching definition is a no-op; the
existing seeded-index test now exercises the real migration-0017-built
wide index narrowing via the lane, then confirms idempotency.

Locally verified with a seeded ~2.1M-row table: query_metric_series's
query plans as an Index Only Scan (Heap Fetches: 0) with the narrow
index (matching the wide index's plan -- no regression on the hot
path), and metric_facet/metric_histogram-equivalent queries return
correct results via a plain Index Scan (heap fetches for attrs/
bucket_bounds/bucket_counts, as expected). Real-prod (10.4M row) EXPLAIN
and index-size measurement are out of scope here -- only reachable
against the production database.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@thejefflarson
thejefflarson merged commit 379afc8 into main Jul 28, 2026
5 checks passed
@thejefflarson
thejefflarson deleted the thejefflarson/jef-591-narrow-the-metric_series_rollups-covering-index-to-scalar branch July 28, 2026 05:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant