Skip to content

Commit 9558f42

Browse files
committed
Enable HOT updates for expression and partial indexes
Currently, PostgreSQL conservatively prevents HOT (Heap-Only Tuple) updates whenever any indexed column changes, even if the indexed portion of that column remains identical. This is overly restrictive for expression indexes (where f(column) might not change even when column changes) and partial indexes (where both old and new tuples might fall outside the predicate). This patch introduces several improvements to enable HOT updates in these cases: Add amcomparedatums callback to IndexAmRoutine. This allows index access methods like GIN to provide custom logic for comparing datums by extracting and comparing index keys rather than comparing the raw datums. GIN indexes now implement gincomparedatums() which extracts keys from both datums and compares the resulting key sets. Add ExecWhichIndexesRequireUpdates() to refine the set of modified attributes and determine precisely which indexes need updating. For partial indexes, this checks whether both old and new tuples satisfy or fail the predicate. For expression indexes, this uses type-specific equality operators to compare computed values. For extraction-based indexes (GIN/RUM), this delegates to amcomparedatums. Modify heap update paths to use the refined modified indexed attrs bitmapset returned by ExecWhichIndexesRequireUpdates(). This allows HOT updates when indexes don't actually require updating, while still preventing HOT updates when they do. Importantly, table access methods can still signal using TU_Update if all, none, or only summarizing indexes should be updated. While the executor layer now owns determining what has changed due to an update and is interested in only updating the minimum number of indexes possible, the table AM can override that while performing table_tuple_update(), which is what heap does. This optimization significantly improves update performance for tables with expression indexes, partial indexes, and GIN/GiST indexes on complex data types like JSONB and tsvector, while maintaining correct index semantics. Minimal additional overhead due to type-specific equality checking should be washed out by the benefits of updating indexes fewer times.
1 parent 90e2782 commit 9558f42

File tree

26 files changed

+1865
-46
lines changed

26 files changed

+1865
-46
lines changed

src/backend/access/gin/ginutil.c

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "storage/indexfsm.h"
2727
#include "utils/builtins.h"
2828
#include "utils/index_selfuncs.h"
29+
#include "utils/memutils.h"
2930
#include "utils/rel.h"
3031
#include "utils/typcache.h"
3132

@@ -78,6 +79,7 @@ ginhandler(PG_FUNCTION_ARGS)
7879
amroutine->amproperty = NULL;
7980
amroutine->ambuildphasename = ginbuildphasename;
8081
amroutine->amvalidate = ginvalidate;
82+
amroutine->amcomparedatums = gincomparedatums;
8183
amroutine->amadjustmembers = ginadjustmembers;
8284
amroutine->ambeginscan = ginbeginscan;
8385
amroutine->amrescan = ginrescan;
@@ -477,13 +479,6 @@ cmpEntries(const void *a, const void *b, void *arg)
477479
return res;
478480
}
479481

480-
481-
/*
482-
* Extract the index key values from an indexable item
483-
*
484-
* The resulting key values are sorted, and any duplicates are removed.
485-
* This avoids generating redundant index entries.
486-
*/
487482
Datum *
488483
ginExtractEntries(GinState *ginstate, OffsetNumber attnum,
489484
Datum value, bool isNull,
@@ -729,3 +724,88 @@ ginbuildphasename(int64 phasenum)
729724
return NULL;
730725
}
731726
}
727+
728+
/*
729+
* gincomparedatums - Compare two datums to determine if they produce identical keys
730+
*
731+
* This function extracts keys from both old_datum and new_datum using the
732+
* opclass's extractValue function, then compares the extracted key arrays.
733+
* Returns true if the key sets are identical (same keys, same counts).
734+
*
735+
* This enables HOT updates for GIN indexes when the indexed portions of a
736+
* value haven't changed, even if the value itself has changed.
737+
*
738+
* Example: JSONB column with GIN index. If an update changes a non-indexed
739+
* key in the JSONB document, the extracted keys are identical and we can
740+
* do a HOT update.
741+
*/
742+
bool
743+
gincomparedatums(Relation index, int attnum,
744+
Datum old_datum, bool old_isnull,
745+
Datum new_datum, bool new_isnull)
746+
{
747+
GinState ginstate;
748+
Datum *old_keys;
749+
Datum *new_keys;
750+
GinNullCategory *old_categories;
751+
GinNullCategory *new_categories;
752+
int32 old_nkeys;
753+
int32 new_nkeys;
754+
MemoryContext tmpcontext;
755+
MemoryContext oldcontext;
756+
bool result = true;
757+
758+
/* Handle NULL cases */
759+
if (old_isnull != new_isnull)
760+
return false;
761+
if (old_isnull)
762+
return true;
763+
764+
/* Create temporary context for extraction work */
765+
tmpcontext = AllocSetContextCreate(CurrentMemoryContext,
766+
"GIN datum comparison",
767+
ALLOCSET_DEFAULT_SIZES);
768+
oldcontext = MemoryContextSwitchTo(tmpcontext);
769+
770+
initGinState(&ginstate, index);
771+
772+
/*
773+
* Extract keys from both datums using existing GIN infrastructure.
774+
*/
775+
old_keys = ginExtractEntries(&ginstate, attnum, old_datum, old_isnull,
776+
&old_nkeys, &old_categories);
777+
new_keys = ginExtractEntries(&ginstate, attnum, new_datum, new_isnull,
778+
&new_nkeys, &new_categories);
779+
780+
/* Different number of keys → definitely different */
781+
if (old_nkeys != new_nkeys)
782+
{
783+
result = false;
784+
goto cleanup;
785+
}
786+
787+
/*
788+
* Compare the sorted key arrays element-by-element. Since both arrays are
789+
* already sorted by ginExtractEntries, we can do a simple O(n)
790+
* comparison.
791+
*/
792+
for (int i = 0; i < old_nkeys; i++)
793+
{
794+
int cmp = ginCompareEntries(&ginstate, attnum,
795+
old_keys[i], old_categories[i],
796+
new_keys[i], new_categories[i]);
797+
798+
if (cmp != 0)
799+
{
800+
result = false;
801+
break;
802+
}
803+
}
804+
805+
cleanup:
806+
/* Clean up */
807+
MemoryContextSwitchTo(oldcontext);
808+
MemoryContextDelete(tmpcontext);
809+
810+
return result;
811+
}

src/backend/access/heap/heapam.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3268,7 +3268,7 @@ heap_update(Relation relation, HeapTupleData *oldtup, HeapTuple newtup,
32683268
TM_FailureData *tmfd, LockTupleMode *lockmode,
32693269
Buffer buffer, Page page, BlockNumber block, ItemId lp,
32703270
Bitmapset *hot_attrs, Bitmapset *sum_attrs, Bitmapset *pk_attrs,
3271-
Bitmapset *rid_attrs, Bitmapset *mix_attrs, Buffer *vmbuffer,
3271+
Bitmapset *rid_attrs, const Bitmapset *mix_attrs, Buffer *vmbuffer,
32723272
bool rep_id_key_required, TU_UpdateIndexes *update_indexes)
32733273
{
32743274
TM_Result result;
@@ -4337,8 +4337,9 @@ HeapDetermineColumnsInfo(Relation relation,
43374337
* This routine may be used to update a tuple when concurrent updates of the
43384338
* target tuple are not expected (for example, because we have a lock on the
43394339
* relation associated with the tuple). Any failure is reported via ereport().
4340+
* Returns the set of modified indexed attributes.
43404341
*/
4341-
void
4342+
Bitmapset *
43424343
simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tuple,
43434344
TU_UpdateIndexes *update_indexes)
43444345
{
@@ -4467,7 +4468,7 @@ simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tup
44674468

44684469
elog(ERROR, "tuple concurrently deleted");
44694470

4470-
return;
4471+
return NULL;
44714472
}
44724473

44734474
/*
@@ -4500,7 +4501,6 @@ simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tup
45004501
bms_free(sum_attrs);
45014502
bms_free(pk_attrs);
45024503
bms_free(rid_attrs);
4503-
bms_free(mix_attrs);
45044504
bms_free(idx_attrs);
45054505

45064506
switch (result)
@@ -4526,6 +4526,8 @@ simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tup
45264526
elog(ERROR, "unrecognized heap_update status: %u", result);
45274527
break;
45284528
}
4529+
4530+
return mix_attrs;
45294531
}
45304532

45314533

src/backend/access/heap/heapam_handler.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
319319
Snapshot crosscheck, bool wait,
320320
TM_FailureData *tmfd,
321321
LockTupleMode *lockmode,
322-
Bitmapset *mix_attrs,
322+
const Bitmapset *mix_attrs,
323323
TU_UpdateIndexes *update_indexes)
324324
{
325325
bool rep_id_key_required = false;

src/backend/access/nbtree/nbtree.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ bthandler(PG_FUNCTION_ARGS)
155155
amroutine->amproperty = btproperty;
156156
amroutine->ambuildphasename = btbuildphasename;
157157
amroutine->amvalidate = btvalidate;
158+
amroutine->amcomparedatums = btcomparedatums;
158159
amroutine->amadjustmembers = btadjustmembers;
159160
amroutine->ambeginscan = btbeginscan;
160161
amroutine->amrescan = btrescan;
@@ -1795,3 +1796,40 @@ bttranslatecmptype(CompareType cmptype, Oid opfamily)
17951796
return InvalidStrategy;
17961797
}
17971798
}
1799+
1800+
/*
1801+
* btcomparedatums - Compare two datums for equality
1802+
*
1803+
* This function is necessary because nbtree requires that keys that are not
1804+
* binary identical not be "equal". Other indexes might allow "A" and "a" to
1805+
* be "equal" when collation is case insensative, but not nbtree. Why? Well,
1806+
* nbtree deduplicates TIDs on page split and the way it accomplish that is by
1807+
* doing a binary comparison of the keys.
1808+
*/
1809+
1810+
bool
1811+
btcomparedatums(Relation index, int attrnum,
1812+
Datum old_datum, bool old_isnull,
1813+
Datum new_datum, bool new_isnull)
1814+
{
1815+
TupleDesc desc = RelationGetDescr(index);
1816+
CompactAttribute *att;
1817+
1818+
/*
1819+
* If one value is NULL and other is not, then they are certainly not
1820+
* equal
1821+
*/
1822+
if (old_isnull != new_isnull)
1823+
return false;
1824+
1825+
/*
1826+
* If both are NULL, they can be considered equal.
1827+
*/
1828+
if (old_isnull)
1829+
return true;
1830+
1831+
/* We do simple binary comparison of the two datums */
1832+
Assert(attrnum <= desc->natts);
1833+
att = TupleDescCompactAttr(desc, attrnum - 1);
1834+
return datumIsEqual(old_datum, new_datum, att->attbyval, att->attlen);
1835+
}

src/backend/access/table/tableam.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ void
336336
simple_table_tuple_update(Relation rel, ItemPointer otid,
337337
TupleTableSlot *slot,
338338
Snapshot snapshot,
339-
Bitmapset *modified_indexed_cols,
339+
const Bitmapset *mix_attrs,
340340
TU_UpdateIndexes *update_indexes)
341341
{
342342
TM_Result result;
@@ -348,7 +348,7 @@ simple_table_tuple_update(Relation rel, ItemPointer otid,
348348
snapshot, InvalidSnapshot,
349349
true /* wait for commit */ ,
350350
&tmfd, &lockmode,
351-
modified_indexed_cols,
351+
mix_attrs,
352352
update_indexes);
353353

354354
switch (result)

src/backend/bootstrap/bootstrap.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,10 +961,18 @@ index_register(Oid heap,
961961
newind->il_info->ii_Expressions =
962962
copyObject(indexInfo->ii_Expressions);
963963
newind->il_info->ii_ExpressionsState = NIL;
964+
/* expression attrs will likely be null, but may as well copy it */
965+
newind->il_info->ii_ExpressionsAttrs =
966+
copyObject(indexInfo->ii_ExpressionsAttrs);
964967
/* predicate will likely be null, but may as well copy it */
965968
newind->il_info->ii_Predicate =
966969
copyObject(indexInfo->ii_Predicate);
967970
newind->il_info->ii_PredicateState = NULL;
971+
/* predicate attrs will likely be null, but may as well copy it */
972+
newind->il_info->ii_PredicateAttrs =
973+
copyObject(indexInfo->ii_PredicateAttrs);
974+
newind->il_info->ii_CheckedPredicate = false;
975+
newind->il_info->ii_PredicateSatisfied = false;
968976
/* no exclusion constraints at bootstrap time, so no need to copy */
969977
Assert(indexInfo->ii_ExclusionOps == NULL);
970978
Assert(indexInfo->ii_ExclusionProcs == NULL);

src/backend/catalog/index.c

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "access/heapam.h"
2828
#include "access/multixact.h"
2929
#include "access/relscan.h"
30+
#include "access/sysattr.h"
3031
#include "access/tableam.h"
3132
#include "access/toast_compression.h"
3233
#include "access/transam.h"
@@ -58,6 +59,7 @@
5859
#include "commands/trigger.h"
5960
#include "executor/executor.h"
6061
#include "miscadmin.h"
62+
#include "nodes/execnodes.h"
6163
#include "nodes/makefuncs.h"
6264
#include "nodes/nodeFuncs.h"
6365
#include "optimizer/optimizer.h"
@@ -2414,6 +2416,61 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
24142416
* ----------------------------------------------------------------
24152417
*/
24162418

2419+
/* ----------------
2420+
* BuildUpdateIndexInfo
2421+
*
2422+
* For expression indexes updates may not change the indexed value allowing
2423+
* for a HOT update. Add information to the IndexInfo to allow for checking
2424+
* if the indexed value has changed.
2425+
*
2426+
* Do this processing here rather than in BuildIndexInfo() to not incur the
2427+
* overhead in the common non-expression cases.
2428+
* ----------------
2429+
*/
2430+
void
2431+
BuildUpdateIndexInfo(ResultRelInfo *resultRelInfo)
2432+
{
2433+
for (int j = 0; j < resultRelInfo->ri_NumIndices; j++)
2434+
{
2435+
int i;
2436+
int indnkeyatts;
2437+
Bitmapset *attrs = NULL;
2438+
IndexInfo *ii = resultRelInfo->ri_IndexRelationInfo[j];
2439+
2440+
/*
2441+
* Expressions are not allowed on non-key attributes, so we can skip
2442+
* them as they should show up in the index HOT-blocking attributes.
2443+
*/
2444+
indnkeyatts = ii->ii_NumIndexKeyAttrs;
2445+
2446+
/* Collect key attributes used by the index */
2447+
for (i = 0; i < indnkeyatts; i++)
2448+
{
2449+
AttrNumber attnum = ii->ii_IndexAttrNumbers[i];
2450+
2451+
if (attnum != 0)
2452+
attrs = bms_add_member(attrs, attnum - FirstLowInvalidHeapAttributeNumber);
2453+
}
2454+
2455+
/* Collect attributes used in the expression */
2456+
if (ii->ii_Expressions)
2457+
pull_varattnos((Node *) ii->ii_Expressions,
2458+
resultRelInfo->ri_RangeTableIndex,
2459+
&ii->ii_ExpressionsAttrs);
2460+
2461+
/* Collect attributes used in the predicate */
2462+
if (ii->ii_Predicate)
2463+
pull_varattnos((Node *) ii->ii_Predicate,
2464+
resultRelInfo->ri_RangeTableIndex,
2465+
&ii->ii_PredicateAttrs);
2466+
2467+
ii->ii_IndexedAttrs = bms_union(attrs, ii->ii_ExpressionsAttrs);
2468+
2469+
/* All indexes should index *something*! */
2470+
Assert(!bms_is_empty(ii->ii_IndexedAttrs));
2471+
}
2472+
}
2473+
24172474
/* ----------------
24182475
* BuildIndexInfo
24192476
* Construct an IndexInfo record for an open index

src/backend/catalog/indexing.c

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ CatalogIndexInsert(CatalogIndexState indstate, HeapTuple heapTuple,
102102
* Get information from the state structure. Fall out if nothing to do.
103103
*/
104104
numIndexes = indstate->ri_NumIndices;
105-
if (numIndexes == 0)
105+
if (numIndexes == 0 || updateIndexes == TU_None)
106106
return;
107107
relationDescs = indstate->ri_IndexRelationDescs;
108108
indexInfoArray = indstate->ri_IndexRelationInfo;
@@ -314,15 +314,18 @@ CatalogTupleUpdate(Relation heapRel, const ItemPointerData *otid, HeapTuple tup)
314314
{
315315
CatalogIndexState indstate;
316316
TU_UpdateIndexes updateIndexes = TU_All;
317+
Bitmapset *updatedAttrs;
317318

318319
CatalogTupleCheckConstraints(heapRel, tup);
319320

320321
indstate = CatalogOpenIndexes(heapRel);
321322

322-
simple_heap_update(heapRel, otid, tup, &updateIndexes);
323-
323+
updatedAttrs = simple_heap_update(heapRel, otid, tup, &updateIndexes);
324+
((ResultRelInfo *) indstate)->ri_ChangedIndexedCols = updatedAttrs;
324325
CatalogIndexInsert(indstate, tup, updateIndexes);
326+
325327
CatalogCloseIndexes(indstate);
328+
bms_free(updatedAttrs);
326329
}
327330

328331
/*
@@ -338,12 +341,15 @@ CatalogTupleUpdateWithInfo(Relation heapRel, const ItemPointerData *otid, HeapTu
338341
CatalogIndexState indstate)
339342
{
340343
TU_UpdateIndexes updateIndexes = TU_All;
344+
Bitmapset *updatedAttrs;
341345

342346
CatalogTupleCheckConstraints(heapRel, tup);
343347

344-
simple_heap_update(heapRel, otid, tup, &updateIndexes);
345-
348+
updatedAttrs = simple_heap_update(heapRel, otid, tup, &updateIndexes);
349+
((ResultRelInfo *) indstate)->ri_ChangedIndexedCols = updatedAttrs;
346350
CatalogIndexInsert(indstate, tup, updateIndexes);
351+
((ResultRelInfo *) indstate)->ri_ChangedIndexedCols = NULL;
352+
bms_free(updatedAttrs);
347353
}
348354

349355
/*

src/backend/catalog/toasting.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,12 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
292292
indexInfo->ii_IndexAttrNumbers[1] = 2;
293293
indexInfo->ii_Expressions = NIL;
294294
indexInfo->ii_ExpressionsState = NIL;
295+
indexInfo->ii_ExpressionsAttrs = NULL;
295296
indexInfo->ii_Predicate = NIL;
296297
indexInfo->ii_PredicateState = NULL;
298+
indexInfo->ii_PredicateAttrs = NULL;
299+
indexInfo->ii_CheckedPredicate = false;
300+
indexInfo->ii_PredicateSatisfied = false;
297301
indexInfo->ii_ExclusionOps = NULL;
298302
indexInfo->ii_ExclusionProcs = NULL;
299303
indexInfo->ii_ExclusionStrats = NULL;

0 commit comments

Comments
 (0)