Skip to content

pg_index_bloat has never returned a row: pgstatindex reads every page of 1,517 indexes in one statement #2617

Description

@erikdarlingdata

Found by dogfooding v99 on the Aurora target. pg_index_bloat has never returned a single row since it shipped — rows_ever=0 — and it fails like this:

2026-08-25 18:14:21 | ERROR | Exception while reading from stream

That is an Npgsql connection-level failure, not a classified miss, so it also does not degrade to the named non-fatal skip every other PostgreSQL collector uses.

Cause: I bounded the wrong dimension

pgstatindex reads every page of the index — that is what makes it a measurement rather than an estimate, and it is why #2561 chose it over the ioguix estimator, which returns nothing under pg_monitor.

The collector carries MeasureCeilingBytes = 20 GB, which bounds any single index and records skipped_reason for the ones above it. Nothing bounds the total. On the target:

indexes_tracked = 1517
total_bytes     = 461 GB

So one collection reads up to 461 GB of index pages in a single statement, on the default command timeout, against a production cluster. It never finishes, the connection drops mid-read, and the cycle records an ERROR.

A per-index ceiling and a total-work ceiling are different things and I only built the first.

Why the local rig did not catch it

Two indexes on one table. The measurement was correct and the design question — how much work is this in total — never arose, because the answer was "none". This is the failure mode a container cannot show you.

Fix

Bound the total work, not just each index:

  1. Rank candidates by size and take the top N per cycle, largest first — bloat that matters is concentrated in big indexes, and a small index at 40% density is worth kilobytes.
  2. Record what was left unmeasured. skipped_reason already exists for exactly this and must say not measured this cycle (work budget) rather than leaving the reader to think the index is fine.
  3. Add a CommandTimeoutSecondsOverride so a slow single index yields a classified timeout rather than a dropped connection — index_object_stats already does this for the same reason ([BUG] index_object_stats collector times out (30s) as a single all‑or‑nothing cross‑database sweep - yields no index data #1135).

Rotating the window across cycles is tempting but should wait: a fixed top-N by size is predictable, and the retention on this table is 90 days, so a large index measured daily is plenty of history.

This also means the exact-bloat claim on #2561 is unverified in production. It was verified on a rig with two indexes; on a real cluster the collector has produced nothing at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions