diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index a157a244e4ef..02dcb20a845e 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -2012,6 +2012,24 @@ WITH ( MODULUS numeric_literal, REM
+
+ expression_checks (boolean)
+
+ expression_checks storage parameter
+
+
+
+
+ Enables or disables evaulation of predicate expressions on partial
+ indexes or expressions used to define indexes during updates.
+ If true, then these expressions are evaluated during
+ updates to data within the heap relation against the old and new values
+ and then compared to determine if HOT updates are
+ allowable or not. The default value is true.
+
+
+
+
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 02ddfda834a2..581ac0a5a510 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -1101,10 +1101,10 @@ data. Empty in ordinary tables.
- The update does not modify any columns referenced by the table's indexes,
- not including summarizing indexes. The only summarizing index method in
- the core PostgreSQL distribution is BRIN.
+ The update does not modify index keys, or when using a summarized
+ index. The only summarizing index method in the core
+ PostgreSQL distribution is
+ BRIN.
@@ -1142,6 +1142,27 @@ data. Empty in ordinary tables.
+
+ HOT updates can occur when the expression used to define
+ an index shows no changes to the indexed value. To determine this requires
+ that the expression be evaulated for the old and new values to be stored in
+ the index and then compared. This allows for HOT updates
+ when data indexed within JSONB columns is unchanged. To disable this
+ behavior and avoid the overhead of evaluating the expression during updates
+ set the expression_checks option to false for the table.
+
+
+
+ HOT updates can also occur when updated values are not
+ within the predicate of a partial index. However, HOT
+ updates are not possible when the updated value and the current value differ
+ with regards to the predicate. To determine this requires that the predicate
+ expression be evaluated for the old and new values to be stored in the index
+ and then compared. To disable this behavior and avoid the overhead of
+ evaluating the expression during updates set
+ the expression_checks option to false for the table.
+
+
You can increase the likelihood of sufficient page space for
HOT updates by decreasing a table's lockmode;
Bitmapset *hot_attrs;
Bitmapset *sum_attrs;
+ Bitmapset *exp_attrs;
Bitmapset *key_attrs;
Bitmapset *id_attrs;
Bitmapset *interesting_attrs;
@@ -3293,6 +3294,8 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
bool checked_lockers;
bool locker_remains;
bool id_has_external = false;
+ bool expression_checks = RelationGetExpressionChecks(relation);
+ bool expr_idx_updated = false;
TransactionId xmax_new_tuple,
xmax_old_tuple;
uint16 infomask_old_tuple,
@@ -3342,12 +3345,15 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
INDEX_ATTR_BITMAP_HOT_BLOCKING);
sum_attrs = RelationGetIndexAttrBitmap(relation,
INDEX_ATTR_BITMAP_SUMMARIZED);
+ exp_attrs = RelationGetIndexAttrBitmap(relation,
+ INDEX_ATTR_BITMAP_EXPRESSION);
key_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_KEY);
id_attrs = RelationGetIndexAttrBitmap(relation,
INDEX_ATTR_BITMAP_IDENTITY_KEY);
interesting_attrs = NULL;
interesting_attrs = bms_add_members(interesting_attrs, hot_attrs);
interesting_attrs = bms_add_members(interesting_attrs, sum_attrs);
+ interesting_attrs = bms_add_members(interesting_attrs, exp_attrs);
interesting_attrs = bms_add_members(interesting_attrs, key_attrs);
interesting_attrs = bms_add_members(interesting_attrs, id_attrs);
@@ -3356,6 +3362,29 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
buffer = ReadBuffer(relation, block);
page = BufferGetPage(buffer);
+ /*
+ * hot_attrs includes indexes with expressions and indexes with predicates
+ * that may not be impacted by this change. If the modified attributes in
+ * this update don't overlap with any attributes referenced by indexes on
+ * the relation then we can use the HOT update path. If they do overlap,
+ * then check to see if the overlap is exclusively due to attributes that
+ * are only referenced within expressions. If that is the case, the HOT
+ * update path may be possible iff the expression indexes are unchanged by
+ * this update or, with partial indexes, both the new and the old heap
+ * tuples don't satisfy the partial index predicate expression (meaning
+ * they are both outside of the scope of the index).
+ */
+ if (expression_checks &&
+ updateCxt->rri &&
+ updateCxt->rri->ri_projectNew &&
+ bms_is_subset(updateCxt->rri->ri_projectNew->pi_modifiedCols, exp_attrs))
+ expr_idx_updated = ExecExprIndexesRequireUpdates(relation,
+ updateCxt->rri,
+ updateCxt->rri->ri_projectNew->pi_modifiedCols,
+ updateCxt->estate,
+ updateCxt->rri->ri_oldTupleSlot,
+ updateCxt->rri->ri_newTupleSlot);
+
/*
* Before locking the buffer, pin the visibility map page if it appears to
* be necessary. Since we haven't got the lock yet, someone else might be
@@ -3403,10 +3432,11 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
tmfd->ctid = *otid;
tmfd->xmax = InvalidTransactionId;
tmfd->cmax = InvalidCommandId;
- *update_indexes = TU_None;
+ updateCxt->updateIndexes = TU_None;
bms_free(hot_attrs);
bms_free(sum_attrs);
+ bms_free(exp_attrs);
bms_free(key_attrs);
bms_free(id_attrs);
/* modified_attrs not yet initialized */
@@ -3704,10 +3734,11 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
UnlockTupleTuplock(relation, &(oldtup.t_self), *lockmode);
if (vmbuffer != InvalidBuffer)
ReleaseBuffer(vmbuffer);
- *update_indexes = TU_None;
+ updateCxt->updateIndexes = TU_None;
bms_free(hot_attrs);
bms_free(sum_attrs);
+ bms_free(exp_attrs);
bms_free(key_attrs);
bms_free(id_attrs);
bms_free(modified_attrs);
@@ -4025,22 +4056,22 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
if (newbuf == buffer)
{
/*
- * Since the new tuple is going into the same page, we might be able
- * to do a HOT update. Check if any of the index columns have been
- * changed.
+ * If all modified attributes were only referenced by summarizing
+ * indexes then we remain HOT, but we need to update those indexes to
+ * ensure that they are consistent with the new tuple.
*/
- if (!bms_overlap(modified_attrs, hot_attrs))
+ if (!bms_overlap(modified_attrs, hot_attrs) ||
+ (expression_checks &&
+ (bms_is_subset(modified_attrs, exp_attrs) && !expr_idx_updated)))
{
use_hot_update = true;
/*
- * If none of the columns that are used in hot-blocking indexes
- * were updated, we can apply HOT, but we do still need to check
- * if we need to update the summarizing indexes, and update those
- * indexes if the columns were updated, or we may fail to detect
- * e.g. value bound changes in BRIN minmax indexes.
+ * If all modified attributes were only referenced by summarizing
+ * indexes then we remain HOT, but we need to update those indexes
+ * to ensure that they are consistent with the new tuple.
*/
- if (bms_overlap(modified_attrs, sum_attrs))
+ if (bms_is_subset(modified_attrs, sum_attrs))
summarized_update = true;
}
}
@@ -4211,18 +4242,19 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
if (use_hot_update)
{
if (summarized_update)
- *update_indexes = TU_Summarizing;
+ updateCxt->updateIndexes = TU_Summarizing;
else
- *update_indexes = TU_None;
+ updateCxt->updateIndexes = TU_None;
}
else
- *update_indexes = TU_All;
+ updateCxt->updateIndexes = TU_All;
if (old_key_tuple != NULL && old_key_copied)
heap_freetuple(old_key_tuple);
bms_free(hot_attrs);
bms_free(sum_attrs);
+ bms_free(exp_attrs);
bms_free(key_attrs);
bms_free(id_attrs);
bms_free(modified_attrs);
@@ -4500,16 +4532,15 @@ HeapDetermineColumnsInfo(Relation relation,
*/
void
simple_heap_update(Relation relation, ItemPointer otid, HeapTuple tup,
- TU_UpdateIndexes *update_indexes)
+ UpdateContext *updateCxt)
{
TM_Result result;
TM_FailureData tmfd;
- LockTupleMode lockmode;
result = heap_update(relation, otid, tup,
GetCurrentCommandId(true), InvalidSnapshot,
true /* wait for commit */ ,
- &tmfd, &lockmode, update_indexes);
+ &tmfd, updateCxt);
switch (result)
{
case TM_SelfModified:
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index bcbac844bb66..a7e65f7e2f60 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -316,8 +316,7 @@ heapam_tuple_delete(Relation relation, ItemPointer tid, CommandId cid,
static TM_Result
heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
CommandId cid, Snapshot snapshot, Snapshot crosscheck,
- bool wait, TM_FailureData *tmfd,
- LockTupleMode *lockmode, TU_UpdateIndexes *update_indexes)
+ bool wait, TM_FailureData *tmfd, UpdateContext *updateCxt)
{
bool shouldFree = true;
HeapTuple tuple = ExecFetchSlotHeapTuple(slot, true, &shouldFree);
@@ -328,7 +327,7 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
tuple->t_tableOid = slot->tts_tableOid;
result = heap_update(relation, otid, tuple, cid, crosscheck, wait,
- tmfd, lockmode, update_indexes);
+ tmfd, updateCxt);
ItemPointerCopy(&tuple->t_self, &slot->tts_tid);
/*
@@ -343,14 +342,14 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
*/
if (result != TM_Ok)
{
- Assert(*update_indexes == TU_None);
- *update_indexes = TU_None;
+ Assert(updateCxt->updateIndexes == TU_None);
+ updateCxt->updateIndexes = TU_None;
}
else if (!HeapTupleIsHeapOnly(tuple))
- Assert(*update_indexes == TU_All);
+ Assert(updateCxt->updateIndexes == TU_All);
else
- Assert((*update_indexes == TU_Summarizing) ||
- (*update_indexes == TU_None));
+ Assert((updateCxt->updateIndexes == TU_Summarizing) ||
+ (updateCxt->updateIndexes == TU_None));
if (shouldFree)
pfree(tuple);
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index 5e41404937eb..c1d41a1fb874 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -336,17 +336,16 @@ void
simple_table_tuple_update(Relation rel, ItemPointer otid,
TupleTableSlot *slot,
Snapshot snapshot,
- TU_UpdateIndexes *update_indexes)
+ UpdateContext *updateCxt)
{
TM_Result result;
TM_FailureData tmfd;
- LockTupleMode lockmode;
result = table_tuple_update(rel, otid, slot,
GetCurrentCommandId(true),
snapshot, InvalidSnapshot,
true /* wait for commit */ ,
- &tmfd, &lockmode, update_indexes);
+ &tmfd, updateCxt);
switch (result)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 5d9db167e595..205147fe341e 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2467,6 +2467,8 @@ BuildIndexInfo(Relation index)
&ii->ii_ExclusionStrats);
}
+ ii->ii_IndexAttrByVal = NULL;
+
return ii;
}
@@ -2707,6 +2709,52 @@ BuildSpeculativeIndexInfo(Relation index, IndexInfo *ii)
}
}
+/* ----------------
+ * BuildExpressionIndexInfo
+ * Add extra state to IndexInfo record
+ *
+ * For expression indexes updates may not change the indexed value allowing
+ * for a HOT update. Add information to the IndexInfo to allow for checking
+ * if the indexed value has changed.
+ *
+ * Do this processing here rather than in BuildIndexInfo() to not incur the
+ * overhead in the common non-expression cases.
+ * ----------------
+ */
+void
+BuildExpressionIndexInfo(Relation index, IndexInfo *ii)
+{
+ int i;
+ int indnkeyatts;
+
+ /*
+ * Expressions are not allowed on non-key attributes, so we can skip them
+ * as they should show up in the index HOT-blocking attributes.
+ */
+ indnkeyatts = IndexRelationGetNumberOfKeyAttributes(index);
+
+ /*
+ * Collect attributes used by the index, their len and if they are by
+ * value.
+ */
+ for (i = 0; i < indnkeyatts; i++)
+ {
+ CompactAttribute *attr = TupleDescCompactAttr(RelationGetDescr(index), i);
+
+ ii->ii_IndexAttrLen[i] = attr->attlen;
+ if (attr->attbyval)
+ ii->ii_IndexAttrByVal = bms_add_member(ii->ii_IndexAttrByVal, i);
+ }
+
+ /* collect attributes used in the expression */
+ if (ii->ii_Expressions)
+ pull_varattnos((Node *) ii->ii_Expressions, 1, &ii->ii_ExpressionsAttrs);
+
+ /* collect attributes used in the predicate */
+ if (ii->ii_Predicate)
+ pull_varattnos((Node *) ii->ii_Predicate, 1, &ii->ii_PredicateAttrs);
+}
+
/* ----------------
* FormIndexDatum
* Construct values[] and isnull[] arrays for a new index tuple.
diff --git a/src/backend/catalog/indexing.c b/src/backend/catalog/indexing.c
index 25c4b6bdc87f..c413c099da84 100644
--- a/src/backend/catalog/indexing.c
+++ b/src/backend/catalog/indexing.c
@@ -49,7 +49,7 @@ CatalogOpenIndexes(Relation heapRel)
resultRelInfo->ri_RelationDesc = heapRel;
resultRelInfo->ri_TrigDesc = NULL; /* we don't fire triggers */
- ExecOpenIndices(resultRelInfo, false);
+ ExecOpenIndices(resultRelInfo, false, false);
return resultRelInfo;
}
@@ -313,15 +313,17 @@ void
CatalogTupleUpdate(Relation heapRel, ItemPointer otid, HeapTuple tup)
{
CatalogIndexState indstate;
- TU_UpdateIndexes updateIndexes = TU_All;
+ UpdateContext updateCxt = {0};
+
+ updateCxt.updateIndexes = TU_All;
CatalogTupleCheckConstraints(heapRel, tup);
indstate = CatalogOpenIndexes(heapRel);
- simple_heap_update(heapRel, otid, tup, &updateIndexes);
+ simple_heap_update(heapRel, otid, tup, &updateCxt);
- CatalogIndexInsert(indstate, tup, updateIndexes);
+ CatalogIndexInsert(indstate, tup, updateCxt.updateIndexes);
CatalogCloseIndexes(indstate);
}
@@ -337,13 +339,15 @@ void
CatalogTupleUpdateWithInfo(Relation heapRel, ItemPointer otid, HeapTuple tup,
CatalogIndexState indstate)
{
- TU_UpdateIndexes updateIndexes = TU_All;
+ UpdateContext updateCxt = {0};
+
+ updateCxt.updateIndexes = TU_All;
CatalogTupleCheckConstraints(heapRel, tup);
- simple_heap_update(heapRel, otid, tup, &updateIndexes);
+ simple_heap_update(heapRel, otid, tup, &updateCxt);
- CatalogIndexInsert(indstate, tup, updateIndexes);
+ CatalogIndexInsert(indstate, tup, updateCxt.updateIndexes);
}
/*
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 12781963b4f9..aea42c495249 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -921,7 +921,7 @@ CopyFrom(CopyFromState cstate)
/* Verify the named relation is a valid target for INSERT */
CheckValidResultRel(resultRelInfo, CMD_INSERT, ONCONFLICT_NONE, NIL);
- ExecOpenIndices(resultRelInfo, false);
+ ExecOpenIndices(resultRelInfo, false, false);
/*
* Set up a ModifyTableState so we can let FDW(s) init themselves for
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index f1569879b529..8bb8692b46ea 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -610,8 +610,9 @@ ExecBuildUpdateProjection(List *targetList,
{
AttrNumber targetattnum = lfirst_int(lc);
- assignedCols = bms_add_member(assignedCols, targetattnum);
+ assignedCols = bms_add_member(assignedCols, targetattnum - FirstLowInvalidHeapAttributeNumber);
}
+ projInfo->pi_modifiedCols = assignedCols;
/*
* We need to insert EEOP_*_FETCHSOME steps to ensure the input tuples are
@@ -624,7 +625,7 @@ ExecBuildUpdateProjection(List *targetList,
if (attr->attisdropped)
continue;
- if (bms_is_member(attnum, assignedCols))
+ if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, assignedCols))
continue;
deform.last_scan = attnum;
break;
@@ -732,7 +733,7 @@ ExecBuildUpdateProjection(List *targetList,
scratch.d.assign_tmp.resultnum = attnum - 1;
ExprEvalPushStep(state, &scratch);
}
- else if (!bms_is_member(attnum, assignedCols))
+ else if (!bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, assignedCols))
{
/* Certainly the right type, so needn't check */
scratch.opcode = EEOP_ASSIGN_SCAN_VAR;
diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index ca33a854278e..940311dc0d6b 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -113,10 +113,13 @@
#include "catalog/index.h"
#include "executor/executor.h"
#include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
#include "storage/lmgr.h"
#include "utils/multirangetypes.h"
#include "utils/rangetypes.h"
#include "utils/snapmgr.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
/* waitMode argument to check_exclusion_or_unique_constraint() */
typedef enum
@@ -157,7 +160,7 @@ static void ExecWithoutOverlapsNotEmpty(Relation rel, NameData attname, Datum at
* ----------------------------------------------------------------
*/
void
-ExecOpenIndices(ResultRelInfo *resultRelInfo, bool speculative)
+ExecOpenIndices(ResultRelInfo *resultRelInfo, bool speculative, bool update)
{
Relation resultRelation = resultRelInfo->ri_RelationDesc;
List *indexoidlist;
@@ -220,6 +223,13 @@ ExecOpenIndices(ResultRelInfo *resultRelInfo, bool speculative)
if (speculative && ii->ii_Unique && !indexDesc->rd_index->indisexclusion)
BuildSpeculativeIndexInfo(indexDesc, ii);
+ /*
+ * If the index uses expressions then let's populate the additional
+ * information nessaary to evaluate them for changes during updates.
+ */
+ if (update && (ii->ii_Expressions || ii->ii_Predicate))
+ BuildExpressionIndexInfo(indexDesc, ii);
+
relationDescs[i] = indexDesc;
indexInfoArray[i] = ii;
i++;
@@ -382,19 +392,33 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo,
ExprState *predicate;
/*
- * If predicate state not set up yet, create it (in the estate's
- * per-query context)
+ * It is possible that we've already checked the predicate, if so
+ * then avoid the duplicate work.
*/
- predicate = indexInfo->ii_PredicateState;
- if (predicate == NULL)
+ if (indexInfo->ii_CheckedPredicate)
{
- predicate = ExecPrepareQual(indexInfo->ii_Predicate, estate);
- indexInfo->ii_PredicateState = predicate;
+ /* Skip this index-update if the predicate isn't satisfied */
+ if (!indexInfo->ii_PredicateSatisfied)
+ continue;
}
+ else
+ {
- /* Skip this index-update if the predicate isn't satisfied */
- if (!ExecQual(predicate, econtext))
- continue;
+ /*
+ * If predicate state not set up yet, create it (in the
+ * estate's per-query context)
+ */
+ predicate = indexInfo->ii_PredicateState;
+ if (predicate == NULL)
+ {
+ predicate = ExecPrepareQual(indexInfo->ii_Predicate, estate);
+ indexInfo->ii_PredicateState = predicate;
+ }
+
+ /* Skip this index-update if the predicate isn't satisfied */
+ if (!ExecQual(predicate, econtext))
+ continue;
+ }
}
/*
@@ -1095,6 +1119,9 @@ index_unchanged_by_update(ResultRelInfo *resultRelInfo, EState *estate,
if (hasexpression)
{
+ if (indexInfo->ii_IndexUnchanged)
+ return true;
+
indexInfo->ii_IndexUnchanged = false;
return false;
}
@@ -1172,3 +1199,168 @@ ExecWithoutOverlapsNotEmpty(Relation rel, NameData attname, Datum attval, char t
errmsg("empty WITHOUT OVERLAPS value found in column \"%s\" in relation \"%s\"",
NameStr(attname), RelationGetRelationName(rel))));
}
+
+/*
+ *
+ * This will first determine if the index has a predicate and if so if the
+ * update satisfies that or not. Then, if necessary, we compare old and
+ * new values of the indexed expression and help determine if it is possible
+ * to use a HOT update or not.
+ *
+ * 'resultRelInfo' is the table with the indexes we should examine.
+ * 'modified' is the set of attributes that are modified by the update.
+ * 'estate' is the executor state for the update.
+ * 'old_tts' is a slot with the old tuple.
+ * 'new_tts' is a slot with the new tuple.
+ *
+ * Returns true iff none of the indexes on this relation require updating.
+ *
+ * When the changes in new tuple impact a value stored in an index we must
+ * return true. When an index has a predicate that is not satisfied by either
+ * the new or old tuples then that index is unchanged. When an index has a
+ * predicate that is satisfied by both the old and new tuples then we can
+ * proceed and check to see if the indexed values were changed or not.
+ */
+bool
+ExecExprIndexesRequireUpdates(Relation relation,
+ ResultRelInfo *resultRelInfo,
+ Bitmapset *modifiedAttrs,
+ EState *estate,
+ TupleTableSlot *old_tts,
+ TupleTableSlot *new_tts)
+{
+ bool expression_checks = RelationGetExpressionChecks(relation);
+ bool result = false;
+ IndexInfo *indexInfo;
+ TupleTableSlot *save_scantuple;
+ ExprContext *econtext = NULL;
+
+ if (resultRelInfo == NULL || estate == NULL ||
+ old_tts == NULL || new_tts == NULL ||
+ !expression_checks || IsolationIsSerializable())
+ return true;
+
+ econtext = GetPerTupleExprContext(estate);
+
+ /*
+ * Examine each index on this relation to see if it is affected by the
+ * changes in newtup. If any index is changed, we must not use a HOT
+ * update.
+ */
+ for (int i = 0; i < resultRelInfo->ri_NumIndices; i++)
+ {
+ indexInfo = resultRelInfo->ri_IndexRelationInfo[i];
+
+ /*
+ * If this is a partial index it has a predicate, evaluate the
+ * expression to determine if we need to include it or not.
+ */
+ if (bms_overlap(indexInfo->ii_PredicateAttrs, modifiedAttrs))
+ {
+ ExprState *pstate;
+ bool old_tuple_qualifies,
+ new_tuple_qualifies;
+
+ pstate = ExecPrepareQual(indexInfo->ii_Predicate, estate);
+
+ /*
+ * Here the term "qualifies" means "satisfies the predicate
+ * condition of the partial index".
+ */
+ save_scantuple = econtext->ecxt_scantuple;
+ econtext->ecxt_scantuple = old_tts;
+ old_tuple_qualifies = ExecQual(pstate, econtext);
+
+ econtext->ecxt_scantuple = new_tts;
+ new_tuple_qualifies = ExecQual(pstate, econtext);
+ econtext->ecxt_scantuple = save_scantuple;
+
+ indexInfo->ii_CheckedPredicate = true;
+ indexInfo->ii_PredicateSatisfied = new_tuple_qualifies;
+
+ /*
+ * If neither the old nor the new tuples satisfy the predicate we
+ * can be sure that this index doesn't need updating, continue to
+ * the next index.
+ */
+ if ((new_tuple_qualifies == false) && (old_tuple_qualifies == false))
+ continue;
+
+ /*
+ * If there is a transition between indexed and not indexed,
+ * that's enough to require an index update.
+ */
+ if (new_tuple_qualifies != old_tuple_qualifies)
+ {
+ result = true;
+ break;
+ }
+
+ /*
+ * Otherwise the old and new values exist in the index, but did
+ * they get updated? We don't yet know, so proceed with the next
+ * statement in the loop to find out.
+ */
+ }
+
+ /*
+ * Indexes with expressions may or may not have changed, it is
+ * impossible to know without exercising their expression and
+ * reviewing index tuple state for changes. This is a lot of work,
+ * but because all indexes on JSONB columns fall into this category it
+ * can be worth it to avoid index updates and remain on the HOT update
+ * path when possible.
+ */
+ if (bms_overlap(indexInfo->ii_ExpressionsAttrs, modifiedAttrs))
+ {
+ Datum old_values[INDEX_MAX_KEYS];
+ bool old_isnull[INDEX_MAX_KEYS];
+ Datum new_values[INDEX_MAX_KEYS];
+ bool new_isnull[INDEX_MAX_KEYS];
+
+ save_scantuple = econtext->ecxt_scantuple;
+ econtext->ecxt_scantuple = old_tts;
+ FormIndexDatum(indexInfo,
+ old_tts,
+ estate,
+ old_values,
+ old_isnull);
+ econtext->ecxt_scantuple = new_tts;
+ FormIndexDatum(indexInfo,
+ new_tts,
+ estate,
+ new_values,
+ new_isnull);
+ econtext->ecxt_scantuple = save_scantuple;
+
+ for (int j = 0; j < indexInfo->ii_NumIndexKeyAttrs; j++)
+ {
+ if (old_isnull[j] != new_isnull[j])
+ {
+ result = true;
+ break;
+ }
+ else if (!old_isnull[j])
+ {
+ int16 elmlen = indexInfo->ii_IndexAttrLen[j];
+ bool elmbyval = bms_is_member(j, indexInfo->ii_IndexAttrByVal);
+
+ if (!datum_image_eq(old_values[j], new_values[j],
+ elmbyval, elmlen))
+ {
+ result = true;
+ break;
+ }
+ }
+ }
+
+ indexInfo->ii_CheckedUnchanged = true;
+ indexInfo->ii_IndexUnchanged = !result;
+
+ if (result)
+ break;
+ }
+ }
+
+ return result;
+}
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f2da072632e..2f48919a6206 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -544,7 +544,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
leaf_part_rri->ri_IndexRelationDescs == NULL)
ExecOpenIndices(leaf_part_rri,
(node != NULL &&
- node->onConflictAction != ONCONFLICT_NONE));
+ node->onConflictAction != ONCONFLICT_NONE), false);
/*
* Build WITH CHECK OPTION constraints for the partition. Note that we
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index def32774c90d..be26a2c76761 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -920,7 +920,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
if (!skip_tuple)
{
List *recheckIndexes = NIL;
- TU_UpdateIndexes update_indexes;
+ UpdateContext updateCxt = {0};
List *conflictindexes;
bool conflict = false;
@@ -936,17 +936,19 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
if (rel->rd_rel->relispartition)
ExecPartitionCheck(resultRelInfo, slot, estate, true);
+ updateCxt.estate = estate;
+ updateCxt.rri = resultRelInfo;
simple_table_tuple_update(rel, tid, slot, estate->es_snapshot,
- &update_indexes);
+ &updateCxt);
conflictindexes = resultRelInfo->ri_onConflictArbiterIndexes;
- if (resultRelInfo->ri_NumIndices > 0 && (update_indexes != TU_None))
+ if (resultRelInfo->ri_NumIndices > 0 && (updateCxt.updateIndexes != TU_None))
recheckIndexes = ExecInsertIndexTuples(resultRelInfo,
slot, estate, true,
conflictindexes ? true : false,
&conflict, conflictindexes,
- (update_indexes == TU_Summarizing));
+ (updateCxt.updateIndexes == TU_Summarizing));
/*
* Refer to the comments above the call to CheckAndReportConflict() in
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 4c5647ac38a1..db3d8789846e 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -116,21 +116,6 @@ typedef struct ModifyTableContext
TupleTableSlot *cpUpdateReturningSlot;
} ModifyTableContext;
-/*
- * Context struct containing output data specific to UPDATE operations.
- */
-typedef struct UpdateContext
-{
- bool crossPartUpdate; /* was it a cross-partition update? */
- TU_UpdateIndexes updateIndexes; /* Which index updates are required? */
-
- /*
- * Lock mode to acquire on the latest tuple version before performing
- * EvalPlanQual on it
- */
- LockTupleMode lockmode;
-} UpdateContext;
-
static void ExecBatchInsert(ModifyTableState *mtstate,
ResultRelInfo *resultRelInfo,
@@ -890,7 +875,7 @@ ExecInsert(ModifyTableContext *context,
*/
if (resultRelationDesc->rd_rel->relhasindex &&
resultRelInfo->ri_IndexRelationDescs == NULL)
- ExecOpenIndices(resultRelInfo, onconflict != ONCONFLICT_NONE);
+ ExecOpenIndices(resultRelInfo, onconflict != ONCONFLICT_NONE, false);
/*
* BEFORE ROW INSERT Triggers.
@@ -2106,7 +2091,7 @@ ExecUpdatePrologue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
*/
if (resultRelationDesc->rd_rel->relhasindex &&
resultRelInfo->ri_IndexRelationDescs == NULL)
- ExecOpenIndices(resultRelInfo, false);
+ ExecOpenIndices(resultRelInfo, false, true);
/* BEFORE ROW UPDATE triggers */
if (resultRelInfo->ri_TrigDesc &&
@@ -2305,8 +2290,7 @@ ExecUpdateAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
estate->es_snapshot,
estate->es_crosscheck_snapshot,
true /* wait for commit */ ,
- &context->tmfd, &updateCxt->lockmode,
- &updateCxt->updateIndexes);
+ &context->tmfd, updateCxt);
return result;
}
@@ -2324,6 +2308,7 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
{
ModifyTableState *mtstate = context->mtstate;
List *recheckIndexes = NIL;
+ bool onlySummarizing = updateCxt->updateIndexes == TU_Summarizing;
/* insert index entries for tuple if necessary */
if (resultRelInfo->ri_NumIndices > 0 && (updateCxt->updateIndexes != TU_None))
@@ -2331,7 +2316,7 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
slot, context->estate,
true, false,
NULL, NIL,
- (updateCxt->updateIndexes == TU_Summarizing));
+ onlySummarizing);
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(context->estate, resultRelInfo,
@@ -2467,6 +2452,9 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
UpdateContext updateCxt = {0};
TM_Result result;
+ updateCxt.estate = estate;
+ updateCxt.rri = resultRelInfo;
+
/*
* abort the operation if not running transactions
*/
@@ -3155,6 +3143,9 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
TM_Result result;
UpdateContext updateCxt = {0};
+ updateCxt.rri = resultRelInfo;
+ updateCxt.estate = estate;
+
/*
* Test condition, if any.
*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 7edd1c9cf060..71be29da71b6 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -2674,7 +2674,7 @@ apply_handle_insert(StringInfo s)
{
ResultRelInfo *relinfo = edata->targetRelInfo;
- ExecOpenIndices(relinfo, false);
+ ExecOpenIndices(relinfo, false, false);
apply_handle_insert_internal(edata, relinfo, remoteslot);
ExecCloseIndices(relinfo);
}
@@ -2897,7 +2897,7 @@ apply_handle_update_internal(ApplyExecutionData *edata,
MemoryContext oldctx;
EvalPlanQualInit(&epqstate, estate, NULL, NIL, -1, NIL);
- ExecOpenIndices(relinfo, false);
+ ExecOpenIndices(relinfo, false, true);
found = FindReplTupleInLocalRel(edata, localrel,
&relmapentry->remoterel,
@@ -3055,7 +3055,7 @@ apply_handle_delete(StringInfo s)
{
ResultRelInfo *relinfo = edata->targetRelInfo;
- ExecOpenIndices(relinfo, false);
+ ExecOpenIndices(relinfo, false, false);
apply_handle_delete_internal(edata, relinfo,
remoteslot, rel->localindexoid);
ExecCloseIndices(relinfo);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 915d0bc90842..a442ff215a72 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -64,6 +64,7 @@
#include "catalog/pg_type.h"
#include "catalog/schemapg.h"
#include "catalog/storage.h"
+#include "catalog/index.h"
#include "commands/policy.h"
#include "commands/publicationcmds.h"
#include "commands/trigger.h"
@@ -2482,6 +2483,7 @@ RelationDestroyRelation(Relation relation, bool remember_tupdesc)
bms_free(relation->rd_idattr);
bms_free(relation->rd_hotblockingattr);
bms_free(relation->rd_summarizedattr);
+ bms_free(relation->rd_expressionattr);
if (relation->rd_pubdesc)
pfree(relation->rd_pubdesc);
if (relation->rd_options)
@@ -5283,6 +5285,7 @@ RelationGetIndexPredicate(Relation relation)
* index (empty if FULL)
* INDEX_ATTR_BITMAP_HOT_BLOCKING Columns that block updates from being HOT
* INDEX_ATTR_BITMAP_SUMMARIZED Columns included in summarizing indexes
+ * INDEX_ATTR_BITMAP_EXPRESSION Columns included in expresion indexes
*
* Attribute numbers are offset by FirstLowInvalidHeapAttributeNumber so that
* we can include system attributes (e.g., OID) in the bitmap representation.
@@ -5305,8 +5308,9 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
Bitmapset *uindexattrs; /* columns in unique indexes */
Bitmapset *pkindexattrs; /* columns in the primary index */
Bitmapset *idindexattrs; /* columns in the replica identity */
- Bitmapset *hotblockingattrs; /* columns with HOT blocking indexes */
- Bitmapset *summarizedattrs; /* columns with summarizing indexes */
+ Bitmapset *idx_attrs; /* columns referenced by indexes */
+ Bitmapset *expr_attrs; /* columns referenced by index expressions */
+ Bitmapset *sum_attrs; /* columns with summarizing indexes */
List *indexoidlist;
List *newindexoidlist;
Oid relpkindex;
@@ -5329,6 +5333,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
return bms_copy(relation->rd_hotblockingattr);
case INDEX_ATTR_BITMAP_SUMMARIZED:
return bms_copy(relation->rd_summarizedattr);
+ case INDEX_ATTR_BITMAP_EXPRESSION:
+ return bms_copy(relation->rd_expressionattr);
default:
elog(ERROR, "unknown attrKind %u", attrKind);
}
@@ -5371,8 +5377,9 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
uindexattrs = NULL;
pkindexattrs = NULL;
idindexattrs = NULL;
- hotblockingattrs = NULL;
- summarizedattrs = NULL;
+ idx_attrs = NULL;
+ expr_attrs = NULL;
+ sum_attrs = NULL;
foreach(l, indexoidlist)
{
Oid indexOid = lfirst_oid(l);
@@ -5386,6 +5393,7 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
bool isPK; /* primary key */
bool isIDKey; /* replica identity index */
Bitmapset **attrs;
+ Bitmapset **exprattrs;
indexDesc = index_open(indexOid, AccessShareLock);
@@ -5429,20 +5437,26 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
* decide which bitmap we'll update in the following loop.
*/
if (indexDesc->rd_indam->amsummarizing)
- attrs = &summarizedattrs;
+ {
+ attrs = &sum_attrs;
+ exprattrs = &sum_attrs;
+ }
else
- attrs = &hotblockingattrs;
+ {
+ attrs = &idx_attrs;
+ exprattrs = &expr_attrs;
+ }
/* Collect simple attribute references */
for (i = 0; i < indexDesc->rd_index->indnatts; i++)
{
- int attrnum = indexDesc->rd_index->indkey.values[i];
+ int attridx = indexDesc->rd_index->indkey.values[i];
/*
* Since we have covering indexes with non-key columns, we must
* handle them accurately here. non-key columns must be added into
- * hotblockingattrs or summarizedattrs, since they are in index,
- * and update shouldn't miss them.
+ * idx_attrs or sum_attrs, since they are in index, and update
+ * shouldn't miss them.
*
* Summarizing indexes do not block HOT, but do need to be updated
* when the column value changes, thus require a separate
@@ -5452,30 +5466,28 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
* key or identity key. Hence we do not include them into
* uindexattrs, pkindexattrs and idindexattrs bitmaps.
*/
- if (attrnum != 0)
+ if (attridx != 0)
{
- *attrs = bms_add_member(*attrs,
- attrnum - FirstLowInvalidHeapAttributeNumber);
+ AttrNumber attrnum = attridx - FirstLowInvalidHeapAttributeNumber;
+
+ *attrs = bms_add_member(*attrs, attrnum);
if (isKey && i < indexDesc->rd_index->indnkeyatts)
- uindexattrs = bms_add_member(uindexattrs,
- attrnum - FirstLowInvalidHeapAttributeNumber);
+ uindexattrs = bms_add_member(uindexattrs, attrnum);
if (isPK && i < indexDesc->rd_index->indnkeyatts)
- pkindexattrs = bms_add_member(pkindexattrs,
- attrnum - FirstLowInvalidHeapAttributeNumber);
+ pkindexattrs = bms_add_member(pkindexattrs, attrnum);
if (isIDKey && i < indexDesc->rd_index->indnkeyatts)
- idindexattrs = bms_add_member(idindexattrs,
- attrnum - FirstLowInvalidHeapAttributeNumber);
+ idindexattrs = bms_add_member(idindexattrs, attrnum);
}
}
/* Collect all attributes used in expressions, too */
- pull_varattnos(indexExpressions, 1, attrs);
+ pull_varattnos(indexExpressions, 1, exprattrs);
/* Collect all attributes in the index predicate, too */
- pull_varattnos(indexPredicate, 1, attrs);
+ pull_varattnos(indexPredicate, 1, exprattrs);
index_close(indexDesc, AccessShareLock);
}
@@ -5503,12 +5515,24 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
bms_free(uindexattrs);
bms_free(pkindexattrs);
bms_free(idindexattrs);
- bms_free(hotblockingattrs);
- bms_free(summarizedattrs);
+ bms_free(idx_attrs);
+ bms_free(expr_attrs);
+ bms_free(sum_attrs);
goto restart;
}
+ /*
+ * HOT-blocking attributes should include all columns that are part of the
+ * index except attributes only referenced in expressions, including
+ * expressions used to form partial indexes. So, we need to remove the
+ * expression-only attributes from the HOT-blocking columns bitmap as
+ * those will be checked separately.
+ */
+ expr_attrs = bms_del_members(expr_attrs, idx_attrs);
+ idx_attrs = bms_add_members(idx_attrs, expr_attrs);
+ expr_attrs = bms_add_members(expr_attrs, sum_attrs);
+
/* Don't leak the old values of these bitmaps, if any */
relation->rd_attrsvalid = false;
bms_free(relation->rd_keyattr);
@@ -5521,6 +5545,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
relation->rd_hotblockingattr = NULL;
bms_free(relation->rd_summarizedattr);
relation->rd_summarizedattr = NULL;
+ bms_free(relation->rd_expressionattr);
+ relation->rd_expressionattr = NULL;
/*
* Now save copies of the bitmaps in the relcache entry. We intentionally
@@ -5533,8 +5559,9 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
relation->rd_keyattr = bms_copy(uindexattrs);
relation->rd_pkattr = bms_copy(pkindexattrs);
relation->rd_idattr = bms_copy(idindexattrs);
- relation->rd_hotblockingattr = bms_copy(hotblockingattrs);
- relation->rd_summarizedattr = bms_copy(summarizedattrs);
+ relation->rd_hotblockingattr = bms_copy(idx_attrs);
+ relation->rd_summarizedattr = bms_copy(sum_attrs);
+ relation->rd_expressionattr = bms_copy(expr_attrs);
relation->rd_attrsvalid = true;
MemoryContextSwitchTo(oldcxt);
@@ -5548,9 +5575,11 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
case INDEX_ATTR_BITMAP_IDENTITY_KEY:
return idindexattrs;
case INDEX_ATTR_BITMAP_HOT_BLOCKING:
- return hotblockingattrs;
+ return idx_attrs;
case INDEX_ATTR_BITMAP_SUMMARIZED:
- return summarizedattrs;
+ return sum_attrs;
+ case INDEX_ATTR_BITMAP_EXPRESSION:
+ return expr_attrs;
default:
elog(ERROR, "unknown attrKind %u", attrKind);
return NULL;
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 36ea6a4d5570..11de9cd434e5 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3046,7 +3046,7 @@ match_previous_words(int pattern_id,
COMPLETE_WITH("(");
/* ALTER TABLESPACE SET|RESET ( */
else if (Matches("ALTER", "TABLESPACE", MatchAny, "SET|RESET", "("))
- COMPLETE_WITH("seq_page_cost", "random_page_cost",
+ COMPLETE_WITH("seq_page_cost", "random_page_cost", "expression_checks",
"effective_io_concurrency", "maintenance_io_concurrency");
/* ALTER TEXT SEARCH */
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 8cbff6ab0eb1..3ca8ace66641 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -22,6 +22,7 @@
#include "access/table.h" /* for backward compatibility */
#include "access/tableam.h"
#include "commands/vacuum.h"
+#include "executor/executor.h"
#include "nodes/lockoptions.h"
#include "nodes/primnodes.h"
#include "storage/bufpage.h"
@@ -324,8 +325,7 @@ extern void heap_abort_speculative(Relation relation, ItemPointer tid);
extern TM_Result heap_update(Relation relation, ItemPointer otid,
HeapTuple newtup,
CommandId cid, Snapshot crosscheck, bool wait,
- TM_FailureData *tmfd, LockTupleMode *lockmode,
- TU_UpdateIndexes *update_indexes);
+ TM_FailureData *tmfd, UpdateContext *updateCxt);
extern TM_Result heap_lock_tuple(Relation relation, HeapTuple tuple,
CommandId cid, LockTupleMode mode, LockWaitPolicy wait_policy,
bool follow_updates,
@@ -360,7 +360,7 @@ extern bool heap_tuple_needs_eventual_freeze(HeapTupleHeader tuple);
extern void simple_heap_insert(Relation relation, HeapTuple tup);
extern void simple_heap_delete(Relation relation, ItemPointer tid);
extern void simple_heap_update(Relation relation, ItemPointer otid,
- HeapTuple tup, TU_UpdateIndexes *update_indexes);
+ HeapTuple tup, UpdateContext *updateCxt);
extern TransactionId heap_index_delete_tuples(Relation rel,
TM_IndexDeleteOp *delstate);
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index e16bf0256928..5a9685eb83ad 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -119,6 +119,31 @@ typedef enum TU_UpdateIndexes
TU_Summarizing,
} TU_UpdateIndexes;
+typedef struct ResultRelInfo ResultRelInfo;
+typedef struct EState EState;
+
+/*
+ * Data specific to processing UPDATE operations.
+ *
+ * When table_tuple_update is called some storage managers, notably heapam,
+ * can at times avoid index updates. In the heapam this is known as a HOT
+ * update. This struct is used to provide the state required to test for
+ * HOT updates and to communicate that decision on to the index AMs.
+ */
+typedef struct UpdateContext
+{
+ TU_UpdateIndexes updateIndexes; /* Which index updates are required? */
+ ResultRelInfo *rri; /* ResultRelInfo for the updated table. */
+ EState *estate; /* EState used within the update. */
+ bool crossPartUpdate; /* Was it a cross-partition update? */
+
+ /*
+ * Lock mode to acquire on the latest tuple version before performing
+ * EvalPlanQual on it
+ */
+ LockTupleMode lockmode;
+} UpdateContext;
+
/*
* When table_tuple_update, table_tuple_delete, or table_tuple_lock fail
* because the target tuple is already outdated, they fill in this struct to
@@ -548,8 +573,7 @@ typedef struct TableAmRoutine
Snapshot crosscheck,
bool wait,
TM_FailureData *tmfd,
- LockTupleMode *lockmode,
- TU_UpdateIndexes *update_indexes);
+ UpdateContext *updateCxt);
/* see table_tuple_lock() for reference about parameters */
TM_Result (*tuple_lock) (Relation rel,
@@ -1501,13 +1525,11 @@ table_tuple_delete(Relation rel, ItemPointer tid, CommandId cid,
static inline TM_Result
table_tuple_update(Relation rel, ItemPointer otid, TupleTableSlot *slot,
CommandId cid, Snapshot snapshot, Snapshot crosscheck,
- bool wait, TM_FailureData *tmfd, LockTupleMode *lockmode,
- TU_UpdateIndexes *update_indexes)
+ bool wait, TM_FailureData *tmfd, UpdateContext *updateCxt)
{
return rel->rd_tableam->tuple_update(rel, otid, slot,
cid, snapshot, crosscheck,
- wait, tmfd,
- lockmode, update_indexes);
+ wait, tmfd, updateCxt);
}
/*
@@ -2010,7 +2032,7 @@ extern void simple_table_tuple_delete(Relation rel, ItemPointer tid,
Snapshot snapshot);
extern void simple_table_tuple_update(Relation rel, ItemPointer otid,
TupleTableSlot *slot, Snapshot snapshot,
- TU_UpdateIndexes *update_indexes);
+ UpdateContext *updateCxt);
/* ----------------------------------------------------------------------------
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index 4daa8bef5eea..fdbf47f607be 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -132,6 +132,7 @@ extern bool CompareIndexInfo(const IndexInfo *info1, const IndexInfo *info2,
const AttrMap *attmap);
extern void BuildSpeculativeIndexInfo(Relation index, IndexInfo *ii);
+extern void BuildExpressionIndexInfo(Relation index, IndexInfo *indexInfo);
extern void FormIndexDatum(IndexInfo *indexInfo,
TupleTableSlot *slot,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 0ba86c2ad723..e0495c3c529f 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -734,7 +734,7 @@ extern Bitmapset *ExecGetAllUpdatedCols(ResultRelInfo *relinfo, EState *estate);
/*
* prototypes from functions in execIndexing.c
*/
-extern void ExecOpenIndices(ResultRelInfo *resultRelInfo, bool speculative);
+extern void ExecOpenIndices(ResultRelInfo *resultRelInfo, bool speculative, bool update);
extern void ExecCloseIndices(ResultRelInfo *resultRelInfo);
extern List *ExecInsertIndexTuples(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate,
@@ -752,6 +752,12 @@ extern void check_exclusion_constraint(Relation heap, Relation index,
ItemPointer tupleid,
const Datum *values, const bool *isnull,
EState *estate, bool newIndex);
+extern bool ExecExprIndexesRequireUpdates(Relation relation,
+ ResultRelInfo *resultRelInfo,
+ Bitmapset *modifiedAttrs,
+ EState *estate,
+ TupleTableSlot *old_tts,
+ TupleTableSlot *new_tts);
/*
* prototypes from functions in execReplication.c
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index a36653c37f9e..e529587a6f11 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -178,11 +178,20 @@ typedef struct IndexInfo
List *ii_Expressions; /* list of Expr */
/* exec state for expressions, or NIL if none */
List *ii_ExpressionsState; /* list of ExprState */
+ /* attributes referenced by expressions, or NULL if none */
+ Bitmapset *ii_ExpressionsAttrs;
+
+ /* index attribute length */
+ uint16 ii_IndexAttrLen[INDEX_MAX_KEYS];
+ /* is the index attribute by-value */
+ Bitmapset *ii_IndexAttrByVal;
/* partial-index predicate, or NIL if none */
List *ii_Predicate; /* list of Expr */
/* exec state for expressions, or NIL if none */
ExprState *ii_PredicateState;
+ /* attributes referenced by the predicate, or NULL if none */
+ Bitmapset *ii_PredicateAttrs;
/* Per-column exclusion operators, or NULL if none */
Oid *ii_ExclusionOps; /* array with one entry per column */
@@ -206,6 +215,10 @@ typedef struct IndexInfo
bool ii_CheckedUnchanged;
/* aminsert hint, cached for retail inserts */
bool ii_IndexUnchanged;
+ /* partial index predicate determined yet? */
+ bool ii_CheckedPredicate;
+ /* amupdate hint used to avoid rechecking predicate */
+ bool ii_PredicateSatisfied;
/* are we doing a concurrent index build? */
bool ii_Concurrent;
/* did we detect any broken HOT chains? */
@@ -386,6 +399,8 @@ typedef struct ProjectionInfo
ExprState pi_state;
/* expression context in which to evaluate expression */
ExprContext *pi_exprContext;
+ /* the set of modified columns (attributes) */
+ Bitmapset *pi_modifiedCols;
} ProjectionInfo;
/* ----------------
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 80286076a111..219e7197bb2a 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -164,6 +164,7 @@ typedef struct RelationData
Bitmapset *rd_idattr; /* included in replica identity index */
Bitmapset *rd_hotblockingattr; /* cols blocking HOT update */
Bitmapset *rd_summarizedattr; /* cols indexed by summarizing indexes */
+ Bitmapset *rd_expressionattr; /* indexed cols referenced by expressions */
PublicationDesc *rd_pubdesc; /* publication descriptor, or NULL */
@@ -349,6 +350,7 @@ typedef struct StdRdOptions
StdRdOptIndexCleanup vacuum_index_cleanup; /* controls index vacuuming */
bool vacuum_truncate; /* enables vacuum to truncate a relation */
bool vacuum_truncate_set; /* whether vacuum_truncate is set */
+ bool expression_checks; /* use expression to checks for changes */
/*
* Fraction of pages in a relation that vacuum can eagerly scan and fail
@@ -410,6 +412,14 @@ typedef struct StdRdOptions
((relation)->rd_options ? \
((StdRdOptions *) (relation)->rd_options)->parallel_workers : (defaultpw))
+/*
+ * RelationGetExpressionChecks
+ * Returns the relation's expression_checks reloption setting.
+ */
+#define RelationGetExpressionChecks(relation) \
+ ((relation)->rd_options ? \
+ ((StdRdOptions *) (relation)->rd_options)->expression_checks : true)
+
/* ViewOptions->check_option values */
typedef enum ViewOptCheckOption
{
diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h
index 3561c6bef0bf..4b312bc8d06c 100644
--- a/src/include/utils/relcache.h
+++ b/src/include/utils/relcache.h
@@ -71,6 +71,7 @@ typedef enum IndexAttrBitmapKind
INDEX_ATTR_BITMAP_IDENTITY_KEY,
INDEX_ATTR_BITMAP_HOT_BLOCKING,
INDEX_ATTR_BITMAP_SUMMARIZED,
+ INDEX_ATTR_BITMAP_EXPRESSION,
} IndexAttrBitmapKind;
extern Bitmapset *RelationGetIndexAttrBitmap(Relation relation,
diff --git a/src/test/regress/expected/heap_hot_updates.out b/src/test/regress/expected/heap_hot_updates.out
new file mode 100644
index 000000000000..7d22befc34a6
--- /dev/null
+++ b/src/test/regress/expected/heap_hot_updates.out
@@ -0,0 +1,1048 @@
+-- Create a function to measure HOT updates
+CREATE OR REPLACE FUNCTION check_hot_updates(
+ expected INT,
+ p_table_name TEXT DEFAULT 't',
+ p_schema_name TEXT DEFAULT current_schema()
+)
+RETURNS TABLE (
+ table_name TEXT,
+ total_updates BIGINT,
+ hot_updates BIGINT,
+ hot_update_percentage NUMERIC,
+ matches_expected BOOLEAN,
+ has_indexes BOOLEAN,
+ index_count INT,
+ fillfactor INT
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_relid oid;
+ v_qualified_name TEXT;
+ v_hot_updates BIGINT;
+ v_updates BIGINT;
+ v_xact_hot_updates BIGINT;
+ v_xact_updates BIGINT;
+BEGIN
+
+ -- We need to wait for statistics to update
+ PERFORM pg_stat_force_next_flush();
+
+ -- Construct qualified name
+ v_qualified_name := quote_ident(p_schema_name) || '.' || quote_ident(p_table_name);
+
+ -- Get the OID using regclass
+ v_relid := v_qualified_name::regclass;
+
+ IF v_relid IS NULL THEN
+ RAISE EXCEPTION 'Table %.% not found', p_schema_name, p_table_name;
+ END IF;
+
+ -- Get cumulative stats
+ v_hot_updates := COALESCE(pg_stat_get_tuples_hot_updated(v_relid), 0);
+ v_updates := COALESCE(pg_stat_get_tuples_updated(v_relid), 0);
+
+ -- Get current transaction stats
+ v_xact_hot_updates := COALESCE(pg_stat_get_xact_tuples_hot_updated(v_relid), 0);
+ v_xact_updates := COALESCE(pg_stat_get_xact_tuples_updated(v_relid), 0);
+
+ -- Combine stats
+ v_hot_updates := v_hot_updates + v_xact_hot_updates;
+ v_updates := v_updates + v_xact_updates;
+
+ RETURN QUERY
+ SELECT
+ p_table_name::TEXT,
+ v_updates::BIGINT as total_updates,
+ v_hot_updates::BIGINT as hot_updates,
+ CASE
+ WHEN v_updates > 0 THEN
+ ROUND((v_hot_updates::numeric / v_updates::numeric * 100)::numeric, 2)
+ ELSE 0
+ END as hot_update_percentage,
+ (v_hot_updates = expected)::BOOLEAN as matches_expected,
+ (EXISTS (
+ SELECT 1 FROM pg_index WHERE indrelid = v_relid
+ ))::BOOLEAN as has_indexes,
+ (
+ SELECT COUNT(*)::INT
+ FROM pg_index
+ WHERE indrelid = v_relid
+ ) as index_count,
+ COALESCE(
+ (
+ SELECT (regexp_match(array_to_string(reloptions, ','), 'fillfactor=(\d+)'))[1]::int
+ FROM pg_class
+ WHERE oid = v_relid
+ ),
+ 100
+ ) as fillfactor;
+END;
+$$;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- This table will have two columns and two indexes, one on the primary key
+-- id and one on the expression (docs->>'name'). That means that the indexed
+-- attributes are 'id' and 'docs'.
+CREATE TABLE t(id INT PRIMARY KEY, docs JSONB) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_docs_idx ON t((docs->>'name'));
+INSERT INTO t VALUES (1, '{"name": "john", "data": "some data"}');
+-- Disable expression checks.
+ALTER TABLE t SET (expression_checks = false);
+SELECT reloptions FROM pg_class WHERE relname = 't';
+ reloptions
+----------------------------------------------------------------
+ {autovacuum_enabled=off,fillfactor=70,expression_checks=false}
+(1 row)
+
+-- While the indexed attribute "name" is unchanged we've disabled expression
+-- checks so this update should not go HOT as the system can't determine if
+-- the indexed attribute has changed without evaluating the expression.
+update t set docs='{"name": "john", "data": "something else"}' where id=1;
+SELECT * FROM check_hot_updates(0);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 1 | 0 | 0.00 | t | t | 2 | 70
+(1 row)
+
+-- Re-enable expression checks.
+ALTER TABLE t SET (expression_checks = true);
+SELECT reloptions FROM pg_class WHERE relname = 't';
+ reloptions
+---------------------------------------------------------------
+ {autovacuum_enabled=off,fillfactor=70,expression_checks=true}
+(1 row)
+
+-- The indexed attribute "name" with value "john" is unchanged, expect a HOT update.
+UPDATE t SET docs='{"name": "john", "data": "some other data"}' WHERE id=1;
+SELECT * FROM check_hot_updates(1);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 2 | 1 | 50.00 | t | t | 2 | 70
+(1 row)
+
+-- The following update changes the indexed attribute "name", this should not be a HOT update.
+UPDATE t SET docs='{"name": "smith", "data": "some other data"}' WHERE id=1;
+SELECT * FROM check_hot_updates(1);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 3 | 1 | 33.33 | t | t | 2 | 70
+(1 row)
+
+-- Now, this update does not change the indexed attribute "name" from "smith", this should be HOT.
+UPDATE t SET docs='{"name": "smith", "data": "some more data"}' WHERE id=1;
+SELECT * FROM check_hot_updates(2);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 4 | 2 | 50.00 | t | t | 2 | 70
+(1 row)
+
+DROP TABLE t;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- This table is the same as the previous one but it has a third index. The
+-- index 'colindex' isn't an expression index, it indexes the entire value
+-- in the docs column. There are still only two indexed attributes for this
+-- relation, the same two as before. The presence of an index on the entire
+-- value of the docs column should prevent HOT updates for any updates to any
+-- portion of JSONB content in that column.
+CREATE TABLE t(id INT PRIMARY KEY, docs JSONB) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_docs_idx ON t((docs->>'name'));
+CREATE INDEX t_docs_col_idx ON t(docs);
+INSERT INTO t VALUES (1, '{"name": "john", "data": "some data"}');
+-- This update doesn't change the value of the expression index, but it does
+-- change the content of the docs column and so should not be HOT because the
+-- indexed value changed as a result of the update.
+UPDATE t SET docs='{"name": "john", "data": "some other data"}' WHERE id=1;
+SELECT * FROM check_hot_updates(0);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 1 | 0 | 0.00 | t | t | 3 | 70
+(1 row)
+
+DROP TABLE t;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- The table has one column docs and two indexes. They are both expression
+-- indexes referencing the same column attribute (docs) but one is a partial
+-- index.
+CREATE TABLE t (docs JSONB) WITH (autovacuum_enabled = off, fillfactor = 70);
+INSERT INTO t (docs) VALUES ('{"a": 0, "b": 0}');
+INSERT INTO t (docs) SELECT jsonb_build_object('b', n) FROM generate_series(100, 10000) as n;
+CREATE INDEX t_idx_a ON t ((docs->>'a'));
+CREATE INDEX t_idx_b ON t ((docs->>'b')) WHERE (docs->>'b')::numeric > 9;
+-- We're using BTREE indexes and for this test we want to make sure that they remain
+-- in sync with changes to our relation. Force the choice of index scans below so
+-- that we know we're checking the index's understanding of what values should be
+-- in the index or not.
+SET SESSION enable_seqscan = OFF;
+SET SESSION enable_bitmapscan = OFF;
+-- Leave 'a' unchanged but modify 'b' to a value outside of the index predicate.
+-- This should be a HOT update because neither index is changed.
+UPDATE t SET docs = jsonb_build_object('a', 0, 'b', 1) WHERE (docs->>'a')::numeric = 0;
+SELECT * FROM check_hot_updates(1);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 1 | 1 | 100.00 | t | t | 2 | 70
+(1 row)
+
+-- Let's check to make sure that the index does not contain a value for 'b'
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+ QUERY PLAN
+--------------------------------------------------------------
+ Index Scan using t_idx_b on t
+ Filter: (((docs ->> 'b'::text))::numeric < '100'::numeric)
+(2 rows)
+
+SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+ docs
+------
+(0 rows)
+
+-- Leave 'a' unchanged but modify 'b' to a value within the index predicate.
+-- This represents a change for field 'b' from unindexed to indexed and so
+-- this should not take the HOT path.
+UPDATE t SET docs = jsonb_build_object('a', 0, 'b', 10) WHERE (docs->>'a')::numeric = 0;
+SELECT * FROM check_hot_updates(1);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 2 | 1 | 50.00 | t | t | 2 | 70
+(1 row)
+
+-- Let's check to make sure that the index contains the new value of 'b'
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+ QUERY PLAN
+--------------------------------------------------------------
+ Index Scan using t_idx_b on t
+ Filter: (((docs ->> 'b'::text))::numeric < '100'::numeric)
+(2 rows)
+
+SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+ docs
+-------------------
+ {"a": 0, "b": 10}
+(1 row)
+
+-- This update modifies the value of 'a', an indexed field, so it also cannot
+-- be a HOT update.
+UPDATE t SET docs = jsonb_build_object('a', 1, 'b', 10) WHERE (docs->>'b')::numeric = 10;
+SELECT * FROM check_hot_updates(1);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 3 | 1 | 33.33 | t | t | 2 | 70
+(1 row)
+
+-- This update changes both 'a' and 'b' to new values that require index updates,
+-- this cannot use the HOT path.
+UPDATE t SET docs = jsonb_build_object('a', 2, 'b', 12) WHERE (docs->>'b')::numeric = 10;
+SELECT * FROM check_hot_updates(1);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 4 | 1 | 25.00 | t | t | 2 | 70
+(1 row)
+
+-- Let's check to make sure that the index contains the new value of 'b'
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+ QUERY PLAN
+--------------------------------------------------------------
+ Index Scan using t_idx_b on t
+ Filter: (((docs ->> 'b'::text))::numeric < '100'::numeric)
+(2 rows)
+
+SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+ docs
+-------------------
+ {"a": 2, "b": 12}
+(1 row)
+
+-- This update changes 'b' to a value outside its predicate requiring that
+-- we remove it from the index. That's a transition that can't be done
+-- during a HOT update.
+UPDATE t SET docs = jsonb_build_object('a', 2, 'b', 1) WHERE (docs->>'b')::numeric = 12;
+SELECT * FROM check_hot_updates(1);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 5 | 1 | 20.00 | t | t | 2 | 70
+(1 row)
+
+-- Let's check to make sure that the index no longer contains the value of 'b'
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+ QUERY PLAN
+--------------------------------------------------------------
+ Index Scan using t_idx_b on t
+ Filter: (((docs ->> 'b'::text))::numeric < '100'::numeric)
+(2 rows)
+
+SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+ docs
+------
+(0 rows)
+
+DROP TABLE t;
+SET SESSION enable_seqscan = ON;
+SET SESSION enable_bitmapscan = ON;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- Tests to ensure that HOT updates are not performed when multiple indexed
+-- attributes are updated.
+CREATE TABLE t(a INT, b INT) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_idx_a ON t(a);
+CREATE INDEX t_idx_b ON t(abs(b));
+INSERT INTO t VALUES (1, -1);
+-- Both are updated, the second is an expression index with an unchanged
+-- index value. The change to the index on a should prevent HOT updates.
+UPDATE t SET a = 2, b = 1 WHERE a = 1;
+SELECT * FROM check_hot_updates(0);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 1 | 0 | 0.00 | t | t | 2 | 70
+(1 row)
+
+DROP TABLE t;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- Tests to check the expression_checks reloption behavior.
+--
+CREATE TABLE t(a INT, b INT) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_idx_a ON t(abs(a)) WHERE abs(a) > 10;
+CREATE INDEX t_idx_b ON t(abs(b));
+INSERT INTO t VALUES (-1, -1), (-2, -2), (-3, -3), (-4, -4);
+SET SESSION enable_seqscan = OFF;
+SET SESSION enable_bitmapscan = OFF;
+-- Disable expression checks on indexes and partial index predicates.
+ALTER TABLE t SET (expression_checks = false);
+-- Before and after values of a are outside the predicate of the index and
+-- the indexed value of b hasn't changed however we've disabled expression
+-- checks so this should not be a HOT update.
+-- (-1, -1) -> (-5, -1)
+UPDATE t SET a = -5, b = -1 WHERE a = -1;
+SELECT * FROM check_hot_updates(0);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 1 | 0 | 0.00 | t | t | 2 | 70
+(1 row)
+
+-- Enable expression checks on indexes, but not on predicates yet.
+ALTER TABLE t SET (expression_checks = true);
+-- The indexed value of b hasn't changed, this should be a HOT update.
+-- (-5, -1) -> (-5, 1)
+UPDATE t SET b = 1 WHERE a = -5;
+SELECT * FROM check_hot_updates(1);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 2 | 1 | 50.00 | t | t | 2 | 70
+(1 row)
+
+-- Now that we're not checking the predicate of the partial index, this
+-- update of a from -5 to 5 should be HOT because we should ignore the
+-- predicate and check the expression and find it unchanged.
+-- (-5, 1) -> (5, 1)
+UPDATE t SET a = 5 WHERE a = -5;
+SELECT * FROM check_hot_updates(2);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 3 | 2 | 66.67 | t | t | 2 | 70
+(1 row)
+
+-- This update meets the critera for the partial index and should not
+-- be HOT. Let's make sure of that and check the index as well.
+-- (-4, -4) -> (-11, -4)
+UPDATE t SET a = -11 WHERE a = -4;
+SELECT * FROM check_hot_updates(2);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 4 | 2 | 50.00 | t | t | 2 | 70
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE abs(a) > 10;
+ QUERY PLAN
+-------------------------------
+ Index Scan using t_idx_a on t
+(1 row)
+
+SELECT * FROM t WHERE abs(a) > 10;
+ a | b
+-----+----
+ -11 | -4
+(1 row)
+
+-- (-11, -4) -> (11, -4)
+UPDATE t SET a = 11 WHERE a = -11;
+SELECT * FROM check_hot_updates(3);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 5 | 3 | 60.00 | t | t | 2 | 70
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE abs(a) > 10;
+ QUERY PLAN
+-------------------------------
+ Index Scan using t_idx_a on t
+(1 row)
+
+SELECT * FROM t WHERE abs(a) > 10;
+ a | b
+----+----
+ 11 | -4
+(1 row)
+
+-- (11, -4) -> (-4, -4)
+UPDATE t SET a = -4 WHERE a = 11;
+SELECT * FROM check_hot_updates(3);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 6 | 3 | 50.00 | t | t | 2 | 70
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE abs(a) > 10;
+ QUERY PLAN
+-------------------------------
+ Index Scan using t_idx_a on t
+(1 row)
+
+SELECT * FROM t WHERE abs(a) > 10;
+ a | b
+---+---
+(0 rows)
+
+-- This update of a from 5 to -1 is HOT despite that attribute
+-- being indexed because the before and after values for the
+-- partial index predicate are outside the index definition.
+-- (5, 1) -> (-1, 1)
+UPDATE t SET a = -1 WHERE a = 5;
+SELECT * FROM check_hot_updates(4);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 7 | 4 | 57.14 | t | t | 2 | 70
+(1 row)
+
+-- This update of a from -2 to -1 with predicate checks enabled should be
+-- HOT because the before/after values of a are both outside the predicate
+-- of the partial index.
+-- (-1, 1) -> (-2, 1)
+UPDATE t SET a = -2 WHERE a = -1;
+SELECT * FROM check_hot_updates(5);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 8 | 5 | 62.50 | t | t | 2 | 70
+(1 row)
+
+-- The indexed value for b isn't changing, this should be HOT.
+-- (-2, -2) -> (-2, 2)
+UPDATE t SET b = 2 WHERE b = -2;
+SELECT * FROM check_hot_updates(6);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 9 | 6 | 66.67 | t | t | 2 | 70
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT abs(b) FROM t;
+ QUERY PLAN
+------------------
+ Seq Scan on t
+ Disabled: true
+(2 rows)
+
+SELECT abs(b) FROM t;
+ abs
+-----
+ 3
+ 4
+ 1
+ 2
+(4 rows)
+
+-- Before and after values for a are outside the predicate of the index,
+-- and because we're checking this should be HOT.
+-- (-2, 1) -> (5, 1)
+-- (-2, -2) -> (5, -2)
+UPDATE t SET a = 5 WHERE a = -2;
+SELECT * FROM check_hot_updates(8);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 11 | 8 | 72.73 | t | t | 2 | 70
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE abs(a) > 10;
+ QUERY PLAN
+-------------------------------
+ Index Scan using t_idx_a on t
+(1 row)
+
+SELECT * FROM t WHERE abs(a) > 10;
+ a | b
+---+---
+(0 rows)
+
+SELECT * FROM t;
+ a | b
+----+----
+ -3 | -3
+ -4 | -4
+ 5 | 1
+ 5 | 2
+(4 rows)
+
+DROP TABLE t;
+SET SESSION enable_seqscan = ON;
+SET SESSION enable_bitmapscan = ON;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- The tests here examines the behavior of HOT updates when the relation
+-- has a JSONB column with an index on the field 'a' and the partial index
+-- expression on a different JSONB field 'b'.
+CREATE TABLE t(docs JSONB) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_docs_idx ON t((docs->'a')) WHERE (docs->'b')::integer = 1;
+INSERT INTO t VALUES ('{"a": 1, "b": 1}');
+EXPLAIN (COSTS OFF) SELECT * FROM t;
+ QUERY PLAN
+---------------
+ Seq Scan on t
+(1 row)
+
+SELECT * FROM t;
+ docs
+------------------
+ {"a": 1, "b": 1}
+(1 row)
+
+SET SESSION enable_seqscan = OFF;
+SET SESSION enable_bitmapscan = OFF;
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE (docs->'b')::integer = 1;
+ QUERY PLAN
+----------------------------------
+ Index Scan using t_docs_idx on t
+(1 row)
+
+SELECT * FROM t WHERE (docs->'b')::integer = 1;
+ docs
+------------------
+ {"a": 1, "b": 1}
+(1 row)
+
+SELECT * FROM check_hot_updates(0);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 0 | 0 | 0 | t | t | 1 | 70
+(1 row)
+
+UPDATE t SET docs='{"a": 1, "b": 0}';
+SELECT * FROM check_hot_updates(0);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 1 | 0 | 0.00 | t | t | 1 | 70
+(1 row)
+
+SELECT * FROM t WHERE (docs->'b')::integer = 1;
+ docs
+------
+(0 rows)
+
+SET SESSION enable_seqscan = ON;
+SET SESSION enable_bitmapscan = ON;
+DROP TABLE t;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- Tests for multi-column indexes
+--
+CREATE TABLE t(id INT, docs JSONB) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_docs_idx ON t(id, (docs->'a'));
+INSERT INTO t VALUES (1, '{"a": 1, "b": 1}');
+SET SESSION enable_seqscan = OFF;
+SET SESSION enable_bitmapscan = OFF;
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer > 0;
+ QUERY PLAN
+------------------------------------------------
+ Index Scan using t_docs_idx on t
+ Index Cond: (id > 0)
+ Filter: (((docs -> 'a'::text))::integer > 0)
+(3 rows)
+
+SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer > 0;
+ id | docs
+----+------------------
+ 1 | {"a": 1, "b": 1}
+(1 row)
+
+SELECT * FROM check_hot_updates(0);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 0 | 0 | 0 | t | t | 1 | 70
+(1 row)
+
+-- Changing the id attribute which is an indexed attribute should
+-- prevent HOT updates.
+UPDATE t SET id = 2;
+SELECT * FROM check_hot_updates(0);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 1 | 0 | 0.00 | t | t | 1 | 70
+(1 row)
+
+SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer > 0;
+ id | docs
+----+------------------
+ 2 | {"a": 1, "b": 1}
+(1 row)
+
+-- Changing the docs->'a' field in the indexed attribute 'docs'
+-- should prevent HOT updates.
+UPDATE t SET docs='{"a": -2, "b": 1}';
+SELECT * FROM check_hot_updates(0);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 2 | 0 | 0.00 | t | t | 1 | 70
+(1 row)
+
+SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer < 0;
+ id | docs
+----+-------------------
+ 2 | {"a": -2, "b": 1}
+(1 row)
+
+-- Leaving the docs->'a' attribute unchanged means that the expression
+-- is unchanged and because the 'id' attribute isn't in the modified
+-- set the indexed tuple is unchanged, this can go HOT.
+UPDATE t SET docs='{"a": -2, "b": 2}';
+SELECT * FROM check_hot_updates(1);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 3 | 1 | 33.33 | t | t | 1 | 70
+(1 row)
+
+SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer < 0;
+ id | docs
+----+-------------------
+ 2 | {"a": -2, "b": 2}
+(1 row)
+
+-- Here we change the 'id' attribute and the 'docs' attribute setting
+-- the expression docs->'a' to a new value, this cannot be a HOT update.
+UPDATE t SET id = 3, docs='{"a": 3, "b": 3}';
+SELECT * FROM check_hot_updates(1);
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ t | 4 | 1 | 25.00 | t | t | 1 | 70
+(1 row)
+
+SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer > 0;
+ id | docs
+----+------------------
+ 3 | {"a": 3, "b": 3}
+(1 row)
+
+SET SESSION enable_seqscan = ON;
+SET SESSION enable_bitmapscan = ON;
+DROP TABLE t;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- This table has a single column 'email' and a unique constraint on it that
+-- should preclude HOT updates.
+CREATE TABLE users (
+ user_id serial primary key,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ EXCLUDE USING btree (lower(email) WITH =)
+);
+-- Add some data to the table and then update it in ways that should and should
+-- not be HOT updates.
+INSERT INTO users (name, email) VALUES
+('user1', 'user1@example.com'),
+('user2', 'user2@example.com'),
+('taken', 'taken@EXAMPLE.com'),
+('you', 'you@domain.com'),
+('taken', 'taken@domain.com');
+-- Should fail because of the unique constraint on the email column.
+UPDATE users SET email = 'user1@example.com' WHERE email = 'user2@example.com';
+ERROR: conflicting key value violates exclusion constraint "users_lower_excl"
+DETAIL: Key (lower(email::text))=(user1@example.com) conflicts with existing key (lower(email::text))=(user1@example.com).
+SELECT * FROM check_hot_updates(0, 'users');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ users | 1 | 0 | 0.00 | t | t | 2 | 100
+(1 row)
+
+-- Should succeed because the email column is not being updated and should go HOT.
+UPDATE users SET name = 'foo' WHERE email = 'user1@example.com';
+SELECT * FROM check_hot_updates(1, 'users');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ users | 2 | 1 | 50.00 | t | t | 2 | 100
+(1 row)
+
+-- Create a partial index on the email column, updates
+CREATE INDEX idx_users_email_no_example ON users (lower(email)) WHERE lower(email) LIKE '%@example.com%';
+-- An update that changes the email column but not the indexed portion of it and falls outside the constraint.
+-- Shouldn't be a HOT update because of the exclusion constraint.
+UPDATE users SET email = 'you+2@domain.com' WHERE name = 'you';
+SELECT * FROM check_hot_updates(1, 'users');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ users | 3 | 1 | 33.33 | t | t | 3 | 100
+(1 row)
+
+-- An update that changes the email column but not the indexed portion of it and falls within the constraint.
+-- Again, should fail constraint and fail to be a HOT update.
+UPDATE users SET email = 'taken@domain.com' WHERE name = 'you';
+ERROR: conflicting key value violates exclusion constraint "users_lower_excl"
+DETAIL: Key (lower(email::text))=(taken@domain.com) conflicts with existing key (lower(email::text))=(taken@domain.com).
+SELECT * FROM check_hot_updates(1, 'users');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ users | 4 | 1 | 25.00 | t | t | 3 | 100
+(1 row)
+
+DROP TABLE users;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- Another test of constraints spoiling HOT updates, this time with a range.
+CREATE TABLE events (
+ id serial primary key,
+ name VARCHAR(255) NOT NULL,
+ event_time tstzrange,
+ constraint no_screening_time_overlap exclude using gist (
+ event_time WITH &&
+ )
+);
+-- Add two non-overlapping events.
+INSERT INTO events (id, event_time, name)
+VALUES
+ (1, '["2023-01-01 19:00:00", "2023-01-01 20:45:00"]', 'event1'),
+ (2, '["2023-01-01 21:00:00", "2023-01-01 21:45:00"]', 'event2');
+-- Update the first event to overlap with the second, should fail the constraint and not be HOT.
+UPDATE events SET event_time = '["2023-01-01 20:00:00", "2023-01-01 21:45:00"]' WHERE id = 1;
+ERROR: conflicting key value violates exclusion constraint "no_screening_time_overlap"
+DETAIL: Key (event_time)=(["Sun Jan 01 20:00:00 2023 PST","Sun Jan 01 21:45:00 2023 PST"]) conflicts with existing key (event_time)=(["Sun Jan 01 21:00:00 2023 PST","Sun Jan 01 21:45:00 2023 PST"]).
+SELECT * FROM check_hot_updates(0, 'events');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ events | 1 | 0 | 0.00 | t | t | 2 | 100
+(1 row)
+
+-- Update the first event to not overlap with the second, again not HOT due to the constraint.
+UPDATE events SET event_time = '["2023-01-01 22:00:00", "2023-01-01 22:45:00"]' WHERE id = 1;
+SELECT * FROM check_hot_updates(0, 'events');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ events | 2 | 0 | 0.00 | t | t | 2 | 100
+(1 row)
+
+-- Update the first event to not overlap with the second, this time we're HOT because we don't overlap with the constraint.
+UPDATE events SET name = 'new name here' WHERE id = 1;
+SELECT * FROM check_hot_updates(1, 'events');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ events | 3 | 1 | 33.33 | t | t | 2 | 100
+(1 row)
+
+DROP TABLE events;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- A test to ensure that only modified summarizing indexes are updated, not
+-- all of them.
+CREATE TABLE ex (id SERIAL primary key, att1 JSONB, att2 text, att3 text, att4 text) WITH (fillfactor = 60);
+CREATE INDEX ex_expr1_idx ON ex USING btree((att1->'data'));
+CREATE INDEX ex_sumr1_idx ON ex USING BRIN(att2);
+CREATE INDEX ex_expr2_idx ON ex USING btree((att1->'a'));
+CREATE INDEX ex_expr3_idx ON ex USING btree((att1->'b'));
+CREATE INDEX ex_expr4_idx ON ex USING btree((att1->'c'));
+CREATE INDEX ex_sumr2_idx ON ex USING BRIN(att3);
+CREATE INDEX ex_sumr3_idx ON ex USING BRIN(att4);
+CREATE INDEX ex_expr5_idx ON ex USING btree((att1->'d'));
+INSERT INTO ex (att1, att2) VALUES ('{"data": []}'::json, 'nothing special');
+SELECT * FROM ex;
+ id | att1 | att2 | att3 | att4
+----+--------------+-----------------+------+------
+ 1 | {"data": []} | nothing special | |
+(1 row)
+
+-- Update att2 and att4 both are BRIN/summarizing indexes, this should be a HOT update and
+-- only update two of the three summarizing indexes.
+UPDATE ex SET att2 = 'special indeed', att4 = 'whatever';
+SELECT * FROM check_hot_updates(1, 'ex');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ ex | 1 | 1 | 100.00 | t | t | 9 | 60
+(1 row)
+
+SELECT * FROM ex;
+ id | att1 | att2 | att3 | att4
+----+--------------+----------------+------+----------
+ 1 | {"data": []} | special indeed | | whatever
+(1 row)
+
+-- Update att1 and att2, only one is BRIN/summarizing, this should NOT be a HOT update.
+UPDATE ex SET att1 = att1 || '{"data": "howdy"}', att2 = 'special, so special';
+SELECT * FROM check_hot_updates(1, 'ex');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ ex | 2 | 1 | 50.00 | t | t | 9 | 60
+(1 row)
+
+SELECT * FROM ex;
+ id | att1 | att2 | att3 | att4
+----+-------------------+---------------------+------+----------
+ 1 | {"data": "howdy"} | special, so special | | whatever
+(1 row)
+
+-- Update att2, att3, and att4 all are BRIN/summarizing indexes, this should be a HOT update
+-- and yet still update all three summarizing indexes.
+UPDATE ex SET att2 = 'a', att3 = 'b', att4 = 'c';
+SELECT * FROM check_hot_updates(2, 'ex');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ ex | 3 | 2 | 66.67 | t | t | 9 | 60
+(1 row)
+
+SELECT * FROM ex;
+ id | att1 | att2 | att3 | att4
+----+-------------------+------+------+------
+ 1 | {"data": "howdy"} | a | b | c
+(1 row)
+
+-- Update att1, att2, and att3 all modified values are BRIN/summarizing indexes, this should be a HOT update
+-- and yet still update all three summarizing indexes.
+UPDATE ex SET att1 = '{"data": "howdy"}', att2 = 'd', att3 = 'e';
+SELECT * FROM check_hot_updates(3, 'ex');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ ex | 4 | 3 | 75.00 | t | t | 9 | 60
+(1 row)
+
+SELECT * FROM ex;
+ id | att1 | att2 | att3 | att4
+----+-------------------+------+------+------
+ 1 | {"data": "howdy"} | d | e | c
+(1 row)
+
+DROP TABLE ex;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- A test to ensure that summarizing indexes are not updated when they don't
+-- change, but are updated when they do while not prefent HOT updates.
+CREATE TABLE ex (att1 JSONB, att2 text) WITH (fillfactor = 60);
+CREATE INDEX ex_expr1_idx ON ex USING btree((att1->'data'));
+CREATE INDEX ex_sumr1_idx ON ex USING BRIN(att2);
+INSERT INTO ex VALUES ('{"data": []}', 'nothing special');
+-- Update the unindexed value of att1, this should be a HOT update and and should
+-- update the summarizing index.
+UPDATE ex SET att1 = att1 || '{"status": "stalemate"}';
+SELECT * FROM check_hot_updates(1, 'ex');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ ex | 1 | 1 | 100.00 | t | t | 2 | 60
+(1 row)
+
+-- Update the indexed value of att2, a summarized value, this is a summarized
+-- only update and should use the HOT path while still triggering an update to
+-- the summarizing BRIN index.
+UPDATE ex SET att2 = 'special indeed';
+SELECT * FROM check_hot_updates(2, 'ex');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ ex | 2 | 2 | 100.00 | t | t | 2 | 60
+(1 row)
+
+-- Update to att1 doesn't change the indexed value while the update to att2 does,
+-- this again is a summarized only update and should use the HOT path as well as
+-- trigger an update to the BRIN index.
+UPDATE ex SET att1 = att1 || '{"status": "checkmate"}', att2 = 'special, so special';
+SELECT * FROM check_hot_updates(3, 'ex');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ ex | 3 | 3 | 100.00 | t | t | 2 | 60
+(1 row)
+
+-- This updates both indexes, the expression index on att1 and the summarizing
+-- index on att2. This should not be a HOT update because there are modified
+-- indexes and only some are summarized, not all. This should force all
+-- indexes to be updated.
+UPDATE ex SET att1 = att1 || '{"data": [1,2,3]}', att2 = 'do you want to play a game?';
+SELECT * FROM check_hot_updates(4, 'ex');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ ex | 4 | 3 | 75.00 | f | t | 2 | 60
+(1 row)
+
+DROP TABLE ex;
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- This test is for a table with a custom type and a custom operators on
+-- the BTREE index. The question is, when comparing values for equality
+-- to determine if there are changes on the index or not... shouldn't we
+-- be using the custom operators?
+-- Create a type
+CREATE TYPE my_custom_type AS (val int);
+-- Comparison functions (returns boolean)
+CREATE FUNCTION my_custom_lt(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val < b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+CREATE FUNCTION my_custom_le(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val <= b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+CREATE FUNCTION my_custom_eq(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val = b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+CREATE FUNCTION my_custom_ge(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val >= b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+CREATE FUNCTION my_custom_gt(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val > b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+CREATE FUNCTION my_custom_ne(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val != b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+-- Comparison function (returns -1, 0, 1)
+CREATE FUNCTION my_custom_cmp(a my_custom_type, b my_custom_type) RETURNS int AS $$
+BEGIN
+ IF a.val < b.val THEN
+ RETURN -1;
+ ELSIF a.val > b.val THEN
+ RETURN 1;
+ ELSE
+ RETURN 0;
+ END IF;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+-- Create the operators
+CREATE OPERATOR < (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_lt,
+ COMMUTATOR = >,
+ NEGATOR = >=
+);
+CREATE OPERATOR <= (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_le,
+ COMMUTATOR = >=,
+ NEGATOR = >
+);
+CREATE OPERATOR = (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_eq,
+ COMMUTATOR = =,
+ NEGATOR = <>
+);
+CREATE OPERATOR >= (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_ge,
+ COMMUTATOR = <=,
+ NEGATOR = <
+);
+CREATE OPERATOR > (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_gt,
+ COMMUTATOR = <,
+ NEGATOR = <=
+);
+CREATE OPERATOR <> (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_ne,
+ COMMUTATOR = <>,
+ NEGATOR = =
+);
+-- Create the operator class (including the support function)
+CREATE OPERATOR CLASS my_custom_ops
+ DEFAULT FOR TYPE my_custom_type USING btree AS
+ OPERATOR 1 <,
+ OPERATOR 2 <=,
+ OPERATOR 3 =,
+ OPERATOR 4 >=,
+ OPERATOR 5 >,
+ FUNCTION 1 my_custom_cmp(my_custom_type, my_custom_type);
+-- Create the table
+CREATE TABLE my_table (
+ id int,
+ custom_val my_custom_type
+);
+-- Insert some data
+INSERT INTO my_table (id, custom_val) VALUES
+(1, ROW(3)::my_custom_type),
+(2, ROW(1)::my_custom_type),
+(3, ROW(4)::my_custom_type),
+(4, ROW(2)::my_custom_type);
+-- Create a function to use when indexing
+CREATE OR REPLACE FUNCTION abs_val(val my_custom_type) RETURNS int AS $$
+BEGIN
+ RETURN abs(val.val);
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+-- Create the index
+CREATE INDEX idx_custom_val_abs ON my_table (abs_val(custom_val));
+-- Update 1
+UPDATE my_table SET custom_val = ROW(5)::my_custom_type WHERE id = 1;
+SELECT * FROM check_hot_updates(0, 'my_table');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ my_table | 1 | 0 | 0.00 | t | t | 1 | 100
+(1 row)
+
+-- Update 2
+UPDATE my_table SET custom_val = ROW(0)::my_custom_type WHERE custom_val < ROW(3)::my_custom_type;
+SELECT * FROM check_hot_updates(0, 'my_table');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ my_table | 3 | 0 | 0.00 | t | t | 1 | 100
+(1 row)
+
+-- Update 3
+UPDATE my_table SET custom_val = ROW(6)::my_custom_type WHERE id = 3;
+SELECT * FROM check_hot_updates(0, 'my_table');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ my_table | 4 | 0 | 0.00 | t | t | 1 | 100
+(1 row)
+
+-- Update 4
+UPDATE my_table SET id = 5 WHERE id = 1;
+SELECT pg_stat_get_xact_tuples_hot_updated('my_table'::regclass);
+ pg_stat_get_xact_tuples_hot_updated
+-------------------------------------
+ 1
+(1 row)
+
+SELECT * FROM check_hot_updates(0, 'my_table');
+ table_name | total_updates | hot_updates | hot_update_percentage | matches_expected | has_indexes | index_count | fillfactor
+------------+---------------+-------------+-----------------------+------------------+-------------+-------------+------------
+ my_table | 5 | 1 | 20.00 | f | t | 1 | 100
+(1 row)
+
+-- Query using the index
+EXPLAIN (COSTS OFF) SELECT * FROM my_table WHERE abs_val(custom_val) = 6;
+ QUERY PLAN
+-------------------------------------
+ Seq Scan on my_table
+ Filter: (abs_val(custom_val) = 6)
+(2 rows)
+
+SELECT * FROM my_table WHERE abs_val(custom_val) = 6;
+ id | custom_val
+----+------------
+ 3 | (6)
+(1 row)
+
+-- Clean up
+DROP TABLE my_table CASCADE;
+DROP OPERATOR CLASS my_custom_ops USING btree CASCADE;
+DROP OPERATOR < (my_custom_type, my_custom_type);
+DROP OPERATOR <= (my_custom_type, my_custom_type);
+DROP OPERATOR = (my_custom_type, my_custom_type);
+DROP OPERATOR >= (my_custom_type, my_custom_type);
+DROP OPERATOR > (my_custom_type, my_custom_type);
+DROP OPERATOR <> (my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_lt(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_le(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_eq(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_ge(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_gt(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_ne(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_cmp(my_custom_type, my_custom_type);
+DROP FUNCTION abs_val(my_custom_type);
+DROP TYPE my_custom_type CASCADE;
+DROP FUNCTION check_hot_updates(int, text, text);
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index a0f5fab0f5df..8b609517a37a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -68,6 +68,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
# ----------
test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated_stored join_hash
+# ----------
+# Another group of parallel tests
+# ----------
+test: heap_hot_updates
+
# ----------
# Additional BRIN tests
# ----------
diff --git a/src/test/regress/sql/heap_hot_updates.sql b/src/test/regress/sql/heap_hot_updates.sql
new file mode 100644
index 000000000000..7016e9eabd01
--- /dev/null
+++ b/src/test/regress/sql/heap_hot_updates.sql
@@ -0,0 +1,718 @@
+-- Create a function to measure HOT updates
+CREATE OR REPLACE FUNCTION check_hot_updates(
+ expected INT,
+ p_table_name TEXT DEFAULT 't',
+ p_schema_name TEXT DEFAULT current_schema()
+)
+RETURNS TABLE (
+ table_name TEXT,
+ total_updates BIGINT,
+ hot_updates BIGINT,
+ hot_update_percentage NUMERIC,
+ matches_expected BOOLEAN,
+ has_indexes BOOLEAN,
+ index_count INT,
+ fillfactor INT
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_relid oid;
+ v_qualified_name TEXT;
+ v_hot_updates BIGINT;
+ v_updates BIGINT;
+ v_xact_hot_updates BIGINT;
+ v_xact_updates BIGINT;
+BEGIN
+
+ -- We need to wait for statistics to update
+ PERFORM pg_stat_force_next_flush();
+
+ -- Construct qualified name
+ v_qualified_name := quote_ident(p_schema_name) || '.' || quote_ident(p_table_name);
+
+ -- Get the OID using regclass
+ v_relid := v_qualified_name::regclass;
+
+ IF v_relid IS NULL THEN
+ RAISE EXCEPTION 'Table %.% not found', p_schema_name, p_table_name;
+ END IF;
+
+ -- Get cumulative stats
+ v_hot_updates := COALESCE(pg_stat_get_tuples_hot_updated(v_relid), 0);
+ v_updates := COALESCE(pg_stat_get_tuples_updated(v_relid), 0);
+
+ -- Get current transaction stats
+ v_xact_hot_updates := COALESCE(pg_stat_get_xact_tuples_hot_updated(v_relid), 0);
+ v_xact_updates := COALESCE(pg_stat_get_xact_tuples_updated(v_relid), 0);
+
+ -- Combine stats
+ v_hot_updates := v_hot_updates + v_xact_hot_updates;
+ v_updates := v_updates + v_xact_updates;
+
+ RETURN QUERY
+ SELECT
+ p_table_name::TEXT,
+ v_updates::BIGINT as total_updates,
+ v_hot_updates::BIGINT as hot_updates,
+ CASE
+ WHEN v_updates > 0 THEN
+ ROUND((v_hot_updates::numeric / v_updates::numeric * 100)::numeric, 2)
+ ELSE 0
+ END as hot_update_percentage,
+ (v_hot_updates = expected)::BOOLEAN as matches_expected,
+ (EXISTS (
+ SELECT 1 FROM pg_index WHERE indrelid = v_relid
+ ))::BOOLEAN as has_indexes,
+ (
+ SELECT COUNT(*)::INT
+ FROM pg_index
+ WHERE indrelid = v_relid
+ ) as index_count,
+ COALESCE(
+ (
+ SELECT (regexp_match(array_to_string(reloptions, ','), 'fillfactor=(\d+)'))[1]::int
+ FROM pg_class
+ WHERE oid = v_relid
+ ),
+ 100
+ ) as fillfactor;
+END;
+$$;
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- This table will have two columns and two indexes, one on the primary key
+-- id and one on the expression (docs->>'name'). That means that the indexed
+-- attributes are 'id' and 'docs'.
+CREATE TABLE t(id INT PRIMARY KEY, docs JSONB) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_docs_idx ON t((docs->>'name'));
+INSERT INTO t VALUES (1, '{"name": "john", "data": "some data"}');
+
+-- Disable expression checks.
+ALTER TABLE t SET (expression_checks = false);
+SELECT reloptions FROM pg_class WHERE relname = 't';
+
+-- While the indexed attribute "name" is unchanged we've disabled expression
+-- checks so this update should not go HOT as the system can't determine if
+-- the indexed attribute has changed without evaluating the expression.
+update t set docs='{"name": "john", "data": "something else"}' where id=1;
+SELECT * FROM check_hot_updates(0);
+
+-- Re-enable expression checks.
+ALTER TABLE t SET (expression_checks = true);
+SELECT reloptions FROM pg_class WHERE relname = 't';
+
+-- The indexed attribute "name" with value "john" is unchanged, expect a HOT update.
+UPDATE t SET docs='{"name": "john", "data": "some other data"}' WHERE id=1;
+SELECT * FROM check_hot_updates(1);
+
+-- The following update changes the indexed attribute "name", this should not be a HOT update.
+UPDATE t SET docs='{"name": "smith", "data": "some other data"}' WHERE id=1;
+SELECT * FROM check_hot_updates(1);
+
+-- Now, this update does not change the indexed attribute "name" from "smith", this should be HOT.
+UPDATE t SET docs='{"name": "smith", "data": "some more data"}' WHERE id=1;
+SELECT * FROM check_hot_updates(2);
+
+DROP TABLE t;
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- This table is the same as the previous one but it has a third index. The
+-- index 'colindex' isn't an expression index, it indexes the entire value
+-- in the docs column. There are still only two indexed attributes for this
+-- relation, the same two as before. The presence of an index on the entire
+-- value of the docs column should prevent HOT updates for any updates to any
+-- portion of JSONB content in that column.
+CREATE TABLE t(id INT PRIMARY KEY, docs JSONB) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_docs_idx ON t((docs->>'name'));
+CREATE INDEX t_docs_col_idx ON t(docs);
+INSERT INTO t VALUES (1, '{"name": "john", "data": "some data"}');
+
+-- This update doesn't change the value of the expression index, but it does
+-- change the content of the docs column and so should not be HOT because the
+-- indexed value changed as a result of the update.
+UPDATE t SET docs='{"name": "john", "data": "some other data"}' WHERE id=1;
+SELECT * FROM check_hot_updates(0);
+DROP TABLE t;
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- The table has one column docs and two indexes. They are both expression
+-- indexes referencing the same column attribute (docs) but one is a partial
+-- index.
+CREATE TABLE t (docs JSONB) WITH (autovacuum_enabled = off, fillfactor = 70);
+INSERT INTO t (docs) VALUES ('{"a": 0, "b": 0}');
+INSERT INTO t (docs) SELECT jsonb_build_object('b', n) FROM generate_series(100, 10000) as n;
+CREATE INDEX t_idx_a ON t ((docs->>'a'));
+CREATE INDEX t_idx_b ON t ((docs->>'b')) WHERE (docs->>'b')::numeric > 9;
+
+-- We're using BTREE indexes and for this test we want to make sure that they remain
+-- in sync with changes to our relation. Force the choice of index scans below so
+-- that we know we're checking the index's understanding of what values should be
+-- in the index or not.
+SET SESSION enable_seqscan = OFF;
+SET SESSION enable_bitmapscan = OFF;
+
+-- Leave 'a' unchanged but modify 'b' to a value outside of the index predicate.
+-- This should be a HOT update because neither index is changed.
+UPDATE t SET docs = jsonb_build_object('a', 0, 'b', 1) WHERE (docs->>'a')::numeric = 0;
+SELECT * FROM check_hot_updates(1);
+-- Let's check to make sure that the index does not contain a value for 'b'
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+
+-- Leave 'a' unchanged but modify 'b' to a value within the index predicate.
+-- This represents a change for field 'b' from unindexed to indexed and so
+-- this should not take the HOT path.
+UPDATE t SET docs = jsonb_build_object('a', 0, 'b', 10) WHERE (docs->>'a')::numeric = 0;
+SELECT * FROM check_hot_updates(1);
+-- Let's check to make sure that the index contains the new value of 'b'
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+
+-- This update modifies the value of 'a', an indexed field, so it also cannot
+-- be a HOT update.
+UPDATE t SET docs = jsonb_build_object('a', 1, 'b', 10) WHERE (docs->>'b')::numeric = 10;
+SELECT * FROM check_hot_updates(1);
+
+-- This update changes both 'a' and 'b' to new values that require index updates,
+-- this cannot use the HOT path.
+UPDATE t SET docs = jsonb_build_object('a', 2, 'b', 12) WHERE (docs->>'b')::numeric = 10;
+SELECT * FROM check_hot_updates(1);
+-- Let's check to make sure that the index contains the new value of 'b'
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+
+-- This update changes 'b' to a value outside its predicate requiring that
+-- we remove it from the index. That's a transition that can't be done
+-- during a HOT update.
+UPDATE t SET docs = jsonb_build_object('a', 2, 'b', 1) WHERE (docs->>'b')::numeric = 12;
+SELECT * FROM check_hot_updates(1);
+-- Let's check to make sure that the index no longer contains the value of 'b'
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+SELECT * FROM t WHERE (docs->>'b')::numeric > 9 AND (docs->>'b')::numeric < 100;
+
+DROP TABLE t;
+SET SESSION enable_seqscan = ON;
+SET SESSION enable_bitmapscan = ON;
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- Tests to ensure that HOT updates are not performed when multiple indexed
+-- attributes are updated.
+CREATE TABLE t(a INT, b INT) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_idx_a ON t(a);
+CREATE INDEX t_idx_b ON t(abs(b));
+INSERT INTO t VALUES (1, -1);
+
+-- Both are updated, the second is an expression index with an unchanged
+-- index value. The change to the index on a should prevent HOT updates.
+UPDATE t SET a = 2, b = 1 WHERE a = 1;
+SELECT * FROM check_hot_updates(0);
+
+DROP TABLE t;
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- Tests to check the expression_checks reloption behavior.
+--
+CREATE TABLE t(a INT, b INT) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_idx_a ON t(abs(a)) WHERE abs(a) > 10;
+CREATE INDEX t_idx_b ON t(abs(b));
+INSERT INTO t VALUES (-1, -1), (-2, -2), (-3, -3), (-4, -4);
+
+SET SESSION enable_seqscan = OFF;
+SET SESSION enable_bitmapscan = OFF;
+
+-- Disable expression checks on indexes and partial index predicates.
+ALTER TABLE t SET (expression_checks = false);
+
+-- Before and after values of a are outside the predicate of the index and
+-- the indexed value of b hasn't changed however we've disabled expression
+-- checks so this should not be a HOT update.
+-- (-1, -1) -> (-5, -1)
+UPDATE t SET a = -5, b = -1 WHERE a = -1;
+SELECT * FROM check_hot_updates(0);
+
+-- Enable expression checks on indexes, but not on predicates yet.
+ALTER TABLE t SET (expression_checks = true);
+
+-- The indexed value of b hasn't changed, this should be a HOT update.
+-- (-5, -1) -> (-5, 1)
+UPDATE t SET b = 1 WHERE a = -5;
+SELECT * FROM check_hot_updates(1);
+
+-- Now that we're not checking the predicate of the partial index, this
+-- update of a from -5 to 5 should be HOT because we should ignore the
+-- predicate and check the expression and find it unchanged.
+-- (-5, 1) -> (5, 1)
+UPDATE t SET a = 5 WHERE a = -5;
+SELECT * FROM check_hot_updates(2);
+
+-- This update meets the critera for the partial index and should not
+-- be HOT. Let's make sure of that and check the index as well.
+-- (-4, -4) -> (-11, -4)
+UPDATE t SET a = -11 WHERE a = -4;
+SELECT * FROM check_hot_updates(2);
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE abs(a) > 10;
+SELECT * FROM t WHERE abs(a) > 10;
+
+-- (-11, -4) -> (11, -4)
+UPDATE t SET a = 11 WHERE a = -11;
+SELECT * FROM check_hot_updates(3);
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE abs(a) > 10;
+SELECT * FROM t WHERE abs(a) > 10;
+
+-- (11, -4) -> (-4, -4)
+UPDATE t SET a = -4 WHERE a = 11;
+SELECT * FROM check_hot_updates(3);
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE abs(a) > 10;
+SELECT * FROM t WHERE abs(a) > 10;
+
+-- This update of a from 5 to -1 is HOT despite that attribute
+-- being indexed because the before and after values for the
+-- partial index predicate are outside the index definition.
+-- (5, 1) -> (-1, 1)
+UPDATE t SET a = -1 WHERE a = 5;
+SELECT * FROM check_hot_updates(4);
+
+-- This update of a from -2 to -1 with predicate checks enabled should be
+-- HOT because the before/after values of a are both outside the predicate
+-- of the partial index.
+-- (-1, 1) -> (-2, 1)
+UPDATE t SET a = -2 WHERE a = -1;
+SELECT * FROM check_hot_updates(5);
+
+-- The indexed value for b isn't changing, this should be HOT.
+-- (-2, -2) -> (-2, 2)
+UPDATE t SET b = 2 WHERE b = -2;
+SELECT * FROM check_hot_updates(6);
+EXPLAIN (COSTS OFF) SELECT abs(b) FROM t;
+SELECT abs(b) FROM t;
+
+-- Before and after values for a are outside the predicate of the index,
+-- and because we're checking this should be HOT.
+-- (-2, 1) -> (5, 1)
+-- (-2, -2) -> (5, -2)
+UPDATE t SET a = 5 WHERE a = -2;
+SELECT * FROM check_hot_updates(8);
+
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE abs(a) > 10;
+SELECT * FROM t WHERE abs(a) > 10;
+
+SELECT * FROM t;
+
+DROP TABLE t;
+SET SESSION enable_seqscan = ON;
+SET SESSION enable_bitmapscan = ON;
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- The tests here examines the behavior of HOT updates when the relation
+-- has a JSONB column with an index on the field 'a' and the partial index
+-- expression on a different JSONB field 'b'.
+CREATE TABLE t(docs JSONB) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_docs_idx ON t((docs->'a')) WHERE (docs->'b')::integer = 1;
+INSERT INTO t VALUES ('{"a": 1, "b": 1}');
+
+EXPLAIN (COSTS OFF) SELECT * FROM t;
+SELECT * FROM t;
+
+SET SESSION enable_seqscan = OFF;
+SET SESSION enable_bitmapscan = OFF;
+
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE (docs->'b')::integer = 1;
+SELECT * FROM t WHERE (docs->'b')::integer = 1;
+
+SELECT * FROM check_hot_updates(0);
+
+UPDATE t SET docs='{"a": 1, "b": 0}';
+SELECT * FROM check_hot_updates(0);
+
+SELECT * FROM t WHERE (docs->'b')::integer = 1;
+
+SET SESSION enable_seqscan = ON;
+SET SESSION enable_bitmapscan = ON;
+
+DROP TABLE t;
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- Tests for multi-column indexes
+--
+CREATE TABLE t(id INT, docs JSONB) WITH (autovacuum_enabled = off, fillfactor = 70);
+CREATE INDEX t_docs_idx ON t(id, (docs->'a'));
+INSERT INTO t VALUES (1, '{"a": 1, "b": 1}');
+
+SET SESSION enable_seqscan = OFF;
+SET SESSION enable_bitmapscan = OFF;
+
+EXPLAIN (COSTS OFF) SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer > 0;
+SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer > 0;
+
+SELECT * FROM check_hot_updates(0);
+
+-- Changing the id attribute which is an indexed attribute should
+-- prevent HOT updates.
+UPDATE t SET id = 2;
+SELECT * FROM check_hot_updates(0);
+
+SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer > 0;
+
+-- Changing the docs->'a' field in the indexed attribute 'docs'
+-- should prevent HOT updates.
+UPDATE t SET docs='{"a": -2, "b": 1}';
+SELECT * FROM check_hot_updates(0);
+
+SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer < 0;
+
+-- Leaving the docs->'a' attribute unchanged means that the expression
+-- is unchanged and because the 'id' attribute isn't in the modified
+-- set the indexed tuple is unchanged, this can go HOT.
+UPDATE t SET docs='{"a": -2, "b": 2}';
+SELECT * FROM check_hot_updates(1);
+
+SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer < 0;
+
+-- Here we change the 'id' attribute and the 'docs' attribute setting
+-- the expression docs->'a' to a new value, this cannot be a HOT update.
+UPDATE t SET id = 3, docs='{"a": 3, "b": 3}';
+SELECT * FROM check_hot_updates(1);
+
+SELECT * FROM t WHERE id > 0 AND (docs->'a')::integer > 0;
+
+SET SESSION enable_seqscan = ON;
+SET SESSION enable_bitmapscan = ON;
+
+DROP TABLE t;
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- This table has a single column 'email' and a unique constraint on it that
+-- should preclude HOT updates.
+CREATE TABLE users (
+ user_id serial primary key,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ EXCLUDE USING btree (lower(email) WITH =)
+);
+
+-- Add some data to the table and then update it in ways that should and should
+-- not be HOT updates.
+INSERT INTO users (name, email) VALUES
+('user1', 'user1@example.com'),
+('user2', 'user2@example.com'),
+('taken', 'taken@EXAMPLE.com'),
+('you', 'you@domain.com'),
+('taken', 'taken@domain.com');
+
+-- Should fail because of the unique constraint on the email column.
+UPDATE users SET email = 'user1@example.com' WHERE email = 'user2@example.com';
+SELECT * FROM check_hot_updates(0, 'users');
+
+-- Should succeed because the email column is not being updated and should go HOT.
+UPDATE users SET name = 'foo' WHERE email = 'user1@example.com';
+SELECT * FROM check_hot_updates(1, 'users');
+
+-- Create a partial index on the email column, updates
+CREATE INDEX idx_users_email_no_example ON users (lower(email)) WHERE lower(email) LIKE '%@example.com%';
+
+-- An update that changes the email column but not the indexed portion of it and falls outside the constraint.
+-- Shouldn't be a HOT update because of the exclusion constraint.
+UPDATE users SET email = 'you+2@domain.com' WHERE name = 'you';
+SELECT * FROM check_hot_updates(1, 'users');
+
+-- An update that changes the email column but not the indexed portion of it and falls within the constraint.
+-- Again, should fail constraint and fail to be a HOT update.
+UPDATE users SET email = 'taken@domain.com' WHERE name = 'you';
+SELECT * FROM check_hot_updates(1, 'users');
+
+DROP TABLE users;
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- Another test of constraints spoiling HOT updates, this time with a range.
+CREATE TABLE events (
+ id serial primary key,
+ name VARCHAR(255) NOT NULL,
+ event_time tstzrange,
+ constraint no_screening_time_overlap exclude using gist (
+ event_time WITH &&
+ )
+);
+
+-- Add two non-overlapping events.
+INSERT INTO events (id, event_time, name)
+VALUES
+ (1, '["2023-01-01 19:00:00", "2023-01-01 20:45:00"]', 'event1'),
+ (2, '["2023-01-01 21:00:00", "2023-01-01 21:45:00"]', 'event2');
+
+-- Update the first event to overlap with the second, should fail the constraint and not be HOT.
+UPDATE events SET event_time = '["2023-01-01 20:00:00", "2023-01-01 21:45:00"]' WHERE id = 1;
+SELECT * FROM check_hot_updates(0, 'events');
+
+-- Update the first event to not overlap with the second, again not HOT due to the constraint.
+UPDATE events SET event_time = '["2023-01-01 22:00:00", "2023-01-01 22:45:00"]' WHERE id = 1;
+SELECT * FROM check_hot_updates(0, 'events');
+
+-- Update the first event to not overlap with the second, this time we're HOT because we don't overlap with the constraint.
+UPDATE events SET name = 'new name here' WHERE id = 1;
+SELECT * FROM check_hot_updates(1, 'events');
+
+DROP TABLE events;
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- A test to ensure that only modified summarizing indexes are updated, not
+-- all of them.
+CREATE TABLE ex (id SERIAL primary key, att1 JSONB, att2 text, att3 text, att4 text) WITH (fillfactor = 60);
+CREATE INDEX ex_expr1_idx ON ex USING btree((att1->'data'));
+CREATE INDEX ex_sumr1_idx ON ex USING BRIN(att2);
+CREATE INDEX ex_expr2_idx ON ex USING btree((att1->'a'));
+CREATE INDEX ex_expr3_idx ON ex USING btree((att1->'b'));
+CREATE INDEX ex_expr4_idx ON ex USING btree((att1->'c'));
+CREATE INDEX ex_sumr2_idx ON ex USING BRIN(att3);
+CREATE INDEX ex_sumr3_idx ON ex USING BRIN(att4);
+CREATE INDEX ex_expr5_idx ON ex USING btree((att1->'d'));
+INSERT INTO ex (att1, att2) VALUES ('{"data": []}'::json, 'nothing special');
+
+SELECT * FROM ex;
+
+-- Update att2 and att4 both are BRIN/summarizing indexes, this should be a HOT update and
+-- only update two of the three summarizing indexes.
+UPDATE ex SET att2 = 'special indeed', att4 = 'whatever';
+SELECT * FROM check_hot_updates(1, 'ex');
+SELECT * FROM ex;
+
+-- Update att1 and att2, only one is BRIN/summarizing, this should NOT be a HOT update.
+UPDATE ex SET att1 = att1 || '{"data": "howdy"}', att2 = 'special, so special';
+SELECT * FROM check_hot_updates(1, 'ex');
+SELECT * FROM ex;
+
+-- Update att2, att3, and att4 all are BRIN/summarizing indexes, this should be a HOT update
+-- and yet still update all three summarizing indexes.
+UPDATE ex SET att2 = 'a', att3 = 'b', att4 = 'c';
+SELECT * FROM check_hot_updates(2, 'ex');
+SELECT * FROM ex;
+
+-- Update att1, att2, and att3 all modified values are BRIN/summarizing indexes, this should be a HOT update
+-- and yet still update all three summarizing indexes.
+UPDATE ex SET att1 = '{"data": "howdy"}', att2 = 'd', att3 = 'e';
+SELECT * FROM check_hot_updates(3, 'ex');
+SELECT * FROM ex;
+
+DROP TABLE ex;
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- A test to ensure that summarizing indexes are not updated when they don't
+-- change, but are updated when they do while not prefent HOT updates.
+CREATE TABLE ex (att1 JSONB, att2 text) WITH (fillfactor = 60);
+CREATE INDEX ex_expr1_idx ON ex USING btree((att1->'data'));
+CREATE INDEX ex_sumr1_idx ON ex USING BRIN(att2);
+INSERT INTO ex VALUES ('{"data": []}', 'nothing special');
+
+-- Update the unindexed value of att1, this should be a HOT update and and should
+-- update the summarizing index.
+UPDATE ex SET att1 = att1 || '{"status": "stalemate"}';
+SELECT * FROM check_hot_updates(1, 'ex');
+
+-- Update the indexed value of att2, a summarized value, this is a summarized
+-- only update and should use the HOT path while still triggering an update to
+-- the summarizing BRIN index.
+UPDATE ex SET att2 = 'special indeed';
+SELECT * FROM check_hot_updates(2, 'ex');
+
+-- Update to att1 doesn't change the indexed value while the update to att2 does,
+-- this again is a summarized only update and should use the HOT path as well as
+-- trigger an update to the BRIN index.
+UPDATE ex SET att1 = att1 || '{"status": "checkmate"}', att2 = 'special, so special';
+SELECT * FROM check_hot_updates(3, 'ex');
+
+-- This updates both indexes, the expression index on att1 and the summarizing
+-- index on att2. This should not be a HOT update because there are modified
+-- indexes and only some are summarized, not all. This should force all
+-- indexes to be updated.
+UPDATE ex SET att1 = att1 || '{"data": [1,2,3]}', att2 = 'do you want to play a game?';
+SELECT * FROM check_hot_updates(4, 'ex');
+
+DROP TABLE ex;
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- This test is for a table with a custom type and a custom operators on
+-- the BTREE index. The question is, when comparing values for equality
+-- to determine if there are changes on the index or not... shouldn't we
+-- be using the custom operators?
+
+-- Create a type
+CREATE TYPE my_custom_type AS (val int);
+
+-- Comparison functions (returns boolean)
+CREATE FUNCTION my_custom_lt(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val < b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+
+CREATE FUNCTION my_custom_le(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val <= b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+
+CREATE FUNCTION my_custom_eq(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val = b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+
+CREATE FUNCTION my_custom_ge(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val >= b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+
+CREATE FUNCTION my_custom_gt(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val > b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+
+CREATE FUNCTION my_custom_ne(a my_custom_type, b my_custom_type) RETURNS boolean AS $$
+BEGIN
+ RETURN a.val != b.val;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+
+-- Comparison function (returns -1, 0, 1)
+CREATE FUNCTION my_custom_cmp(a my_custom_type, b my_custom_type) RETURNS int AS $$
+BEGIN
+ IF a.val < b.val THEN
+ RETURN -1;
+ ELSIF a.val > b.val THEN
+ RETURN 1;
+ ELSE
+ RETURN 0;
+ END IF;
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+
+-- Create the operators
+CREATE OPERATOR < (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_lt,
+ COMMUTATOR = >,
+ NEGATOR = >=
+);
+
+CREATE OPERATOR <= (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_le,
+ COMMUTATOR = >=,
+ NEGATOR = >
+);
+
+CREATE OPERATOR = (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_eq,
+ COMMUTATOR = =,
+ NEGATOR = <>
+);
+
+CREATE OPERATOR >= (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_ge,
+ COMMUTATOR = <=,
+ NEGATOR = <
+);
+
+CREATE OPERATOR > (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_gt,
+ COMMUTATOR = <,
+ NEGATOR = <=
+);
+
+CREATE OPERATOR <> (
+ LEFTARG = my_custom_type,
+ RIGHTARG = my_custom_type,
+ PROCEDURE = my_custom_ne,
+ COMMUTATOR = <>,
+ NEGATOR = =
+);
+
+-- Create the operator class (including the support function)
+CREATE OPERATOR CLASS my_custom_ops
+ DEFAULT FOR TYPE my_custom_type USING btree AS
+ OPERATOR 1 <,
+ OPERATOR 2 <=,
+ OPERATOR 3 =,
+ OPERATOR 4 >=,
+ OPERATOR 5 >,
+ FUNCTION 1 my_custom_cmp(my_custom_type, my_custom_type);
+
+-- Create the table
+CREATE TABLE my_table (
+ id int,
+ custom_val my_custom_type
+);
+
+-- Insert some data
+INSERT INTO my_table (id, custom_val) VALUES
+(1, ROW(3)::my_custom_type),
+(2, ROW(1)::my_custom_type),
+(3, ROW(4)::my_custom_type),
+(4, ROW(2)::my_custom_type);
+
+-- Create a function to use when indexing
+CREATE OR REPLACE FUNCTION abs_val(val my_custom_type) RETURNS int AS $$
+BEGIN
+ RETURN abs(val.val);
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+
+-- Create the index
+CREATE INDEX idx_custom_val_abs ON my_table (abs_val(custom_val));
+
+-- Update 1
+UPDATE my_table SET custom_val = ROW(5)::my_custom_type WHERE id = 1;
+SELECT * FROM check_hot_updates(0, 'my_table');
+
+-- Update 2
+UPDATE my_table SET custom_val = ROW(0)::my_custom_type WHERE custom_val < ROW(3)::my_custom_type;
+SELECT * FROM check_hot_updates(0, 'my_table');
+
+-- Update 3
+UPDATE my_table SET custom_val = ROW(6)::my_custom_type WHERE id = 3;
+SELECT * FROM check_hot_updates(0, 'my_table');
+
+-- Update 4
+UPDATE my_table SET id = 5 WHERE id = 1;
+SELECT pg_stat_get_xact_tuples_hot_updated('my_table'::regclass);
+SELECT * FROM check_hot_updates(0, 'my_table');
+
+-- Query using the index
+EXPLAIN (COSTS OFF) SELECT * FROM my_table WHERE abs_val(custom_val) = 6;
+SELECT * FROM my_table WHERE abs_val(custom_val) = 6;
+
+-- Clean up
+DROP TABLE my_table CASCADE;
+DROP OPERATOR CLASS my_custom_ops USING btree CASCADE;
+DROP OPERATOR < (my_custom_type, my_custom_type);
+DROP OPERATOR <= (my_custom_type, my_custom_type);
+DROP OPERATOR = (my_custom_type, my_custom_type);
+DROP OPERATOR >= (my_custom_type, my_custom_type);
+DROP OPERATOR > (my_custom_type, my_custom_type);
+DROP OPERATOR <> (my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_lt(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_le(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_eq(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_ge(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_gt(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_ne(my_custom_type, my_custom_type);
+DROP FUNCTION my_custom_cmp(my_custom_type, my_custom_type);
+DROP FUNCTION abs_val(my_custom_type);
+DROP TYPE my_custom_type CASCADE;
+
+DROP FUNCTION check_hot_updates(int, text, text);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 018b5919cf66..e1a586b72e42 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2988,6 +2988,7 @@ TSVectorStat
TState
TStatus
TStoreState
+UpdateContext
TU_UpdateIndexes
TXNEntryFile
TYPCATEGORY
@@ -3175,7 +3176,6 @@ UniqueState
UnlistenStmt
UnresolvedTup
UnresolvedTupData
-UpdateContext
UpdateStmt
UpgradeTask
UpgradeTaskProcessCB