Build1 distinct publisher3 min readUpdated
A dev.to workshop makes a case worth repeating: the plan label describes intent, not cost. Whether a covering index actually skips the heap is decided by autovacuum, not by the index.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
A workshop published on dev.to by MVP Factory argues that most backend teams read the node type in an execution plan and stop, which is how a covering index ships to production looking healthy and performing exactly like the scan it replaced [12]. The post's example plan is the whole argument: an Index Only Scan on `idx_events_covering` returning 50 rows, with `Heap Fetches: 2847` and `Buffers: shared hit=312 read=198` [5].
The mechanism is not obscure, it is just easy to skip. Under Postgres MVCC, visibility metadata lives on heap tuples, and an index entry does not carry it [1]. Before skipping a heap fetch, the executor consults the visibility map, one bit per heap page, where a set bit asserts that every tuple on that page is visible to all current and future transactions [2]. Only a set bit permits the fetch to be skipped [3]. VACUUM is what sets those bits, and autovacuum is what runs VACUUM [4]. Any UPDATE or DELETE touching a page clears the bit again [7].
That is why the index is not the variable. The two plans in the post sit either side of the same index: the degraded one touches 510 buffers, the healthy one touches 14, roughly a 36x difference [14]. Divide the fetches by the rows and the degraded plan is doing about 57 heap visits per row it returns [15]. The author reports latency in the degraded case as nearly identical to the pre-index query [12].
The post's throughput table is where I would slow down. It claims near-zero heap fetch rates below 100 rows/sec, 10 to 40 percent between 100 and 500 rows/sec, 60 to 100 percent above 500, and 100 percent on bulk ETL ingest [8]. No methodology, hardware, or table geometry is given for those bands, so treat them as one practitioner's rule of thumb rather than a curve you can plan capacity against. The direction is sound; the digits are not measurements you own.
The prescribed fix is per-table autovacuum overrides: `autovacuum_vacuum_scale_factor` at 0.01 against a default of 0.2, `autovacuum_vacuum_cost_delay` at 2ms, and `autovacuum_vacuum_cost_limit` at 800 against a default of 200 [9]. Read those as what they are. The scale factor makes vacuum trigger at one twentieth of the dead-tuple accumulation [16], and the cost limit hands each round four times the I/O budget [17]. You are buying visibility map freshness with vacuum I/O on a table that is already write-saturated, which is a trade, not a free win.
The part worth adopting immediately is the measurement, not the tuning. The post's query joins `pg_stat_user_tables` with `pg_visibility` to compare all-visible pages against total heap pages, and the read is simple: if `vm_visible_pages` trails `heap_pages`, your index-only scans are paying for heap fetches [10]. That needs PostgreSQL 12 or later and access to both views [11].
Two things to watch. First, put heap fetches per returned row into whatever you use to review slow queries, because the plan label will not regress when the visibility map goes stale. Second, batch loads: the post's fourth step is headed as forcing VACUUM after batch loads, though the copy supplied here is truncated before the detail [13]. An ETL window that leaves every touched page unmarked will degrade the same covering index that behaved perfectly all night.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The visibility map is a compact one-bit-per-heap-page structure where a set bit means every tuple on that page is visible to all current and future transactions.
Only when the visibility map bit is set can an index-only scan skip the heap fetch entirely.
The post's degraded example plan reads: Index Only Scan using idx_events_covering on mobile_events, actual time 0.341..1.204, rows=50, Heap Fetches: 2847, Buffers: shared hit=312 read=198.
The post recommends per-table overrides: autovacuum_vacuum_scale_factor = 0.01 (trigger at 1% dead tuples, default 20%), autovacuum_vacuum_cost_delay = 2 ms, autovacuum_vacuum_cost_limit = 800 (default 200).
The post lists prerequisites of PostgreSQL 12+, a table with at least one covering index, familiarity with EXPLAIN ANALYZE output, and access to pg_stat_user_tables and pg_visibility.
Under PostgreSQL's MVCC model, heap tuples carry visibility metadata while an index entry does not.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Sound mechanism, unsourced numbers, one publisher
The causal chain the cluster rests on - MVCC visibility metadata on heap tuples, a one-bit-per-page visibility map, VACUUM setting those bits, writes clearing them - is coherent and matches long-documented PostgreSQL behaviour, and the diagnostic and monitoring instructions are specific enough to execute. But every quantitative element is an unsourced illustration: two plan snippets with no schema, version, or hardware, and a throughput-to-heap-fetch-rate table whose only stated provenance is the author's audit experience. With one publisher, no citations, and no independent replication in the cluster, verifiable support sits below the midpoint.
No adoption signal in cluster
The supplied material is instructional. It contains no release, deployment, benchmark run, usage disclosure, pricing or licence event, and no named organisation reporting that it applies these settings or tracks this metric. The author's reference to codebases audited is an assertion of experience, not an observable adoption fact, so no adoption score can be assigned without inventing one.
Real mechanism, overstated quantification
The underlying point is legitimate and arguably under-appreciated, which pulls the gap toward zero. Pushing it positive is the framing around it: a specific heap-fetch count and a tidy table of degradation percentages are presented as typical outcomes without any measurement, the recommended overrides are given as a fix with none of their I/O or replication costs, and visibility map coverage is billed as the single most under-monitored PostgreSQL metric in a piece that also serves as agency marketing. The claims run modestly ahead of what the cluster demonstrates rather than being fabricated.
Agency content marketing, disclosed by byline
The post is published by a software agency account and canonicalised to that agency's own blog, and its authority rests on the author's claim to audit client codebases and build production systems. That is a commercial lead-generation incentive to present the problem as widespread and the fix as expert-dependent. It is mitigated by the fact that no product is being sold in the text, the advice uses only built-in PostgreSQL features, and the affiliation is visible on the byline and canonical URL rather than hidden.
Single publisher, illustrative data, adoption unknown
Confidence is limited by structure rather than by contradiction: one publisher, one item, no corroborating documentation or second operator account, no adoption dimension at all, and quantitative claims that cannot be checked. Confidence is not lower because the mechanism claims are precise, mutually consistent, and align with well-established PostgreSQL behaviour, and because the one internal discrepancy found - the ledger's note that Step 4 was missing - is resolvable directly against the supplied body.
build
Your ORM never puts a WHERE clause in an index, and that is where the seq scans live1 distinct publisher
build
The optional EntityManager is the bug: moving the transaction boundary into AsyncLocalStorage1 distinct publisher
build
Three attackers hide behind one connect button, and encryption only stops one of them1 distinct publisher
build
Three services you can delete: queue, cache and search in one Postgres1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 18, 2026