Avoid calling SetMatViewPopulatedState if possible

First seen: 2026-04-10 03:36:43+00:00 · Messages: 3 · Participants: 2

Latest Update

2026-05-06 · opus 4.7

Overview

This is a small, focused optimization patch for REFRESH MATERIALIZED VIEW that targets an unnecessary catalog update performed on every refresh. The thread is short (three messages) but surfaces a non-obvious correctness/bloat concern that elevates it above a pure micro-optimization.

The Core Problem

SetMatViewPopulatedState() is called unconditionally at the end of ExecRefreshMatView() to mark the matview's pg_class.relispopulated flag as true. The observation from the submitter (cca5507) is that when a matview is already populated — which is the overwhelmingly common case for REFRESH MATERIALIZED VIEW (you can only refresh something that exists and is typically already populated after the first refresh) — this call performs a catalog update that changes nothing semantically.

The architectural cost isn't CPU cycles; it's MVCC bloat in pg_class. Every call to SetMatViewPopulatedState() performs a heap_update() on the matview's pg_class row, which under MVCC means:

  1. The old tuple is marked dead (xmax set).
  2. A new, logically identical tuple is inserted.
  3. Any indexes on pg_class get new entries (HOT may or may not apply depending on indexed columns and page fill).
  4. Subsequent VACUUM/autovacuum work is required to reclaim the dead tuple.

The submitter demonstrates this concretely: the ctid of the pg_class row for matview m changes on every REFRESH MATERIALIZED VIEW CONCURRENTLY m, proving that a new heap tuple version is produced each time. On workloads that refresh matviews frequently (common for incremental-style analytics pipelines that refresh every few minutes), this produces a steady stream of pg_class bloat that autovacuum has to clean up — and pg_class is a catalog everyone hits, so its bloat has outsized performance implications.

The Proposed Fix

The patch (not quoted in full, but inferable from the description) wraps the SetMatViewPopulatedState(matviewOid, true) call with a guard that checks the current relispopulated flag on the matview's pg_class tuple. If it is already true, the call is skipped. Only when refreshing a matview created with WITH NO DATA (where relispopulated = false) does the update actually fire.

This is a classic "avoid no-op catalog updates" pattern that PostgreSQL has applied elsewhere (e.g., ATExecChangeOwner skips when the owner is unchanged, and various ALTER paths check before updating).

Key Technical Considerations

Why the performance delta is invisible

The submitter correctly notes that benchmarking won't show much. A single heap_update on one pg_class row is dwarfed by the actual refresh work (scanning base tables, rebuilding the matview heap, possibly rebuilding indexes, or — for CONCURRENTLY — diffing and applying deltas). Geibel's (geidav.pg) request for performance data is a reasonable reviewer reflex, but in this case the justification is correctness-adjacent (avoiding bloat) rather than latency.

Why pg_class bloat matters disproportionately

pg_class is read by virtually every query during planning (relation lookups, relcache building). Bloat here degrades:

CONCURRENTLY mode makes this more important

REFRESH MATERIALIZED VIEW CONCURRENTLY is explicitly designed for frequent refreshes of populated matviews — it diffs and applies changes rather than truncate-and-reload. So the case where the flag is already true is essentially all of the CONCURRENTLY workload. The patch aligns the code with the intended usage pattern.

Race conditions / locking

ExecRefreshMatView already holds AccessExclusiveLock (for non-CONCURRENTLY) or ExclusiveLock (for CONCURRENTLY) on the matview, so reading relispopulated and deciding to skip the update is safe — no one else can flip the flag under us.

Design Tradeoffs

Participant Analysis

No committer has weighed in in the three messages shown, and no design disagreement has surfaced. The patch is the kind of thing that typically gets committed after one more round of review confirming the guard logic is correct and there's no subtle case (e.g., recovery from a failed refresh) where forcing the flag write is desirable.

Open Questions Not Addressed in the Thread

  1. Should the same guard be added symmetrically when creating a matview WITH NO DATA and then later populating it? (Probably not an issue since that path flips false→true.)
  2. Does REFRESH ... WITH NO DATA on an already-unpopulated matview have the same redundant write? Worth checking in the patch.
  3. Should this be backpatched? Likely no — it's a minor optimization, not a bug fix, and backpatching catalog-touching logic changes is generally avoided.