Build1 distinct publisher3 min readUpdated
A dev.to walkthrough of tuple storage shows why write volume, not row count, sets heap size. The explainer stops one sentence before the part operators actually need.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A dev.to walkthrough of PostgreSQL storage lays out the mechanic most application developers have backwards: an UPDATE does not change the old value in place, it writes a new tuple version and leaves the previous one sitting in the page [8]. The consequence is that a table with a flat row count can still grow, because heap occupancy tracks how many times you wrote, not how many rows you keep [16][17].
The physical layout is worth being precise about. PostgreSQL stores table data in fixed-size pages, usually 8 KB [1], and each page holds multiple tuples, which is what Postgres internally calls a row [2]. A page is not a stack of rows; it has a header, an array of line pointers, free space, and the tuples themselves [3]. Each line pointer records the byte offset and length of one tuple inside that page [4]. The `ctid` system column exposes a pair of (block number, item identifier number), where the second number identifies a line pointer rather than a byte offset [5]. A B-tree index does not carry the row, only a TID that locates the heap tuple [6], so a lookup goes index, then TID, then heap page, then line pointer, then tuple [7].
Now run the update. In the source's example the original tuple sits at (0,1) with age 25; after the update the old version stays at (0,1) and a new version appears at (0,2) with age 30 [8]. The reason given is that another transaction may still need to see the old value, which is the basis of MVCC [9]. Tuple headers carry xmin, the transaction that created the version, and xmax, the transaction that deleted or replaced it [10]. With transaction 8 doing the update, the old tuple carries xmin 5 and xmax 8, and the new one carries xmin 8 [11]. A SELECT does not simply take the highest ctid; it takes whichever version is visible in its snapshot [12], and the real rules involve transaction status and the snapshot's view of committed and in-progress transactions, not a bare comparison of xmin and xmax [13]. That is how two concurrent transactions read different versions of what looks like one row [14].
Two operational facts fall straight out of that. First, one row updated n times, with nothing reclaimed, means n+1 tuple versions and n+1 line pointers in the heap [16]. Second, the new version in the example landed in block 0, the same page as the old one, which only works if that page still had free space; the source does not say what happens when it does not [18]. Nor does it say how index entries follow the move, even though the new version has a different TID from the one an existing index entry addresses [19].
That gap is the honest limit of this material. The explainer reaches the question of what happens to the old version and is cut off mid-sentence, with no treatment of reclamation, vacuum settings, or fill-factor [15]. So take the storage model as established and the cleanup story as unwritten. What to watch is whether the follow-up quantifies the reclamation side, because the sizing question sits entirely there: everything above only explains why dead versions accumulate, not what it costs to get rid of them [15][16].
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.
PostgreSQL stores table data in fixed-size pages, and the usual page size is 8 KB.
Each page can contain multiple rows, and internally PostgreSQL calls a row a tuple.
A simplified page consists of a page header, line pointers, free space, and tuples.
A line pointer tells PostgreSQL where a tuple is located inside the page; it contains the actual byte offset and length of the tuple.
PostgreSQL exposes a system column called ctid, which is essentially (block number, item identifier number); (0,1) does not mean byte offset 1, the second value identifies a line pointer.
A B-tree index does not contain the entire row; it stores a TID that helps PostgreSQL locate the corresponding heap tuple.
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.
Single explainer, mechanism-only, no measurements
The mechanics described (8 KB pages, line-pointer indirection, ctid semantics, TID-bearing index entries, new tuple version per UPDATE, xmin/xmax plus snapshot visibility) are internally consistent and illustrated with a concrete worked example, which supports the descriptive claims. But the cluster rests on exactly one community post with no version reference, no documentation citation, no independent corroboration and no measurements, and one ledger claim about the text's scope is contradicted by the text itself.
No adoption signal in supplied sources
The single source is an educational explainer of existing PostgreSQL behaviour. It contains no release, deployment, benchmark, pricing, licensing, security or usage-disclosure event, so no adoption observation could be recorded and adoption cannot be scored without inventing facts.
Framing slightly overshoots the text
The source itself is measured and repeatedly hedges ('simplified', 'very roughly'). The overstatement sits in the cluster framing: the claim that the explainer stops before the part operators need is contradicted by a body that does cover dead tuples, VACUUM, autovacuum and HOT updates, and the space-consumption argument is presented as a consequence without any quantified bloat or write-amplification evidence.
Low commercial pressure, attention-driven format
The observable incentive structure is a single-author community post on dev.to about an open-source engine: no vendor product, pricing, sponsorship or benchmark comparison appears anywhere in the text, so commercial distortion pressure is low. The residual incentive is engagement-oriented packaging - a provocative title and a simplified mental model that omits caveats an operator would need.
Mechanisms credible, corroboration thin
Confidence is moderate: the described storage and MVCC mechanics are coherent and self-consistent, and the post is honest about being simplified. It is held down by single-publisher sourcing, absence of measurements or documentation references, no adoption dimension at all, and two contested claims where the ledger's characterisation of the source conflicts with the supplied text.
build
Your "Index Only Scan" Did 2,847 Heap Fetches: Covering Indexes Are a Vacuum Problem1 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 16, 2026