From 348aeadbff5e2f570050276a7103d228277ff38d Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Mon, 19 Oct 2026 19:10:37 -0400 Subject: [PATCH v43 5/8] Add amcomparedatums index AM callback for HOT updates An UPDATE that changes a column an index stores directly, as a key or included column, always gets new index entries, even when the index would store exactly what it already does. Only the index's access method can tell: a hash index stores a hash code, a GIN index the keys its operator class extracts. Index access methods for documents such as RUM store keys for a few paths of a document chosen by operator class options, so to core every change to the document looks like a change to the index. Add an optional callback, bool amcomparedatums(Relation index, IndexInfo *indexInfo, int attnum, Oid typid, int16 typlen, bool typbyval, Datum oldValue, Datum newValue); which says whether the new, non-null value of an index column gives exactly the same index entries as the old one. ExecUpdateModifiedIdxAttrs already narrows the set of changed indexed attributes for those used only through expressions or partial-index predicates, by forming each such index's datums for the old and new row and comparing them byte for byte. Treat a column that indexes also store directly the same way, if all of those indexes have the callback, and ask the callback whenever an index datum differs byte for byte. The new relcache bitmap INDEX_ATTR_BITMAP_COMPARABLE names those columns. The callback is not asked about a column the index can return in index-only scans (amcanreturn): the index stores the value itself there, so only the same value gives the same entries, and an access method that learns to return a column later cannot be left giving a wrong answer. The columns of candidate keys and of the replica identity are left out: the table AM takes its tuple lock strength and what it logs for logical decoding from the changed set, and those stay decided by value. So are the indexes of exclusion constraints: the constraint is checked on the values when an entry is inserted, and two values that give the same entries need not have the same conflicts. No core access method implements the callback yet, so nothing changes until one does. dummy_index_am gets one, configured by its options, to test the executor side. --- contrib/bloom/blutils.c | 1 + doc/src/sgml/indexam.sgml | 51 ++++++ doc/src/sgml/storage.sgml | 8 +- src/backend/access/brin/brin.c | 1 + src/backend/access/gin/ginutil.c | 1 + src/backend/access/gist/gist.c | 1 + src/backend/access/hash/hash.c | 1 + src/backend/access/heap/README.HOT | 16 +- src/backend/access/heap/heapam.c | 16 +- src/backend/access/nbtree/nbtree.c | 1 + src/backend/access/spgist/spgutils.c | 1 + src/backend/executor/nodeModifyTable.c | 88 +++++++--- src/backend/utils/cache/relcache.c | 49 ++++++ src/include/access/amapi.h | 10 ++ src/include/nodes/execnodes.h | 3 +- src/include/utils/rel.h | 3 + src/include/utils/relcache.h | 1 + src/test/modules/dummy_index_am/Makefile | 2 +- .../modules/dummy_index_am/dummy_index_am.c | 38 ++++- .../dummy_index_am/expected/comparedatums.out | 152 ++++++++++++++++++ src/test/modules/dummy_index_am/meson.build | 1 + .../dummy_index_am/sql/comparedatums.sql | 82 ++++++++++ src/tools/pgindent/typedefs.list | 1 + 23 files changed, 492 insertions(+), 36 deletions(-) create mode 100644 src/test/modules/dummy_index_am/expected/comparedatums.out create mode 100644 src/test/modules/dummy_index_am/sql/comparedatums.sql diff --git a/contrib/bloom/blutils.c b/contrib/bloom/blutils.c index 5111cdc6dd6..9fa7710739b 100644 --- a/contrib/bloom/blutils.c +++ b/contrib/bloom/blutils.c @@ -155,6 +155,7 @@ blhandler(PG_FUNCTION_ARGS) .amparallelrescan = NULL, .amtranslatestrategy = NULL, .amtranslatecmptype = NULL, + .amcomparedatums = NULL, }; PG_RETURN_POINTER(&amroutine); diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml index f48da318530..d91192bc011 100644 --- a/doc/src/sgml/indexam.sgml +++ b/doc/src/sgml/indexam.sgml @@ -180,6 +180,9 @@ typedef struct IndexAmRoutine /* interface functions to support planning */ amtranslate_strategy_function amtranslatestrategy; /* can be NULL */ amtranslate_cmptype_function amtranslatecmptype; /* can be NULL */ + + /* interface function to support HOT updates */ + amcomparedatums_function amcomparedatums; /* can be NULL */ } IndexAmRoutine; @@ -915,6 +918,54 @@ amtranslatecmptype (CompareType cmptype, Oid opfamily, Oid opcintype); fully functional. + + +bool +amcomparedatums (Relation indexRelation, + IndexInfo *indexInfo, + int attnum, + Oid typid, + int16 typlen, + bool typbyval, + Datum oldValue, + Datum newValue); + + Would inserting a row whose index column attnum + (counting from 1) has the value newValue make + exactly the same index entries as oldValue made? + The values are not null, are of type typid (the + type of the table column or index expression, which need not be the type + the index stores), whose length and by-value property are + typlen and typbyval, and + are not equal byte for byte. The function is + called during UPDATE, before the table access method + is, for every index that uses a column whose value changed; if every such + index answers true for every changed column, the update + can be a heap-only tuple (HOT) update, which makes no new + index entries at all (see ). The index then + keeps the entries it made for the old value, and reaches the new row + through them. indexInfo is the one + aminsert would get for the new row, so the function + may use ii_AmCache in the same way. + + + + So true is only correct if no scan can tell the old + entries from the ones the new value would make: they must have the same + keys, compared as the index compares them, and anything a scan returns + from them must be the same. The function is not called for a column the + index can return in index-only scans (see amcanreturn): + the index stores the value itself there, so only the same value gives the + same entries. If the function cannot be sure, it must return false, which + is always correct. It must not modify the index, and it may be called for + an index that is not yet valid. Columns of unique indexes that can be + referenced by a foreign key, and of the replica identity index, are not + compared this way, and the function is not called for the index of an + exclusion constraint, whose check needs the values themselves. If amcomparedatums is + NULL, a change to a column that the index stores + always needs new index entries. + + diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml index 4523d073a19..15b5dae1752 100644 --- a/doc/src/sgml/storage.sgml +++ b/doc/src/sgml/storage.sgml @@ -1120,7 +1120,13 @@ data. Empty in ordinary tables. SET clause changes only parts of the column that the indexes do not read, for example through jsonb_set or array subscripting, this is known without evaluating the indexes' - expressions. + expressions. A column that indexes store directly is treated the same + way if the access method of each such index can tell whether the new + value gives the same index entries as the old one; among the built-in + methods, that depends on the method and on the change (see + ). This does not apply to the columns of + unique indexes that a foreign key can reference, of the replica + identity index, or of exclusion constraints. diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c index 5059da8dce7..83da4850ddc 100644 --- a/src/backend/access/brin/brin.c +++ b/src/backend/access/brin/brin.c @@ -307,6 +307,7 @@ brinhandler(PG_FUNCTION_ARGS) .amparallelrescan = NULL, .amtranslatestrategy = NULL, .amtranslatecmptype = NULL, + .amcomparedatums = NULL, }; PG_RETURN_POINTER(&amroutine); diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c index e7cba81d477..b57c016aea2 100644 --- a/src/backend/access/gin/ginutil.c +++ b/src/backend/access/gin/ginutil.c @@ -90,6 +90,7 @@ ginhandler(PG_FUNCTION_ARGS) .amestimateparallelscan = NULL, .aminitparallelscan = NULL, .amparallelrescan = NULL, + .amcomparedatums = NULL, }; PG_RETURN_POINTER(&amroutine); diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c index 8565e225be7..0d562466821 100644 --- a/src/backend/access/gist/gist.c +++ b/src/backend/access/gist/gist.c @@ -112,6 +112,7 @@ gisthandler(PG_FUNCTION_ARGS) .amparallelrescan = NULL, .amtranslatestrategy = NULL, .amtranslatecmptype = gisttranslatecmptype, + .amcomparedatums = NULL, }; PG_RETURN_POINTER(&amroutine); diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c index 63846c10840..0b05c4606e1 100644 --- a/src/backend/access/hash/hash.c +++ b/src/backend/access/hash/hash.c @@ -123,6 +123,7 @@ hashhandler(PG_FUNCTION_ARGS) .amparallelrescan = NULL, .amtranslatestrategy = hashtranslatestrategy, .amtranslatecmptype = hashtranslatecmptype, + .amcomparedatums = NULL, }; PG_RETURN_POINTER(&amroutine); diff --git a/src/backend/access/heap/README.HOT b/src/backend/access/heap/README.HOT index 0377f508055..5c2dfc27f58 100644 --- a/src/backend/access/heap/README.HOT +++ b/src/backend/access/heap/README.HOT @@ -41,10 +41,15 @@ A column that indexes reference only through expressions, or only as partial indexes, may change if the update changes nothing those indexes store: not the value of any of their expressions or columns, as compared byte for byte, and not whether the tuple satisfies their predicates. -The executor works that out before calling the table AM, and passes on -only the indexed columns whose change matters. An index that is not yet -ready for inserts, or whose build failed, counts as changed whenever a -column it references does; see "CREATE INDEX CONCURRENTLY" below. +The same holds for a column that indexes also store directly, if every +index that does so has an access method that can tell, from the old and +new values, that it would store the same entries (amcomparedatums), and +the column is in no candidate key, replica identity or exclusion +constraint. The executor works that out before calling the table AM, +and passes on only the indexed columns whose change matters. An index +that is not yet ready for inserts, or whose build failed, counts as +changed whenever a column it references does; see "CREATE INDEX +CONCURRENTLY" below. Second, where the modified columns are only used in indexes that do not contain tuple IDs, but maintain summaries of the indexed data by block. @@ -506,7 +511,8 @@ Indexed column actually be stored in the index --- it could be used in a functional index's expression, or used in a partial index predicate. A change to a column that indexes use only in these - ways blocks HOT only if it changes what those indexes store. + ways, or whose indexes can compare entries, blocks HOT only if it + changes what those indexes store. Redirecting line pointer diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index e0cd1ab7612..a0e2d904beb 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -3362,6 +3362,13 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup, check_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_INDEXED); narrowable_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_EXPRESSION); + { + Bitmapset *cmp_attrs = RelationGetIndexAttrBitmap(relation, + INDEX_ATTR_BITMAP_COMPARABLE); + + narrowable_attrs = bms_add_members(narrowable_attrs, cmp_attrs); + bms_free(cmp_attrs); + } #endif block = ItemPointerGetBlockNumber(otid); @@ -3801,10 +3808,11 @@ l2: * attribute comparing the two tuples here finds changed is in the * caller's set, so that a caller that misses a change, or compares the * wrong tuples, is caught. The caller may leave out an attribute that - * indexes use only through expressions or as partial indexes, having - * found that no index's entry changes (see ExecUpdateModifiedIdxAttrs()), - * and may report more: comparing slots, it cannot always tell that two - * values will be stored alike. + * indexes use only through expressions or as partial indexes, or only in + * indexes that can compare entries, having found that no index's entry + * changes (see ExecUpdateModifiedIdxAttrs()), and may report more: + * comparing slots, it cannot always tell that two values will be stored + * alike. */ { Bitmapset *changed = HeapUpdateModifiedIdxAttrs(relation, check_attrs, diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c index 0abdd7b49f5..754ddaee71d 100644 --- a/src/backend/access/nbtree/nbtree.c +++ b/src/backend/access/nbtree/nbtree.c @@ -171,6 +171,7 @@ bthandler(PG_FUNCTION_ARGS) .amparallelrescan = btparallelrescan, .amtranslatestrategy = bttranslatestrategy, .amtranslatecmptype = bttranslatecmptype, + .amcomparedatums = NULL, }; PG_RETURN_POINTER(&amroutine); diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c index f2ee333f60d..985a9474c9c 100644 --- a/src/backend/access/spgist/spgutils.c +++ b/src/backend/access/spgist/spgutils.c @@ -97,6 +97,7 @@ spghandler(PG_FUNCTION_ARGS) .amparallelrescan = NULL, .amtranslatestrategy = NULL, .amtranslatecmptype = NULL, + .amcomparedatums = NULL, }; PG_RETURN_POINTER(&amroutine); diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 25bfb9bce55..4054f6da0df 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -54,6 +54,8 @@ #include "postgres.h" +#include "access/amapi.h" +#include "access/genam.h" #include "access/htup_details.h" #include "access/tableam.h" #include "access/tupconvert.h" @@ -198,41 +200,58 @@ static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context, bool canSetTag); /* - * Do the old and new index datums differ, byte for byte? Compare each - * column as the type FormIndexDatum() produced it in: the table column's - * type or the expression's, which need not be the type the index stores. + * Do the old and new index datums differ? Compare each column as the type + * FormIndexDatum() produced it in: the table column's type or the + * expression's, which need not be the type the index stores. Datums that + * differ byte for byte may still give the index the same entries; if its + * access method can tell (amcomparedatums), ask it, but not for a column the + * index can return in index-only scans, where only the same value will do. + * + * Not for an exclusion constraint's index, though: the constraint is checked + * when an entry is inserted, with its operators, against the values, and two + * values that give the same entries need not have the same conflicts. (A + * value that is the same byte for byte has, so the check is not needed.) */ static bool -IndexDatumsDiffer(Relation heapRel, IndexInfo *indexInfo, +IndexDatumsDiffer(Relation heapRel, Relation indexRel, IndexInfo *indexInfo, const Datum *old_values, const bool *old_isnull, const Datum *new_values, const bool *new_isnull) { ListCell *expr = list_head(indexInfo->ii_Expressions); + amcomparedatums_function cmp = indexRel->rd_index->indisexclusion ? NULL : + indexRel->rd_indam->amcomparedatums; for (int i = 0; i < indexInfo->ii_NumIndexAttrs; i++) { AttrNumber attnum = indexInfo->ii_IndexAttrNumbers[i]; + Oid typid; int16 typlen; bool typbyval; if (attnum != 0) { - CompactAttribute *att = TupleDescCompactAttr(RelationGetDescr(heapRel), - attnum - 1); + Form_pg_attribute att = TupleDescAttr(RelationGetDescr(heapRel), + attnum - 1); + typid = att->atttypid; typlen = att->attlen; typbyval = att->attbyval; } else { - get_typlenbyval(exprType(lfirst(expr)), &typlen, &typbyval); + typid = exprType(lfirst(expr)); + get_typlenbyval(typid, &typlen, &typbyval); expr = lnext(indexInfo->ii_Expressions, expr); } if (old_isnull[i] != new_isnull[i]) return true; - if (!old_isnull[i] && - !datumIsEqual(old_values[i], new_values[i], typbyval, typlen)) + if (old_isnull[i] || + datumIsEqual(old_values[i], new_values[i], typbyval, typlen)) + continue; + if (cmp == NULL || index_can_return(indexRel, i + 1) || + !cmp(indexRel, indexInfo, i + 1, typid, typlen, typbyval, + old_values[i], new_values[i])) return true; } return false; @@ -248,9 +267,9 @@ IndexDatumsDiffer(Relation heapRel, IndexInfo *indexInfo, * it in the query context. */ static bool -IndexEntryChanges(ResultRelInfo *resultRelInfo, IndexInfo *indexInfo, - TupleTableSlot *old_tts, TupleTableSlot *new_tts, - EState *estate) +IndexEntryChanges(ResultRelInfo *resultRelInfo, Relation indexRel, + IndexInfo *indexInfo, TupleTableSlot *old_tts, + TupleTableSlot *new_tts, EState *estate) { ExprContext *econtext = GetPerTupleExprContext(estate); TupleTableSlot *save_scantuple = econtext->ecxt_scantuple; @@ -294,16 +313,18 @@ IndexEntryChanges(ResultRelInfo *resultRelInfo, IndexInfo *indexInfo, FormIndexDatum(indexInfo, new_tts, estate, new_values, new_isnull); econtext->ecxt_scantuple = save_scantuple; - result = IndexDatumsDiffer(resultRelInfo->ri_RelationDesc, indexInfo, - old_values, old_isnull, new_values, new_isnull); + result = IndexDatumsDiffer(resultRelInfo->ri_RelationDesc, indexRel, + indexInfo, old_values, old_isnull, + new_values, new_isnull); MemoryContextSwitchTo(oldcxt); return result; } /* * Return the attributes of the i'th index that INDEX_ATTR_BITMAP_EXPRESSION - * can contain: those it references through its expressions, and for a - * partial index all those it references. Builds the per-index cache on + * or INDEX_ATTR_BITMAP_COMPARABLE can contain: those it references through + * its expressions, and all those it references if it is a partial index or + * its access method can compare entries. Builds the per-index cache on * first use. */ static Bitmapset * @@ -321,9 +342,12 @@ IndexExprAttrs(ResultRelInfo *resultRelInfo, int i, EState *estate) Bitmapset *attrs = NULL; pull_varattnos((Node *) ii->ii_Expressions, 1, &attrs); - if (ii->ii_Predicate != NIL) + pull_varattnos((Node *) ii->ii_Predicate, 1, &attrs); + if (ii->ii_Predicate != NIL || + (resultRelInfo->ri_IndexRelationDescs[j] != NULL && + resultRelInfo->ri_IndexRelationDescs[j]->rd_indam->amcomparedatums != NULL && + !resultRelInfo->ri_IndexRelationDescs[j]->rd_index->indisexclusion)) { - pull_varattnos((Node *) ii->ii_Predicate, 1, &attrs); for (int k = 0; k < ii->ii_NumIndexAttrs; k++) { if (ii->ii_IndexAttrNumbers[k] != 0) @@ -376,7 +400,9 @@ IndexUnaffected(ResultRelInfo *resultRelInfo, int i, Bitmapset *mine, #ifdef USE_ASSERT_CHECKING if (proven) { - if (IndexEntryChanges(resultRelInfo, indexInfo, old_tts, new_tts, + if (IndexEntryChanges(resultRelInfo, + resultRelInfo->ri_IndexRelationDescs[i], + indexInfo, old_tts, new_tts, estate)) elog(ERROR, "index \"%s\" proven unaffected by UPDATE, but its entry changed", RelationGetRelationName(resultRelInfo->ri_IndexRelationDescs[i])); @@ -387,7 +413,9 @@ IndexUnaffected(ResultRelInfo *resultRelInfo, int i, Bitmapset *mine, return true; #endif - return !IndexEntryChanges(resultRelInfo, indexInfo, old_tts, new_tts, + return !IndexEntryChanges(resultRelInfo, + resultRelInfo->ri_IndexRelationDescs[i], + indexInfo, old_tts, new_tts, estate); } @@ -423,6 +451,12 @@ IndexUnaffected(ResultRelInfo *resultRelInfo, int i, Bitmapset *mine, * update as if it had not changed the attribute: no index needs a new * entry because of it. * + * An attribute that non-summarizing indexes also reference directly, as key + * or included columns, is a candidate too if all of those indexes' access + * methods can compare entries (INDEX_ATTR_BITMAP_COMPARABLE): such an index + * is unaffected when, column by column, its datums are equal or its access + * method's amcomparedatums says they give the same entries. + * * An index that is not ready for inserts or not valid counts as affected by * any change to an attribute it references, without evaluating anything. * Such an index is being built, or its build failed, so the old row may be @@ -430,9 +464,10 @@ IndexUnaffected(ResultRelInfo *resultRelInfo, int i, Bitmapset *mine, * would make the UPDATE fail. It still constrains HOT, as README.HOT * requires. * - * Narrowing never removes an attribute of a key or replica identity index, - * as those have neither expressions nor predicates, so the table AM may - * also take its lock mode and replica identity decisions from the result. + * Narrowing never removes an attribute of a key or replica identity index: + * those have neither expressions nor predicates, and their columns are left + * out of INDEX_ATTR_BITMAP_COMPARABLE. So the table AM may also take its + * lock mode and replica identity decisions from the result. */ Bitmapset * ExecUpdateModifiedIdxAttrs(ResultRelInfo *resultRelInfo, @@ -461,6 +496,13 @@ ExecUpdateModifiedIdxAttrs(ResultRelInfo *resultRelInfo, */ exprattrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_EXPRESSION); + { + Bitmapset *cmpattrs = RelationGetIndexAttrBitmap(relation, + INDEX_ATTR_BITMAP_COMPARABLE); + + exprattrs = bms_add_members(exprattrs, cmpattrs); + bms_free(cmpattrs); + } candidates = bms_intersect(attrs, exprattrs); if (bms_is_empty(candidates)) { diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index c0941182b91..f93c456c86c 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -30,6 +30,7 @@ #include #include +#include "access/genam.h" #include "access/htup_details.h" #include "access/multixact.h" #include "access/parallel.h" @@ -2479,6 +2480,7 @@ RelationDestroyRelation(Relation relation, bool remember_tupdesc) bms_free(relation->rd_summarizedattr); bms_free(relation->rd_indexedattr); bms_free(relation->rd_exprattr); + bms_free(relation->rd_cmpattr); if (relation->rd_pubdesc) pfree(relation->rd_pubdesc); if (relation->rd_options) @@ -5331,6 +5333,11 @@ RelationGetIndexPredicate(Relation relation) * INDEX_ATTR_BITMAP_EXPRESSION Columns that non-summarizing indexes * reference only through expressions or * as partial indexes + * INDEX_ATTR_BITMAP_COMPARABLE Columns that non-summarizing indexes + * reference directly only if their + * access method can compare entries + * (amcomparedatums), less key and replica + * identity columns * * Attribute numbers are offset by FirstLowInvalidHeapAttributeNumber so that * we can include system attributes (e.g., OID) in the bitmap representation. @@ -5358,6 +5365,9 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind) Bitmapset *indexedattrs; /* columns referenced by any index */ Bitmapset *exprattrs; /* columns of expression or partial * non-summarizing indexes */ + Bitmapset *cmpattrs; /* plain columns of indexes whose AM can + * compare entries (amcomparedatums) */ + Bitmapset *nocmpattrs; /* plain columns of indexes whose AM cannot */ Bitmapset *plainattrs; /* columns of other non-summarizing indexes, * and plain columns of expression indexes */ List *indexoidlist; @@ -5386,6 +5396,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind) return bms_copy(relation->rd_indexedattr); case INDEX_ATTR_BITMAP_EXPRESSION: return bms_copy(relation->rd_exprattr); + case INDEX_ATTR_BITMAP_COMPARABLE: + return bms_copy(relation->rd_cmpattr); default: elog(ERROR, "unknown attrKind %u", attrKind); } @@ -5431,6 +5443,8 @@ restart: hotblockingattrs = NULL; summarizedattrs = NULL; exprattrs = NULL; + cmpattrs = NULL; + nocmpattrs = NULL; plainattrs = NULL; foreach(l, indexoidlist) { @@ -5523,6 +5537,14 @@ restart: else plainattrs = bms_add_member(plainattrs, attrnum - FirstLowInvalidHeapAttributeNumber); + if (indexDesc->rd_indam->amcomparedatums != NULL && + !indexDesc->rd_index->indisexclusion && + !index_can_return(indexDesc, i + 1)) + cmpattrs = bms_add_member(cmpattrs, + attrnum - FirstLowInvalidHeapAttributeNumber); + else + nocmpattrs = bms_add_member(nocmpattrs, + attrnum - FirstLowInvalidHeapAttributeNumber); } if (isKey && i < indexDesc->rd_index->indnkeyatts) @@ -5580,6 +5602,8 @@ restart: bms_free(hotblockingattrs); bms_free(summarizedattrs); bms_free(exprattrs); + bms_free(cmpattrs); + bms_free(nocmpattrs); bms_free(plainattrs); goto restart; @@ -5599,6 +5623,26 @@ restart: exprattrs = bms_del_members(exprattrs, summarizedattrs); bms_free(plainattrs); + /* + * Record what attributes non-summarizing indexes also reference directly, + * as key or included columns, only in indexes whose access method can + * tell whether a new value gives the same entries (amcomparedatums). A + * change to one of these needs new index entries only if, for some index, + * it does not. Like those above, and for the same reason, these leave + * out what summarizing indexes reference. They also leave out the + * columns of candidate keys and the replica identity, whose changes are + * decided by value as before: the table AM takes its tuple lock strength + * and what it logs for logical decoding from them. Indexes of exclusion + * constraints count as unable to compare: the constraint is checked on + * the values when an entry is inserted, and values that give the same + * entries need not have the same conflicts. + */ + cmpattrs = bms_del_members(cmpattrs, nocmpattrs); + cmpattrs = bms_del_members(cmpattrs, summarizedattrs); + cmpattrs = bms_del_members(cmpattrs, uindexattrs); + cmpattrs = bms_del_members(cmpattrs, idindexattrs); + bms_free(nocmpattrs); + /* Don't leak the old values of these bitmaps, if any */ relation->rd_attrsvalid = false; bms_free(relation->rd_keyattr); @@ -5615,6 +5659,8 @@ restart: relation->rd_indexedattr = NULL; bms_free(relation->rd_exprattr); relation->rd_exprattr = NULL; + bms_free(relation->rd_cmpattr); + relation->rd_cmpattr = NULL; /* * Now save copies of the bitmaps in the relcache entry. We intentionally @@ -5631,6 +5677,7 @@ restart: relation->rd_summarizedattr = bms_copy(summarizedattrs); relation->rd_indexedattr = bms_copy(indexedattrs); relation->rd_exprattr = bms_copy(exprattrs); + relation->rd_cmpattr = bms_copy(cmpattrs); relation->rd_attrsvalid = true; MemoryContextSwitchTo(oldcxt); @@ -5651,6 +5698,8 @@ restart: return indexedattrs; case INDEX_ATTR_BITMAP_EXPRESSION: return exprattrs; + case INDEX_ATTR_BITMAP_COMPARABLE: + return cmpattrs; default: elog(ERROR, "unknown attrKind %u", attrKind); return NULL; diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h index 79240333530..f8e1b8197ae 100644 --- a/src/include/access/amapi.h +++ b/src/include/access/amapi.h @@ -109,6 +109,13 @@ typedef CompareType (*amtranslate_strategy_function) (StrategyNumber strategy, O /* translate general operator types to AM-specific strategies */ typedef StrategyNumber (*amtranslate_cmptype_function) (CompareType cmptype, Oid opfamily); +/* would two non-null values of an index column give the same entries? */ +typedef bool (*amcomparedatums_function) (Relation indexRelation, + IndexInfo *indexInfo, + int attnum, Oid typid, + int16 typlen, bool typbyval, + Datum oldValue, Datum newValue); + /* build new index */ typedef IndexBuildResult *(*ambuild_function) (Relation heapRelation, Relation indexRelation, @@ -323,6 +330,9 @@ typedef struct IndexAmRoutine /* interface functions to support planning */ amtranslate_strategy_function amtranslatestrategy; /* can be NULL */ amtranslate_cmptype_function amtranslatecmptype; /* can be NULL */ + + /* interface function to support HOT updates */ + amcomparedatums_function amcomparedatums; /* can be NULL */ } IndexAmRoutine; diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 716668c5aad..7c914ee001d 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -504,7 +504,8 @@ typedef struct ResultRelInfo /* * For UPDATE, the table attributes each index references through - * expressions or its predicate, offset by + * expressions or its predicate, and directly if the index is partial or + * its access method has amcomparedatums, offset by * FirstLowInvalidHeapAttributeNumber. Built on first use; NULL until * then. */ diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index fdd2a1e3213..b882c0e50f8 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -167,6 +167,9 @@ typedef struct RelationData Bitmapset *rd_indexedattr; /* cols referenced by any index */ Bitmapset *rd_exprattr; /* cols non-summarizing indexes reference only * via expressions or as partial indexes */ + Bitmapset *rd_cmpattr; /* cols indexed directly only by indexes with + * amcomparedatums, less key and replica + * identity cols */ PublicationDesc *rd_pubdesc; /* publication descriptor, or NULL */ diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h index 955d0186c2d..26fd0af54dd 100644 --- a/src/include/utils/relcache.h +++ b/src/include/utils/relcache.h @@ -74,6 +74,7 @@ typedef enum IndexAttrBitmapKind INDEX_ATTR_BITMAP_SUMMARIZED, INDEX_ATTR_BITMAP_INDEXED, INDEX_ATTR_BITMAP_EXPRESSION, + INDEX_ATTR_BITMAP_COMPARABLE, } IndexAttrBitmapKind; extern Bitmapset *RelationGetIndexAttrBitmap(Relation relation, diff --git a/src/test/modules/dummy_index_am/Makefile b/src/test/modules/dummy_index_am/Makefile index aaf544a3854..6ca1f96d655 100644 --- a/src/test/modules/dummy_index_am/Makefile +++ b/src/test/modules/dummy_index_am/Makefile @@ -6,7 +6,7 @@ EXTENSION = dummy_index_am DATA = dummy_index_am--1.0.sql PGFILEDESC = "dummy_index_am - index access method template" -REGRESS = reloptions +REGRESS = comparedatums reloptions ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/src/test/modules/dummy_index_am/dummy_index_am.c b/src/test/modules/dummy_index_am/dummy_index_am.c index 31f8d2b8161..18fe386f2cc 100644 --- a/src/test/modules/dummy_index_am/dummy_index_am.c +++ b/src/test/modules/dummy_index_am/dummy_index_am.c @@ -16,8 +16,10 @@ #include "access/amapi.h" #include "access/reloptions.h" #include "catalog/index.h" +#include "catalog/pg_type.h" #include "commands/vacuum.h" #include "nodes/pathnodes.h" +#include "utils/rel.h" PG_MODULE_MAGIC; @@ -244,6 +246,39 @@ dioptions(Datum reloptions, bool validate) di_relopt_tab, lengthof(di_relopt_tab)); } +/* + * amcanreturn: with option_ternary_1 set to true, claim that every column + * can be returned, so that amcomparedatums is not to be asked about it. + * (The index cannot actually be scanned.) + */ +static bool +dicanreturn(Relation index, int attno) +{ + DummyIndexOptions *opts = (DummyIndexOptions *) index->rd_options; + + return opts != NULL && opts->option_ternary_1 == PG_TERNARY_TRUE; +} + +/* + * Would newValue give the same entries as oldValue? The dummy index stores + * nothing, so any answer is correct. For testing the executor's use of + * amcomparedatums: with option_real set to 1, int4 values that are equal + * modulo option_int count as giving the same entries. + */ +static bool +dicomparedatums(Relation index, IndexInfo *indexInfo, int attnum, + Oid typid, int16 typlen, bool typbyval, + Datum oldValue, Datum newValue) +{ + DummyIndexOptions *opts = (DummyIndexOptions *) index->rd_options; + + if (opts == NULL || opts->option_real != 1 || opts->option_int < 2 || + typid != INT4OID) + return false; + return DatumGetInt32(oldValue) % opts->option_int == + DatumGetInt32(newValue) % opts->option_int; +} + /* * Validator for index AM. */ @@ -324,7 +359,7 @@ dihandler(PG_FUNCTION_ARGS) .aminsert = diinsert, .ambulkdelete = dibulkdelete, .amvacuumcleanup = divacuumcleanup, - .amcanreturn = NULL, + .amcanreturn = dicanreturn, .amcostestimate = dicostestimate, .amgettreeheight = NULL, .amoptions = dioptions, @@ -341,6 +376,7 @@ dihandler(PG_FUNCTION_ARGS) .amestimateparallelscan = NULL, .aminitparallelscan = NULL, .amparallelrescan = NULL, + .amcomparedatums = dicomparedatums, }; PG_RETURN_POINTER(&amroutine); diff --git a/src/test/modules/dummy_index_am/expected/comparedatums.out b/src/test/modules/dummy_index_am/expected/comparedatums.out new file mode 100644 index 00000000000..754ba1fe1d0 --- /dev/null +++ b/src/test/modules/dummy_index_am/expected/comparedatums.out @@ -0,0 +1,152 @@ +-- Test the executor's use of amcomparedatums on UPDATE: a column that an +-- index stores directly does not block HOT if the index's access method +-- says the new value gives the same entries. +CREATE EXTENSION dummy_index_am; +CREATE TABLE dummy_cmp (id int, a int, b int) WITH (fillfactor = 50); +INSERT INTO dummy_cmp SELECT g, g, g FROM generate_series(1, 10) g; +-- With option_real = 1, dummy_index_am says int4 values equal modulo +-- option_int give the same entries. A btree index on another column, and +-- no index on b. +CREATE INDEX dummy_cmp_a ON dummy_cmp USING dummy_index_am (a) + WITH (option_real = 1, option_int = 10); +CREATE INDEX dummy_cmp_id ON dummy_cmp (id); +CREATE FUNCTION dummy_cmp_hot(OUT updated bigint, OUT hot bigint) +LANGUAGE sql AS $$ + SELECT pg_stat_get_xact_tuples_updated('dummy_cmp'::regclass), + pg_stat_get_xact_tuples_hot_updated('dummy_cmp'::regclass) $$; +-- count from zero: flush what earlier transactions counted +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +BEGIN; +-- a + 10 is equal modulo 10: HOT +UPDATE dummy_cmp SET a = a + 10; +SELECT * FROM dummy_cmp_hot(); + updated | hot +---------+----- + 10 | 10 +(1 row) + +-- a + 1 is not: not HOT +UPDATE dummy_cmp SET a = a + 1; +SELECT * FROM dummy_cmp_hot(); + updated | hot +---------+----- + 20 | 10 +(1 row) + +-- b is not indexed: HOT +UPDATE dummy_cmp SET b = b + 1; +SELECT * FROM dummy_cmp_hot(); + updated | hot +---------+----- + 30 | 20 +(1 row) + +-- id + 100 needs a new entry in the btree index on id: not HOT, even though +-- a changes in a way dummy_index_am accepts +UPDATE dummy_cmp SET a = a + 10, id = id + 100; +SELECT * FROM dummy_cmp_hot(); + updated | hot +---------+----- + 40 | 20 +(1 row) + +-- to and from null always needs new entries +UPDATE dummy_cmp SET a = NULL WHERE id = 101; +UPDATE dummy_cmp SET a = 2 WHERE id = 101; +SELECT * FROM dummy_cmp_hot(); + updated | hot +---------+----- + 42 | 20 +(1 row) + +COMMIT; +-- A change to a column that another index also stores needs a new entry +-- if that index needs one: a + 10 does in a btree index on a, so not HOT. +CREATE INDEX dummy_cmp_a2 ON dummy_cmp (a); +-- count from zero: flush what earlier transactions counted +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +BEGIN; +UPDATE dummy_cmp SET a = a + 10; +SELECT * FROM dummy_cmp_hot(); + updated | hot +---------+----- + 10 | 0 +(1 row) + +COMMIT; +DROP INDEX dummy_cmp_a2; +-- Nor is the callback asked about a column the index says it can return in +-- index-only scans (option_ternary_1 makes dummy_index_am say so): a + 10 +-- is not HOT then. +ALTER INDEX dummy_cmp_a SET (option_ternary_1 = true); +-- count from zero: flush what earlier transactions counted +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +BEGIN; +UPDATE dummy_cmp SET a = a + 10; +SELECT * FROM dummy_cmp_hot(); + updated | hot +---------+----- + 10 | 0 +(1 row) + +COMMIT; +ALTER INDEX dummy_cmp_a RESET (option_ternary_1); +-- The columns of a unique index that a foreign key could reference are +-- never compared this way. (Nor are those of the replica identity, but an +-- index that can be the replica identity is always such a key.) Without +-- the unique index, id + 100 would be HOT: dummy_index_am says it gives +-- the same entries, and nothing else indexes id. +DROP INDEX dummy_cmp_id; +CREATE INDEX dummy_cmp_id_d ON dummy_cmp USING dummy_index_am (id) + WITH (option_real = 1, option_int = 100); +-- count from zero: flush what earlier transactions counted +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +BEGIN; +UPDATE dummy_cmp SET id = id + 100; +SELECT * FROM dummy_cmp_hot(); + updated | hot +---------+----- + 10 | 10 +(1 row) + +COMMIT; +CREATE UNIQUE INDEX dummy_cmp_id_u ON dummy_cmp (id); +-- count from zero: flush what earlier transactions counted +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +BEGIN; +UPDATE dummy_cmp SET id = id + 100; +SELECT * FROM dummy_cmp_hot(); + updated | hot +---------+----- + 10 | 0 +(1 row) + +COMMIT; +DROP TABLE dummy_cmp; +DROP FUNCTION dummy_cmp_hot; +DROP EXTENSION dummy_index_am; diff --git a/src/test/modules/dummy_index_am/meson.build b/src/test/modules/dummy_index_am/meson.build index 9cedfd2cf8b..a4174c2632c 100644 --- a/src/test/modules/dummy_index_am/meson.build +++ b/src/test/modules/dummy_index_am/meson.build @@ -27,6 +27,7 @@ tests += { 'bd': meson.current_build_dir(), 'regress': { 'sql': [ + 'comparedatums', 'reloptions', ], }, diff --git a/src/test/modules/dummy_index_am/sql/comparedatums.sql b/src/test/modules/dummy_index_am/sql/comparedatums.sql new file mode 100644 index 00000000000..cd8016a679b --- /dev/null +++ b/src/test/modules/dummy_index_am/sql/comparedatums.sql @@ -0,0 +1,82 @@ +-- Test the executor's use of amcomparedatums on UPDATE: a column that an +-- index stores directly does not block HOT if the index's access method +-- says the new value gives the same entries. +CREATE EXTENSION dummy_index_am; +CREATE TABLE dummy_cmp (id int, a int, b int) WITH (fillfactor = 50); +INSERT INTO dummy_cmp SELECT g, g, g FROM generate_series(1, 10) g; +-- With option_real = 1, dummy_index_am says int4 values equal modulo +-- option_int give the same entries. A btree index on another column, and +-- no index on b. +CREATE INDEX dummy_cmp_a ON dummy_cmp USING dummy_index_am (a) + WITH (option_real = 1, option_int = 10); +CREATE INDEX dummy_cmp_id ON dummy_cmp (id); +CREATE FUNCTION dummy_cmp_hot(OUT updated bigint, OUT hot bigint) +LANGUAGE sql AS $$ + SELECT pg_stat_get_xact_tuples_updated('dummy_cmp'::regclass), + pg_stat_get_xact_tuples_hot_updated('dummy_cmp'::regclass) $$; +-- count from zero: flush what earlier transactions counted +SELECT pg_stat_force_next_flush(); +BEGIN; +-- a + 10 is equal modulo 10: HOT +UPDATE dummy_cmp SET a = a + 10; +SELECT * FROM dummy_cmp_hot(); +-- a + 1 is not: not HOT +UPDATE dummy_cmp SET a = a + 1; +SELECT * FROM dummy_cmp_hot(); +-- b is not indexed: HOT +UPDATE dummy_cmp SET b = b + 1; +SELECT * FROM dummy_cmp_hot(); +-- id + 100 needs a new entry in the btree index on id: not HOT, even though +-- a changes in a way dummy_index_am accepts +UPDATE dummy_cmp SET a = a + 10, id = id + 100; +SELECT * FROM dummy_cmp_hot(); +-- to and from null always needs new entries +UPDATE dummy_cmp SET a = NULL WHERE id = 101; +UPDATE dummy_cmp SET a = 2 WHERE id = 101; +SELECT * FROM dummy_cmp_hot(); +COMMIT; +-- A change to a column that another index also stores needs a new entry +-- if that index needs one: a + 10 does in a btree index on a, so not HOT. +CREATE INDEX dummy_cmp_a2 ON dummy_cmp (a); +-- count from zero: flush what earlier transactions counted +SELECT pg_stat_force_next_flush(); +BEGIN; +UPDATE dummy_cmp SET a = a + 10; +SELECT * FROM dummy_cmp_hot(); +COMMIT; +DROP INDEX dummy_cmp_a2; +-- Nor is the callback asked about a column the index says it can return in +-- index-only scans (option_ternary_1 makes dummy_index_am say so): a + 10 +-- is not HOT then. +ALTER INDEX dummy_cmp_a SET (option_ternary_1 = true); +-- count from zero: flush what earlier transactions counted +SELECT pg_stat_force_next_flush(); +BEGIN; +UPDATE dummy_cmp SET a = a + 10; +SELECT * FROM dummy_cmp_hot(); +COMMIT; +ALTER INDEX dummy_cmp_a RESET (option_ternary_1); +-- The columns of a unique index that a foreign key could reference are +-- never compared this way. (Nor are those of the replica identity, but an +-- index that can be the replica identity is always such a key.) Without +-- the unique index, id + 100 would be HOT: dummy_index_am says it gives +-- the same entries, and nothing else indexes id. +DROP INDEX dummy_cmp_id; +CREATE INDEX dummy_cmp_id_d ON dummy_cmp USING dummy_index_am (id) + WITH (option_real = 1, option_int = 100); +-- count from zero: flush what earlier transactions counted +SELECT pg_stat_force_next_flush(); +BEGIN; +UPDATE dummy_cmp SET id = id + 100; +SELECT * FROM dummy_cmp_hot(); +COMMIT; +CREATE UNIQUE INDEX dummy_cmp_id_u ON dummy_cmp (id); +-- count from zero: flush what earlier transactions counted +SELECT pg_stat_force_next_flush(); +BEGIN; +UPDATE dummy_cmp SET id = id + 100; +SELECT * FROM dummy_cmp_hot(); +COMMIT; +DROP TABLE dummy_cmp; +DROP FUNCTION dummy_cmp_hot; +DROP EXTENSION dummy_index_am; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index bccd3cac465..16e12f0854f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3572,6 +3572,7 @@ ambuildempty_function ambuildphasename_function ambulkdelete_function amcanreturn_function +amcomparedatums_function amcostestimate_function amendscan_function amestimateparallelscan_function -- 2.50.1