From d88271135ccdf26272abc155743f2bfe85d7ec31 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Fri, 16 Oct 2026 12:29:42 -0400 Subject: [PATCH v43 6/8] Treat an indexed value stored differently but unchanged as unchanged The executor compares indexed values as stored, as heap_update() always has, so it sees a change when an UPDATE writes back the value a row already has but the old value is stored compressed or out of line and the new one is not compressed yet. Every UPDATE that sends all columns, as ORMs and logical replication do, is like that for large indexed values, and so was never HOT. Two values that are the same once detoasted give every index the same entries, whatever the access method: index entries are a function of the values. So when two values differ as stored and one of them is compressed or stored out of line, compare them with datum_image_eq(), and if they are the same, treat the column as unchanged. This is done once for each changed indexed column, before the narrowing for expression and partial indexes. The narrowing then compares again only the datums of expressions: a table column an index stores gives it the same entries exactly when the column is no longer in the changed set, so its values are neither compared nor fetched a second time. It applies to btree, GiST, SP-GiST and extension access methods alike, so no access method needs amcomparedatums for it; the previous version of this patch gave btree a callback that did exactly this, which Matthias van de Meent pointed out told us nothing about btree. datum_image_eq() fetches and decompresses only when the stored forms differ, one of them is compressed or out of line, and their sizes are the same: an unchanged value that keeps its toast pointer still compares equal without being fetched, and values of different sizes are not fetched either. What remains costs both values' detoasting, where before the update would have been non-HOT and the index insertion would have detoasted the new value. Columns of unique indexes that foreign keys can reference, and of the replica identity, are still compared as stored: the table AM decides its lock strength and what it logs for logical decoding from them. Values that are equal but not the same, such as numeric 1.0 and 1.00 or strings equal under a nondeterministic collation, still count as changed: an index-only scan would return the old one. In builds with assertions, heap_update()'s check of the caller's set of changed columns allows such a column to be left out, if one of its stored values is compressed or out of line and it is in no candidate key or replica identity. --- contrib/amcheck/expected/check_btree.out | 98 +++++++++++++++++ contrib/amcheck/sql/check_btree.sql | 46 ++++++++ doc/src/sgml/indexam.sgml | 4 +- doc/src/sgml/storage.sgml | 7 +- src/backend/access/heap/README.HOT | 7 +- src/backend/access/heap/heapam.c | 35 ++++++- src/backend/executor/nodeModifyTable.c | 122 +++++++++++++++++++--- src/test/regress/expected/hot_updates.out | 6 +- 8 files changed, 302 insertions(+), 23 deletions(-) diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out index 6fc1ee97902..4e02954b6f8 100644 --- a/contrib/amcheck/expected/check_btree.out +++ b/contrib/amcheck/expected/check_btree.out @@ -366,6 +366,104 @@ SELECT bt_index_check('hot_expr_multi_vip', heapallindexed => true); (1 row) DROP TABLE hot_expr_multi; +-- An UPDATE that writes back the value a row has, when the old value is +-- stored compressed or out of line and the new one is not yet, is HOT: the +-- index would store the same bytes. Index-only scans must return the new +-- row's value. +CREATE TABLE hot_bt_image (id int, t text, e text) WITH (fillfactor = 50); +ALTER TABLE hot_bt_image ALTER COLUMN e SET STORAGE EXTERNAL; +CREATE INDEX hot_bt_image_t ON hot_bt_image (t); +CREATE INDEX hot_bt_image_e ON hot_bt_image (e) INCLUDE (id); +INSERT INTO hot_bt_image + SELECT g, repeat('x', 3000) || g, repeat('y', 2500) || g + FROM generate_series(1, 20) g; +SELECT pg_column_compression(t) IS NOT NULL AS t_compressed, + pg_column_size(e) > 2500 AS e_out_of_line + FROM hot_bt_image WHERE id = 1; + t_compressed | e_out_of_line +--------------+--------------- + t | t +(1 row) + +CREATE FUNCTION hot_bt_image_count(OUT updated bigint, OUT hot bigint) +LANGUAGE sql AS $$ + SELECT pg_stat_get_xact_tuples_updated('hot_bt_image'::regclass), + pg_stat_get_xact_tuples_hot_updated('hot_bt_image'::regclass) $$; +BEGIN; +UPDATE hot_bt_image SET t = repeat('x', 3000) || id; +SELECT * FROM hot_bt_image_count(); + updated | hot +---------+----- + 20 | 20 +(1 row) + +UPDATE hot_bt_image SET e = e || ''; +SELECT * FROM hot_bt_image_count(); + updated | hot +---------+----- + 40 | 40 +(1 row) + +UPDATE hot_bt_image SET t = repeat('x', 3000) || (id + 100) WHERE id <= 5; +SELECT * FROM hot_bt_image_count(); + updated | hot +---------+----- + 45 | 40 +(1 row) + +COMMIT; +SET enable_seqscan = off; +SET enable_bitmapscan = off; +CREATE FUNCTION hot_bt_image_plan() RETURNS SETOF text LANGUAGE plpgsql AS +$$ DECLARE l text; BEGIN + FOR l IN EXPLAIN (COSTS OFF) + SELECT right(t, 3) FROM hot_bt_image WHERE t > repeat('x', 3000) + LOOP RETURN NEXT left(l, 40); END LOOP; END $$; +SELECT * FROM hot_bt_image_plan(); + hot_bt_image_plan +------------------------------------------ + Index Only Scan using hot_bt_image_t on + Index Cond: (t > 'xxxxxxxxxxxxxxxxxxxx +(2 rows) + +SELECT right(t, 3) FROM hot_bt_image WHERE t > repeat('x', 3000) + ORDER BY t COLLATE "C" LIMIT 3; + right +------- + x10 + 101 + 102 +(3 rows) + +SELECT count(*) FROM hot_bt_image WHERE t > repeat('x', 3000); + count +------- + 20 +(1 row) + +SELECT count(*) FROM hot_bt_image WHERE e > repeat('y', 2500); + count +------- + 20 +(1 row) + +RESET enable_seqscan; +RESET enable_bitmapscan; +SELECT bt_index_check('hot_bt_image_t', heapallindexed => true); + bt_index_check +---------------- + +(1 row) + +SELECT bt_index_check('hot_bt_image_e', heapallindexed => true); + bt_index_check +---------------- + +(1 row) + +DROP TABLE hot_bt_image; +DROP FUNCTION hot_bt_image_count; +DROP FUNCTION hot_bt_image_plan; -- cleanup DROP TABLE bttest_a; DROP TABLE bttest_b; diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql index 4c54aad5d9f..0e03eeef9e0 100644 --- a/contrib/amcheck/sql/check_btree.sql +++ b/contrib/amcheck/sql/check_btree.sql @@ -233,6 +233,52 @@ SELECT bt_index_check('hot_expr_multi_status', heapallindexed => true); SELECT bt_index_check('hot_expr_multi_vip', heapallindexed => true); DROP TABLE hot_expr_multi; +-- An UPDATE that writes back the value a row has, when the old value is +-- stored compressed or out of line and the new one is not yet, is HOT: the +-- index would store the same bytes. Index-only scans must return the new +-- row's value. +CREATE TABLE hot_bt_image (id int, t text, e text) WITH (fillfactor = 50); +ALTER TABLE hot_bt_image ALTER COLUMN e SET STORAGE EXTERNAL; +CREATE INDEX hot_bt_image_t ON hot_bt_image (t); +CREATE INDEX hot_bt_image_e ON hot_bt_image (e) INCLUDE (id); +INSERT INTO hot_bt_image + SELECT g, repeat('x', 3000) || g, repeat('y', 2500) || g + FROM generate_series(1, 20) g; +SELECT pg_column_compression(t) IS NOT NULL AS t_compressed, + pg_column_size(e) > 2500 AS e_out_of_line + FROM hot_bt_image WHERE id = 1; +CREATE FUNCTION hot_bt_image_count(OUT updated bigint, OUT hot bigint) +LANGUAGE sql AS $$ + SELECT pg_stat_get_xact_tuples_updated('hot_bt_image'::regclass), + pg_stat_get_xact_tuples_hot_updated('hot_bt_image'::regclass) $$; +BEGIN; +UPDATE hot_bt_image SET t = repeat('x', 3000) || id; +SELECT * FROM hot_bt_image_count(); +UPDATE hot_bt_image SET e = e || ''; +SELECT * FROM hot_bt_image_count(); +UPDATE hot_bt_image SET t = repeat('x', 3000) || (id + 100) WHERE id <= 5; +SELECT * FROM hot_bt_image_count(); +COMMIT; +SET enable_seqscan = off; +SET enable_bitmapscan = off; +CREATE FUNCTION hot_bt_image_plan() RETURNS SETOF text LANGUAGE plpgsql AS +$$ DECLARE l text; BEGIN + FOR l IN EXPLAIN (COSTS OFF) + SELECT right(t, 3) FROM hot_bt_image WHERE t > repeat('x', 3000) + LOOP RETURN NEXT left(l, 40); END LOOP; END $$; +SELECT * FROM hot_bt_image_plan(); +SELECT right(t, 3) FROM hot_bt_image WHERE t > repeat('x', 3000) + ORDER BY t COLLATE "C" LIMIT 3; +SELECT count(*) FROM hot_bt_image WHERE t > repeat('x', 3000); +SELECT count(*) FROM hot_bt_image WHERE e > repeat('y', 2500); +RESET enable_seqscan; +RESET enable_bitmapscan; +SELECT bt_index_check('hot_bt_image_t', heapallindexed => true); +SELECT bt_index_check('hot_bt_image_e', heapallindexed => true); +DROP TABLE hot_bt_image; +DROP FUNCTION hot_bt_image_count; +DROP FUNCTION hot_bt_image_plan; + -- cleanup DROP TABLE bttest_a; DROP TABLE bttest_b; diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml index d91192bc011..111494b7b53 100644 --- a/doc/src/sgml/indexam.sgml +++ b/doc/src/sgml/indexam.sgml @@ -937,7 +937,9 @@ amcomparedatums (Relation indexRelation, type of the table column or index expression, which need not be the type the index stores), whose length and by-value property are typlen and typbyval, and - are not equal byte for byte. The function is + are not equal byte for byte, nor once detoasted: the executor itself + treats two values that are the same, but stored differently, as giving + the same entries in any index. The function is called during UPDATE, before the table access method is, for every index that uses a column whose value changed; if every such index answers true for every changed column, the update diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml index 15b5dae1752..66cfe0e7b14 100644 --- a/doc/src/sgml/storage.sgml +++ b/doc/src/sgml/storage.sgml @@ -1126,7 +1126,12 @@ data. Empty in ordinary tables. methods, that depends on the method and on the change (see ). This does not apply to the columns of unique indexes that a foreign key can reference, of the replica - identity index, or of exclusion constraints. + identity index, or of exclusion constraints. Nor is a column changed + whose new value is the same as the old one, only stored differently, as + when the old value is compressed or stored out of line + (see ) and an update writes it back; + except for the columns of those unique and replica identity indexes, + this holds for every kind of index. diff --git a/src/backend/access/heap/README.HOT b/src/backend/access/heap/README.HOT index 5c2dfc27f58..c317840cb3d 100644 --- a/src/backend/access/heap/README.HOT +++ b/src/backend/access/heap/README.HOT @@ -45,8 +45,11 @@ The same holds for a column that indexes also store directly, if every index that does so has an access method that can tell, from the old and new values, that it would store the same entries (amcomparedatums), and the column is in no candidate key, replica identity or exclusion -constraint. The executor works that out before calling the table AM, -and passes on only the indexed columns whose change matters. An index +constraint. And a column of no candidate key or replica identity is not +changed if its value is the same, only stored differently: compressed or +out of line in the old tuple, say, and not yet in the new one. The +executor works that out before calling the table AM, and passes on only +the indexed columns whose change matters. An index that is not yet ready for inserts, or whose build failed, counts as changed whenever a column it references does; see "CREATE INDEX CONCURRENTLY" below. diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index a0e2d904beb..a224e25bad2 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -3317,6 +3317,7 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup, #ifdef USE_ASSERT_CHECKING Bitmapset *check_attrs; Bitmapset *narrowable_attrs; + Bitmapset *key_attrs; #endif TransactionId xmax_new_tuple, xmax_old_tuple; @@ -3360,6 +3361,7 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup, id_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_IDENTITY_KEY); #ifdef USE_ASSERT_CHECKING check_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_INDEXED); + key_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_KEY); narrowable_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_EXPRESSION); { @@ -3428,6 +3430,7 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup, #ifdef USE_ASSERT_CHECKING bms_free(check_attrs); bms_free(narrowable_attrs); + bms_free(key_attrs); #endif /* modified_idx_attrs is owned by the caller, don't free it */ @@ -3777,6 +3780,7 @@ l2: #ifdef USE_ASSERT_CHECKING bms_free(check_attrs); bms_free(narrowable_attrs); + bms_free(key_attrs); #endif /* modified_idx_attrs is owned by the caller, don't free it */ @@ -3810,19 +3814,42 @@ l2: * wrong tuples, is caught. The caller may leave out an attribute that * indexes use only through expressions or as partial indexes, or only in * indexes that can compare entries, having found that no index's entry - * changes (see ExecUpdateModifiedIdxAttrs()), and may report more: - * comparing slots, it cannot always tell that two values will be stored - * alike. + * changes (see ExecUpdateModifiedIdxAttrs()), or one whose value is the + * same, stored compressed or out of line in one of the tuples, unless it + * is in a candidate key or the replica identity. It may also report + * more: comparing slots, it cannot always tell that two values will be + * stored alike. */ { Bitmapset *changed = HeapUpdateModifiedIdxAttrs(relation, check_attrs, &oldtup, newtup); + TupleDesc tupdesc = RelationGetDescr(relation); + int attidx = -1; changed = bms_del_members(changed, narrowable_attrs); - Assert(bms_is_subset(changed, modified_idx_attrs)); + changed = bms_del_members(changed, modified_idx_attrs); + while ((attidx = bms_next_member(changed, attidx)) >= 0) + { + AttrNumber attrnum = attidx + FirstLowInvalidHeapAttributeNumber; + Datum value1, + value2; + bool isnull1, + isnull2; + + Assert(attrnum > 0); + Assert(!bms_is_member(attidx, id_attrs)); + Assert(!bms_is_member(attidx, key_attrs)); + Assert(TupleDescCompactAttr(tupdesc, attrnum - 1)->attlen == -1); + value1 = heap_getattr(&oldtup, attrnum, tupdesc, &isnull1); + value2 = heap_getattr(newtup, attrnum, tupdesc, &isnull2); + Assert(!isnull1 && !isnull2); + Assert(VARATT_IS_EXTENDED(DatumGetPointer(value1)) || + VARATT_IS_EXTENDED(DatumGetPointer(value2))); + } bms_free(changed); bms_free(check_attrs); bms_free(narrowable_attrs); + bms_free(key_attrs); } #endif diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 4054f6da0df..b2f46945637 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -82,6 +82,7 @@ #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/snapmgr.h" +#include "varatt.h" typedef struct MTTargetRelLookup @@ -199,11 +200,94 @@ static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, bool canSetTag); +/* + * Are two varlena values that differ as stored the same value? They can + * only be if one of them is compressed or stored out of line; otherwise the + * stored bytes are the value. datum_image_eq() then compares them + * detoasted, which it skips when their sizes differ. Any index makes the + * same entries for the same value, so for HOT the two are the same. + */ +static bool +VarlenaImageEqual(Datum value1, Datum value2) +{ + varlena *v1 = (varlena *) DatumGetPointer(value1); + varlena *v2 = (varlena *) DatumGetPointer(value2); + + if (!VARATT_IS_EXTERNAL(v1) && !VARATT_IS_COMPRESSED(v1) && + !VARATT_IS_EXTERNAL(v2) && !VARATT_IS_COMPRESSED(v2)) + return false; + return datum_image_eq(value1, value2, false, -1); +} + +/* + * Remove from attrs, the indexed attributes whose values differ as stored + * between old_tts and new_tts, those that are the same value stored + * differently: the old one compressed or out of line, say, and the new one + * not yet, as when an UPDATE writes back the value a row has. No index needs + * a new entry for them. Attributes of candidate keys and the replica + * identity stay, as the table AM decides its lock strength and what it logs + * from the values as stored. + */ +static Bitmapset * +DropRestoredAttrs(Relation relation, Bitmapset *attrs, + TupleTableSlot *old_tts, TupleTableSlot *new_tts, + EState *estate) +{ + TupleDesc tupdesc = RelationGetDescr(relation); + Bitmapset *keepattrs = NULL; + bool fetched = false; + MemoryContext oldcxt; + int attidx = -1; + + oldcxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + while ((attidx = bms_next_member(attrs, attidx)) >= 0) + { + AttrNumber attrnum = attidx + FirstLowInvalidHeapAttributeNumber; + Datum value1, + value2; + bool null1, + null2; + + if (attrnum <= 0 || + TupleDescCompactAttr(tupdesc, attrnum - 1)->attlen != -1) + continue; + value1 = slot_getattr(old_tts, attrnum, &null1); + value2 = slot_getattr(new_tts, attrnum, &null2); + if (null1 || null2 || !VarlenaImageEqual(value1, value2)) + continue; + + if (!fetched) + { + Bitmapset *idattrs; + + MemoryContextSwitchTo(oldcxt); + keepattrs = RelationGetIndexAttrBitmap(relation, + INDEX_ATTR_BITMAP_KEY); + idattrs = RelationGetIndexAttrBitmap(relation, + INDEX_ATTR_BITMAP_IDENTITY_KEY); + keepattrs = bms_add_members(keepattrs, idattrs); + bms_free(idattrs); + MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + fetched = true; + } + if (!bms_is_member(attidx, keepattrs)) + attrs = bms_del_member(attrs, attidx); + } + MemoryContextSwitchTo(oldcxt); + bms_free(keepattrs); + + return attrs; +} + /* * Do the old and new index datums differ? Compare each column as the type * FormIndexDatum() produced it in: the table column's type or the - * expression's, which need not be the type the index stores. Datums that - * differ byte for byte may still give the index the same entries; if its + * expression's, which need not be the type the index stores. A table column + * is not compared again: "changed" says which of the index's columns the + * update changed, both as stored and once detoasted, and the others give the + * same entries. An expression's datums that differ byte for byte may be the + * same value stored differently, which gives any index the same entries. + * Other datums that differ may still give the index the same entries; if its * access method can tell (amcomparedatums), ask it, but not for a column the * index can return in index-only scans, where only the same value will do. * @@ -214,6 +298,7 @@ static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context, */ static bool IndexDatumsDiffer(Relation heapRel, Relation indexRel, IndexInfo *indexInfo, + const Bitmapset *changed, const Datum *old_values, const bool *old_isnull, const Datum *new_values, const bool *new_isnull) { @@ -230,9 +315,12 @@ IndexDatumsDiffer(Relation heapRel, Relation indexRel, IndexInfo *indexInfo, if (attnum != 0) { - Form_pg_attribute att = TupleDescAttr(RelationGetDescr(heapRel), - attnum - 1); + Form_pg_attribute att; + if (!bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, + changed)) + continue; + att = TupleDescAttr(RelationGetDescr(heapRel), attnum - 1); typid = att->atttypid; typlen = att->attlen; typbyval = att->attbyval; @@ -246,8 +334,12 @@ IndexDatumsDiffer(Relation heapRel, Relation indexRel, IndexInfo *indexInfo, if (old_isnull[i] != new_isnull[i]) return true; - if (old_isnull[i] || - datumIsEqual(old_values[i], new_values[i], typbyval, typlen)) + if (old_isnull[i]) + continue; + if (attnum == 0 && + (datumIsEqual(old_values[i], new_values[i], typbyval, typlen) || + (typlen == -1 && + VarlenaImageEqual(old_values[i], new_values[i])))) continue; if (cmp == NULL || index_can_return(indexRel, i + 1) || !cmp(indexRel, indexInfo, i + 1, typid, typlen, typbyval, @@ -259,7 +351,8 @@ IndexDatumsDiffer(Relation heapRel, Relation indexRel, IndexInfo *indexInfo, /* * Would the update from old_tts to new_tts give this index a new entry, or - * take one away? + * take one away? "changed" are the attributes the update changed that the + * index uses, as for IndexDatumsDiffer(). * * The index datums are formed in the per-tuple memory context, which * callers reset for each row. The predicate's ExprState is kept in the @@ -268,8 +361,9 @@ IndexDatumsDiffer(Relation heapRel, Relation indexRel, IndexInfo *indexInfo, */ static bool IndexEntryChanges(ResultRelInfo *resultRelInfo, Relation indexRel, - IndexInfo *indexInfo, TupleTableSlot *old_tts, - TupleTableSlot *new_tts, EState *estate) + IndexInfo *indexInfo, const Bitmapset *changed, + TupleTableSlot *old_tts, TupleTableSlot *new_tts, + EState *estate) { ExprContext *econtext = GetPerTupleExprContext(estate); TupleTableSlot *save_scantuple = econtext->ecxt_scantuple; @@ -314,7 +408,7 @@ IndexEntryChanges(ResultRelInfo *resultRelInfo, Relation indexRel, econtext->ecxt_scantuple = save_scantuple; result = IndexDatumsDiffer(resultRelInfo->ri_RelationDesc, indexRel, - indexInfo, old_values, old_isnull, + indexInfo, changed, old_values, old_isnull, new_values, new_isnull); MemoryContextSwitchTo(oldcxt); return result; @@ -402,7 +496,7 @@ IndexUnaffected(ResultRelInfo *resultRelInfo, int i, Bitmapset *mine, { if (IndexEntryChanges(resultRelInfo, resultRelInfo->ri_IndexRelationDescs[i], - indexInfo, old_tts, new_tts, + indexInfo, mine, old_tts, new_tts, estate)) elog(ERROR, "index \"%s\" proven unaffected by UPDATE, but its entry changed", RelationGetRelationName(resultRelInfo->ri_IndexRelationDescs[i])); @@ -415,7 +509,7 @@ IndexUnaffected(ResultRelInfo *resultRelInfo, int i, Bitmapset *mine, return !IndexEntryChanges(resultRelInfo, resultRelInfo->ri_IndexRelationDescs[i], - indexInfo, old_tts, new_tts, + indexInfo, mine, old_tts, new_tts, estate); } @@ -485,6 +579,10 @@ ExecUpdateModifiedIdxAttrs(ResultRelInfo *resultRelInfo, attrs = table_slot_modified_idx_attrs(relation, old_tts, new_tts); + if (bms_is_empty(attrs)) + return attrs; + + attrs = DropRestoredAttrs(relation, attrs, old_tts, new_tts, estate); if (bms_is_empty(attrs)) return attrs; diff --git a/src/test/regress/expected/hot_updates.out b/src/test/regress/expected/hot_updates.out index 276a845a3e9..fc820671060 100644 --- a/src/test/regress/expected/hot_updates.out +++ b/src/test/regress/expected/hot_updates.out @@ -671,7 +671,7 @@ SELECT * FROM update_case( NOTICE: table "hot_expr_test" does not exist, skipping updated | hot ---------+----- - 10 | 0 + 10 | 10 (1 row) SET enable_seqscan = off; @@ -692,7 +692,7 @@ SELECT * FROM update_case( $$UPDATE hot_expr_test SET t = repeat('x', 3000) || id, x = x + 1$$); updated | hot ---------+----- - 10 | 0 + 10 | 10 (1 row) -- The same through an expression that returns the column's value as is. @@ -703,7 +703,7 @@ SELECT * FROM update_case( $$UPDATE hot_expr_test SET t = repeat('x', 3000) || id, x = x + 1$$); updated | hot ---------+----- - 10 | 0 + 10 | 10 (1 row) -- ... but not for a column of a unique index, which the table's lock -- 2.50.1