Conversation
01237c9 to
3b56d29
Compare
c2a5026 to
d8fed9e
Compare
c999d4e to
d3a10f3
Compare
14db4ac to
bad9884
Compare
7dda555 to
44349f6
Compare
f44609c to
25c1f98
Compare
7e77e41 to
f9645af
Compare
e85fc46 to
22b384f
Compare
f9645af to
7ca55a1
Compare
Add regression coverage for existing classic Heap-Only Tuple (HOT) update behavior, committed first so the behavioral changes in the later HOT-indexed commits are diffable against a known-good baseline. This commit adds tests so as to codify explicitly the HOT contract for the heap AM. The new hot_updates regression test exercises: - Basic HOT vs non-HOT update decisions - The all-or-none property across multiple indexes - Partial indexes and predicate handling - BRIN (summarizing) indexes allowing HOT updates - TOAST column handling with HOT - Unique constraint behavior - Multi-column indexes - Partitioned table HOT updates - HOT chain formation and the index-scan walk over a chain Authored-by: Greg Burd <greg@burd.me>
Refactor executor update logic to determine which indexed columns have actually changed during an UPDATE operation rather than leaving this up to HeapDetermineColumnsInfo() in heap_update(). Finding this set of attributes is not heap-specific, but more general to all table AMs and having this information in the executor could inform other decisions about when index inserts are required and when they are not regardless of the table AM's MVCC implementation strategy. The heap-only tuple decision (HOT) in heap functions as it always has; what moves to the executor is only the determination of the "modified indexed attributes" (modified_idx_attrs). ExecUpdateModifiedIdxAttrs() replaces HeapDetermineColumnsInfo() and is called before table_tuple_update() crucially without the need for an exclusive buffer lock on the page that holds the tuple being updated. This reduces the time the buffer lock is held later within heapam_tuple_update() and heap_update(). Besides identifying the set of modified indexed attributes HeapDetermineColumnsInfo() was also partially responsible for the decision about what to WAL log for the replica identity key. That logic moves into heap_update() and into the replacement helper HeapUpdateModifiedIdxAttrs(), so simple_heap_update() and heapam_tuple_update() share the same logic since both call into heap_update(). Updates stemming from logical replication also use the new ExecUpdateModifiedIdxAttrs() in ExecSimpleRelationUpdate(). ExecUpdateModifiedIdxAttrs() uses ExecCompareSlotAttrs() to identify which attributes have changed and then intersects that with the set of indexed attributes to identify the modified indexed set, the modified_idx_attrs. This patch introduces a few helper functions to reduce code duplication and increase readability: HeapUpdateHotAllowable() and HeapUpdateDetermineLockmode(), used in both heap_update() and simple_heap_update(). heap_update() is now called with lockmode pre-determined and a boolean indicating whether the update may be HOT, both const. If during heap_update() the new tuple fits on the same page and that boolean is true, the update is HOT. So although the functions and timing of the HOT decision code have changed, none of the logic governing when HOT is allowed has changed. Development of this feature exposed nondeterministic behavior in three existing tests, which have been adjusted to avoid inconsistent results due to tuple ordering during heap page scans. Authored-by: Greg Burd <greg@burd.me> Discussion: https://commitfest.postgresql.org/patch/5556/ Discussion: https://www.postgresql.org/message-id/flat/78574B24-BE0A-42C5-8075-3FA9FA63B8FC%40amazon.com
Define the on-disk representation a HOT-indexed update and its later prune/collapse produce, ahead of the code that reads or writes it: - HEAP_INDEXED_UPDATED (htup_details.h), the t_infomask2 bit marking a heap-only tuple whose producing UPDATE also changed an indexed column; and - access/hot_indexed.h, the inline fixed-size modified-attrs bitmap stored in the tail of such a tuple, plus the xid-free "collapse-survivor stub" format (HEAP_INDEXED_UPDATED with natts == 0, a forward link, and the segment's bitmap) and the accessors both share. README.HOT-INDEXED introduces the design and the relaxed classic-HOT invariant; later commits document the eligibility, write, read, and prune/collapse machinery in their own sections. Co-authored-by: Greg Burd <greg@burd.me> Co-authored-by: Nathan Bossart <nathandbossart@gmail.com>
Implement the HOT-indexed (Selective Index Update) feature on the foundation laid by the executor's modified-attribute identification. Eligibility: HeapUpdateHotAllowable returns a HeapUpdateIndexMode -- HEAP_UPDATE_ALL_INDEXES (not HOT; every index needs an entry), HEAP_UPDATE_HOT (classic HOT; no index needs an entry), or HEAP_SELECTIVE_INDEX_UPDATE (HOT chain, only the changed indexes maintained) -- computed from modified_idx_attrs and the per-relation indexed-attribute set (RelationGetIndexedAttrs). An UPDATE that changes a non-summarizing indexed attribute is HEAP_SELECTIVE_INDEX_UPDATE unless it is forced to HEAP_UPDATE_ALL_INDEXES by one of: every indexed attribute changed (nothing to skip), an attribute referenced by an expression index changed (expression-aware maintenance is not implemented yet), a system catalog, or the logical-replication apply gate (see the apply-gating commit). Partial indexes, exclusion constraints, partitioned tables, and non-btree access methods are all eligible -- the read path is access-method agnostic and the predicate column is part of the index's attribute set, so no carve-out is needed for them. Write path: the table-AM update contract carries modified attributes IN/OUT as a Bitmapset (on output the AM adds the whole-row sentinel, TableTupleUpdateAllIndexes, to signal "every index needs an entry"), and heap_update, for HEAP_SELECTIVE_INDEX_UPDATE, keeps the new version on the HOT chain while ExecInsertIndexTuples maintains only the indexes whose attributes changed. The new heap-only tuple records, in an inline bitmap in its tail, the attributes that changed at its hop. Only the stored tuple carries the bitmap and the HEAP_INDEXED_UPDATED flag; the caller's in-memory copy is left unmarked so the flag never promises a trailing bitmap that is not present. Read path: a chain walk to the live tuple unions the modified-attribute bitmaps of every hop it crosses. The index-access layer treats that crossed-attribute bitmap as the staleness authority: if it overlaps the arriving index's key columns the entry is stale and is dropped, and the row is re-supplied by the fresh entry the same update planted. The read path is access-method agnostic and needs no value recheck or leaf key: it is correct even when a key is cycled away and back, because the value-restoring update planted a fresh entry whose walk crosses no later key-changing hop. Unique checks are the one place that does compare values: _bt_check_unique fetches the conflicting tuple under SnapshotDirty and, on a crossed-hop arrival, compares the live tuple's current key against the arriving leaf with the index's own ordering procedure (_bt_heap_keys_equal_leaf, BTORDER_PROC under each column's collation). Using the opclass comparator -- not a bitwise image comparison -- distinguishes a stale ancestor leaf from a genuinely live duplicate (equal under the opclass even if not bitwise-identical) and, in the in-flight window of a restoring update, routes the stale-ancestor hit into _bt_doinsert's xwait so the duplicate is still caught. The comparison reads plain key columns straight from the heap slot; it never evaluates an indexed expression, because an UPDATE touching an expression-index attribute is ineligible for HOT-indexed, so an expression index is never the one receiving the fresh entry whose insert runs this check. Co-authored-by: Greg Burd <greg@burd.me> Co-authored-by: Nathan Bossart <nathandbossart@gmail.com>
A HOT-indexed (SIU) update's fresh entry in a changed index points at the new heap-only tuple, not at the chain root the way every other index entry for the same logical row does -- that positional distinction is what lets the read side judge staleness from the crossed-attribute bitmap without a value recheck. BitmapAnd/BitmapOr combine two indexes' TID sets at raw block+offset granularity in tidbitmap.c, before either side ever touches the heap. An unrelated, unchanged index's root-pointing entry for the same row will not agree with the changed index's fresh entry's offset, so an exact-mode intersection can silently drop a row that matches both predicates. Reported by Alexander Korotkov. Fixed by reserving one otherwise-unused bit (bit 14) in a stored TID's offset field, ItemPointerSIUMaybeStaleFlag. MaxOffsetNumber never needs more than 14 bits even at the largest configurable BLCKSZ, so the bit is free for any real offset; it is set only on the local TID copy handed to a HOT-indexed fresh entry's index_insert() call in ExecInsertIndexTuples, never on the slot's own tts_tid. ItemPointerGetOffsetNumber and ItemPointerCompare strip the bit by default (via the sentinel-safe ItemPointerOffsetNumberStrip, which leaves SpecTokenOffsetNumber/MovedPartitionsOffsetNumber -- both of which already have this bit set as part of their own encoding -- untouched) so every ordinary consumer keeps seeing the real offset; only ItemPointerGetOffsetNumberNoCheck exposes the raw value. The consumption is centralized in tbm_add_tuples(), the single choke point every amgetbitmap funnels exact heap TIDs through: it tests the raw flag (before the offset is stripped) and, when set, adds the whole page as lossy (tbm_add_page) instead of the single exact offset. Per tbm_intersect_page's own case analysis a lossy page survives any AND/OR against an exact-mode page and forces a recheck, so BitmapHeapScan resolves the chain and the existing heap-side crossed-attribute staleness test makes the final, correct call. Because this lives in tbm_add_tuples and not in each access method, no index AM needs to know about HOT-indexed chains: btree, hash, GIN, GiST, SP-GiST, contrib/bloom, and out-of-tree AMs are all correct with no AM-specific code, and a TID that never carries the flag takes the identical path it always did. GIN's own page-level lossy sentinel (ItemPointerIsLossyPage, an unrelated 0xffff marker used before a real heap TID is produced) is untouched; the new check only applies to genuine heap-item TIDs. The cost is precision, not correctness: any heap page carrying a live fresh entry contributes lossy (a whole-page recheck for all its tuples on that bitmap scan, not just the SIU row) until the chain collapses. It is bounded and self-healing -- prune/VACUUM collapse restores exact-mode entries. amcheck's heapallindexed verification fingerprints leaf tuples' stored TIDs and compares them against the plain heap TIDs it re-derives from the heap scan; verify_nbtree.c now strips the marker while fingerprinting so a fresh entry does not raise a spurious "lacks matching index tuple". pageinspect 1.14's bt_page_items reports the real offset in its ctid and htid columns (earlier versions surfaced the marker as an inflated offset) and adds a hot_indexed boolean column exposing the marker explicitly. Caught its own regression during development: the first cut masked bit 14 unconditionally, which corrupted SpecTokenOffsetNumber and MovedPartitionsOffsetNumber (both already have bit 14 set), silently breaking cross-partition-UPDATE conflict detection -- caught by the isolation suite (eval-plan-qual, merge-update, partition-key-update). Fixed by gating the strip on the value being below the sentinel range. Regression coverage (BitmapAnd/BitmapOr across a changed+unchanged index for every access method SIU exercises, plus a bloom case in contrib/bloom and heapallindexed on a changed index) is added alongside the rest of the HOT-indexed test suite.
A HOT-indexed update plants index entries that point at mid-chain heap-only tuples, so a dead chain member cannot simply be removed: a not-yet-swept index entry may still arrive at it, and the per-hop modified-attrs bitmap on it is what a reader unions to judge staleness. Teach prune to collapse a dead chain prefix into xid-free forwarding stubs: each preserved dead key tuple is rewritten in place to a stub (frozen, natts == 0, HEAP_INDEXED_UPDATED, forwarding via t_ctid.offnum) that keeps its segment's modified-attrs bitmap, and a member whose attributes are wholly subsumed by later hops is reclaimed instead. Readers step through stubs transparently and still cross every surviving hop's bitmap. The collapse back to classic HOT is driven by prune: once a chain is fully dead, a later prune (heap_prune_chain / heap_prune_chain_find_live) reclaims its members and re-points the root redirect straight at the first live tuple. VACUUM's index cleanup sweeps the stale leaves; its second pass (lazy_vacuum_heap_page) does the usual LP_DEAD -> LP_UNUSED conversion and leaves the HOT-indexed collapse to prune. The collapse reuses the existing prune/freeze WAL via an xlhp_prune_items sub-record carrying the (offset, forward) stub pairs; no new record type is introduced. A page that still carries a preserved stub (or a redirect that forwards into a live HOT-indexed member) is kept non-all-visible so index-only scans heap-fetch through the chain; heap_page_would_be_all_visible recognizes both the redirect-to-SIU and the stub case explicitly. Co-authored-by: Greg Burd <greg@burd.me> Co-authored-by: Nathan Bossart <nathandbossart@gmail.com>
verify_heapam must not flag the HOT-indexed artifacts as corruption: a live HEAP_INDEXED_UPDATED heap-only tuple whose mid-chain line pointer is preserved because an index entry still points at it, an xid-free collapse-survivor stub, and more than one LP_REDIRECT forwarding to the same live tuple are all legitimate. Recognize them and continue checking the rest of the chain. Cover this with an amcheck regression test, and add a pg_upgrade test that carries a relation with HOT-indexed chains, an ABA-cycled indexed column, an out-of-line indexed column, and VACUUM-collapsed stubs across an upgrade, verifying the data, verify_heapam, bt_index_check, and the chain scans on the new cluster. Authored-by: Greg Burd <greg@burd.me>
Expose the HOT-indexed activity counters maintained by the write path: pg_stat_all_tables.n_tup_hot_indexed_upd, the per-index n_tup_hot_indexed_upd_matched / n_tup_hot_indexed_upd_skipped counters in pg_stat_all_indexes, and pg_relation_hot_indexed_stats() reporting per-relation HOT-indexed chain composition. Document them in monitoring.sgml and the README. With statistics, prune/collapse, and amcheck recognition all in place, add the full feature test suite, which uses those facilities to verify behavior: - hot_indexed_updates (regression): eligibility and classification; selective maintenance across multiple/composite indexes; the crossed-attribute read path for equality, range, and inequality scans; a key cycled away and back (ABA), including across two distinct live rows; TOASTed indexed columns; partial-index predicate flips (key and non-key predicate columns); trigger-modified indexed columns; exclusion-constraint tables; partitioned tables; non-btree access methods (hash, GIN, GiST); a UNIQUE index on a type where image equality differs from operator equality; CREATE INDEX / REINDEX and DROP INDEX over live chains; prune reclamation, stub mixes, and re-collapse across partial VACUUMs; the never-all-visible guard; and DDL after a chain exists (ADD COLUMN crossing a bitmap-size boundary, DROP COLUMN). - hot_indexed_adversarial (isolation): concurrent UPDATE / VACUUM / prune and index scans, key cycling, aborts, and reader consistency across a concurrent collapse. - 054_hot_indexed_recovery (recovery): WAL replay of the chain and its collapse under wal_consistency_checking. - pg_surgery handling of HOT-indexed tuples and collapse-survivor stubs. Authored-by: Greg Burd <greg@burd.me>
A HOT-indexed update of a replica-identity attribute on a subscriber leaves a stale index leaf that the apply worker's replica-identity lookups must tolerate -- which they do, but only when the subscriber's indexed attributes do not extend past the columns those lookups key on. Add the per-subscription hot_indexed_on_apply option (subhotindexedonapply: off / subset_only (default) / always) and have HeapUpdateHotAllowable consult it when running in an apply worker, comparing the relation's indexed-attribute set against its primary-key attributes: "off" disqualifies HOT-indexed whenever any indexed attribute lies outside the primary key, "subset_only" requires the indexed attributes to be a subset of the primary key, and "always" applies no apply-path gating. Wire the option through CREATE/ALTER SUBSCRIPTION, pg_subscription, pg_dump, and psql's \dRs+, and document it (create_subscription, alter_subscription, catalogs). Cover apply under each mode (039), apply under REPLICA IDENTITY FULL and a non-PK USING INDEX whose key is cycled (040), and decoding of HOT-indexed update chains (test_decoding). Authored-by: Greg Burd <greg@burd.me>
A/B and single-variant benchmark scripts for HOT-indexed updates: build two postgres variants, run pgbench workloads exercising classic-HOT, non-HOT, and HOT-indexed paths, and a self-contained bloat probe that reports the skip count (index writes avoided on unchanged indexes) and changed-index bounding. Not for merge; kept for evaluating the feature.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.