diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml index 1fd21e12bd..ba1c5d6392 100644 --- a/doc/src/sgml/ref/create_index.sgml +++ b/doc/src/sgml/ref/create_index.sgml @@ -309,8 +309,41 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] The optional WITH clause specifies storage parameters for the index. Each index method has its own set of allowed - storage parameters. The B-tree, hash, GiST and SP-GiST index methods all - accept this parameter: + storage parameters. All indexes accept the following parameter: + + + + + recheck_on_update + + + Specifies whether to recheck a functional index value to see whether + we can use a HOT update or not. The default value is on for functional + indexes with an total expression cost less than 1000, otherwise off. + You might decide to turn this off if you knew that a function used in + an index is unlikely to return the same value when one of the input + columns is updated and so the recheck is not worth the additional cost + of executing the function. + + + + Functional indexes are used frequently for the case where the function + returns a subset of the argument. Examples of this would be accessing + part of a string with SUBSTR() or accessing a single + field in a JSON document using an expression such as + (bookinfo->>'isbn'). In this example, the JSON + document might be updated frequently, yet it is uncommon for the ISBN + field for a book to change so we would keep the parameter set to on + for that index. A more frequently changing field might have an index + with this parameter turned off, while very frequently changing fields + might be better to avoid indexing at all under high load. + + + + + + + The B-tree, hash, GiST and SP-GiST index methods all accept this parameter: diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index 46276ceff1..35c09987ad 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -129,6 +129,15 @@ static relopt_bool boolRelOpts[] = }, true }, + { + { + "recheck_on_update", + "Recheck functional index expression for changed value after update", + RELOPT_KIND_INDEX, + ShareUpdateExclusiveLock /* since only applies to later UPDATEs */ + }, + true + }, { { "security_barrier", @@ -1310,7 +1319,7 @@ fillRelOptions(void *rdopts, Size basesize, break; } } - if (validate && !found) + if (validate && !found && options[i].gen->kinds != RELOPT_KIND_INDEX) elog(ERROR, "reloption \"%s\" not found in parse table", options[i].gen->name); } @@ -1466,6 +1475,40 @@ index_reloptions(amoptions_function amoptions, Datum reloptions, bool validate) return amoptions(reloptions, validate); } +/* + * Parse generic options for all indexes. + * + * reloptions options as text[] datum + * validate error flag + */ +bytea * +index_generic_reloptions(Datum reloptions, bool validate) +{ + int numoptions; + GenericIndexOpts *idxopts; + relopt_value *options; + static const relopt_parse_elt tab[] = { + {"recheck_on_update", RELOPT_TYPE_BOOL, offsetof(GenericIndexOpts, recheck_on_update)} + }; + + options = parseRelOptions(reloptions, validate, + RELOPT_KIND_INDEX, + &numoptions); + + /* if none set, we're done */ + if (numoptions == 0) + return NULL; + + idxopts = allocateReloptStruct(sizeof(GenericIndexOpts), options, numoptions); + + fillRelOptions((void *)idxopts, sizeof(GenericIndexOpts), options, numoptions, + validate, tab, lengthof(tab)); + + pfree(options); + + return (bytea*) idxopts; +} + /* * Option parser for attribute reloptions */ diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index c08ab14c02..d7279248e7 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -56,6 +56,7 @@ #include "access/xlogutils.h" #include "catalog/catalog.h" #include "catalog/namespace.h" +#include "catalog/index.h" #include "miscadmin.h" #include "pgstat.h" #include "port/atomics.h" @@ -74,7 +75,9 @@ #include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/tqual.h" - +#include "utils/memutils.h" +#include "nodes/execnodes.h" +#include "executor/executor.h" /* GUC variable */ bool synchronize_seqscans = true; @@ -126,6 +129,7 @@ static bool ConditionalMultiXactIdWait(MultiXactId multi, MultiXactStatus status static XLogRecPtr log_heap_new_cid(Relation relation, HeapTuple tup); static HeapTuple ExtractReplicaIdentity(Relation rel, HeapTuple tup, bool key_modified, bool *copy); +static bool ProjIndexIsUnchanged(Relation relation, HeapTuple oldtup, HeapTuple newtup); /* @@ -3508,6 +3512,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, HTSU_Result result; TransactionId xid = GetCurrentTransactionId(); Bitmapset *hot_attrs; + Bitmapset *proj_idx_attrs; Bitmapset *key_attrs; Bitmapset *id_attrs; Bitmapset *interesting_attrs; @@ -3571,12 +3576,11 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, * Note that we get copies of each bitmap, so we need not worry about * relcache flush happening midway through. */ - hot_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_ALL); + hot_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_HOT); + proj_idx_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_PROJ); key_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_KEY); id_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_IDENTITY_KEY); - - block = ItemPointerGetBlockNumber(otid); buffer = ReadBuffer(relation, block); page = BufferGetPage(buffer); @@ -3596,6 +3600,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, if (!PageIsFull(page)) { interesting_attrs = bms_add_members(interesting_attrs, hot_attrs); + interesting_attrs = bms_add_members(interesting_attrs, proj_idx_attrs); hot_attrs_checked = true; } interesting_attrs = bms_add_members(interesting_attrs, key_attrs); @@ -3894,6 +3899,7 @@ l2: if (vmbuffer != InvalidBuffer) ReleaseBuffer(vmbuffer); bms_free(hot_attrs); + bms_free(proj_idx_attrs); bms_free(key_attrs); bms_free(id_attrs); bms_free(modified_attrs); @@ -4201,11 +4207,18 @@ l2: /* * 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 the page was already full, we may have skipped checking - * for index columns. If so, HOT update is possible. + * changed, or if we have projection functional indexes, check whether + * the old and the new values are the same. If the page was already + * full, we may have skipped checking for index columns. If so, HOT + * update is possible. */ - if (hot_attrs_checked && !bms_overlap(modified_attrs, hot_attrs)) + if (hot_attrs_checked + && !bms_overlap(modified_attrs, hot_attrs) + && (!bms_overlap(modified_attrs, proj_idx_attrs) + || ProjIndexIsUnchanged(relation, &oldtup, newtup))) + { use_hot_update = true; + } } else { @@ -4367,6 +4380,7 @@ l2: heap_freetuple(old_key_tuple); bms_free(hot_attrs); + bms_free(proj_idx_attrs); bms_free(key_attrs); bms_free(id_attrs); bms_free(modified_attrs); @@ -4453,6 +4467,83 @@ heap_tuple_attr_equals(TupleDesc tupdesc, int attrnum, } } +/* + * Check whether the value is unchanged after update of a projection + * functional index. Compare the new and old values of the indexed + * expression to see if we are able to use a HOT update or not. + */ +static bool ProjIndexIsUnchanged(Relation relation, HeapTuple oldtup, HeapTuple newtup) +{ + ListCell *l; + List *indexoidlist = RelationGetIndexList(relation); + EState *estate = CreateExecutorState(); + ExprContext *econtext = GetPerTupleExprContext(estate); + TupleTableSlot *slot = MakeSingleTupleTableSlot(RelationGetDescr(relation)); + bool equals = true; + Datum old_values[INDEX_MAX_KEYS]; + bool old_isnull[INDEX_MAX_KEYS]; + Datum new_values[INDEX_MAX_KEYS]; + bool new_isnull[INDEX_MAX_KEYS]; + int indexno = 0; + econtext->ecxt_scantuple = slot; + + foreach(l, indexoidlist) + { + if (bms_is_member(indexno, relation->rd_projidx)) + { + Oid indexOid = lfirst_oid(l); + Relation indexDesc = index_open(indexOid, AccessShareLock); + IndexInfo *indexInfo = BuildIndexInfo(indexDesc); + int i; + + ResetExprContext(econtext); + ExecStoreTuple(oldtup, slot, InvalidBuffer, false); + FormIndexDatum(indexInfo, + slot, + estate, + old_values, + old_isnull); + + ExecStoreTuple(newtup, slot, InvalidBuffer, false); + FormIndexDatum(indexInfo, + slot, + estate, + new_values, + new_isnull); + + for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++) + { + if (old_isnull[i] != new_isnull[i]) + { + equals = false; + break; + } + else if (!old_isnull[i]) + { + Form_pg_attribute att = TupleDescAttr(RelationGetDescr(indexDesc), i); + if (!datumIsEqual(old_values[i], new_values[i], att->attbyval, att->attlen)) + { + equals = false; + break; + } + } + } + index_close(indexDesc, AccessShareLock); + + if (!equals) + { + break; + } + } + indexno += 1; + } + ExecDropSingleTupleTableSlot(slot); + FreeExecutorState(estate); + + return equals; +} + + /* * Check which columns are being updated. * diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index bfac37f9d1..dee1a8ac78 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -26,6 +26,7 @@ #include "access/amapi.h" #include "access/multixact.h" #include "access/relscan.h" +#include "access/reloptions.h" #include "access/sysattr.h" #include "access/transam.h" #include "access/visibilitymap.h" @@ -3863,7 +3864,7 @@ reindex_relation(Oid relid, int flags, int options) /* Ensure rd_indexattr is valid; see comments for RelationSetIndexList */ if (is_pg_class) - (void) RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_ALL); + (void) RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_HOT); PG_TRY(); { diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 6ab4db26bd..4651302440 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -69,8 +69,10 @@ #include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "optimizer/clauses.h" +#include "optimizer/cost.h" #include "optimizer/prep.h" #include "optimizer/var.h" +#include "pgstat.h" #include "rewrite/rewriteDefine.h" #include "rewrite/rowsecurity.h" #include "storage/lmgr.h" @@ -2314,9 +2316,11 @@ RelationDestroyRelation(Relation relation, bool remember_tupdesc) list_free_deep(relation->rd_fkeylist); list_free(relation->rd_indexlist); bms_free(relation->rd_indexattr); + bms_free(relation->rd_projindexattr); bms_free(relation->rd_keyattr); bms_free(relation->rd_pkattr); bms_free(relation->rd_idattr); + bms_free(relation->rd_projidx); if (relation->rd_pubactions) pfree(relation->rd_pubactions); if (relation->rd_options) @@ -4799,6 +4803,73 @@ RelationGetIndexPredicate(Relation relation) return result; } +#define HEURISTIC_MAX_HOT_RECHECK_EXPR_COST 1000 + +/* + * Check if functional index is projection: index expression returns some subset + * of its argument values. During HOT update check we handle projection indexes + * differently: instead of checking if any of attributes used in indexed + * expression were updated, we calculate and compare values of index expression + * for old and new tuple values. + * + * Decision made by this function is based on two sources: + * 1. Calculated cost of index expression: if greater than some heuristic limit + then extra comparison of index expression values is expected to be too + expensive, so we don't attempt it by default. + * 2. "recheck_on_update" index option explicitly set by user, which overrides 1) + */ +static bool IsProjectionFunctionalIndex(Relation index, IndexInfo* ii) +{ + bool is_projection = false; + + if (ii->ii_Expressions) + { + HeapTuple tuple; + Datum reloptions; + bool isnull; + QualCost index_expr_cost; + + /* by default functional index is considered as non-injective */ + is_projection = true; + + cost_qual_eval(&index_expr_cost, ii->ii_Expressions, NULL); + + /* + * If index expression is too expensive, then disable projection + * optimization, because extra evaluation of index expression is + * expected to be more expensive than index update. Currently the + * projection optimization has to calculate index expression twice + * when the value of index expression has not changed and three times + * when values differ because the expression is recalculated when + * inserting a new index entry for the changed value. + */ + if ((index_expr_cost.startup + index_expr_cost.per_tuple) > + HEURISTIC_MAX_HOT_RECHECK_EXPR_COST) + is_projection = false; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(RelationGetRelid(index))); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", RelationGetRelid(index)); + + reloptions = SysCacheGetAttr(RELOID, tuple, + Anum_pg_class_reloptions, &isnull); + if (!isnull) + { + GenericIndexOpts *idxopts; + + idxopts = (GenericIndexOpts *) index_generic_reloptions(reloptions, false); + + if (idxopts != NULL) + { + is_projection = idxopts->recheck_on_update; + pfree(idxopts); + } + } + ReleaseSysCache(tuple); + } + return is_projection; +} + /* * RelationGetIndexAttrBitmap -- get a bitmap of index attribute numbers * @@ -4826,24 +4897,29 @@ RelationGetIndexPredicate(Relation relation) Bitmapset * RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind) { - Bitmapset *indexattrs; /* indexed columns */ + Bitmapset *indexattrs; /* columns used in non-projection indexes */ + Bitmapset *projindexattrs; /* columns used in projection indexes */ Bitmapset *uindexattrs; /* columns in unique indexes */ Bitmapset *pkindexattrs; /* columns in the primary index */ Bitmapset *idindexattrs; /* columns in the replica identity */ + Bitmapset *projindexes; /* projection indexes */ List *indexoidlist; List *newindexoidlist; Oid relpkindex; Oid relreplindex; ListCell *l; MemoryContext oldcxt; + int indexno; /* Quick exit if we already computed the result. */ if (relation->rd_indexattr != NULL) { switch (attrKind) { - case INDEX_ATTR_BITMAP_ALL: + case INDEX_ATTR_BITMAP_HOT: return bms_copy(relation->rd_indexattr); + case INDEX_ATTR_BITMAP_PROJ: + return bms_copy(relation->rd_projindexattr); case INDEX_ATTR_BITMAP_KEY: return bms_copy(relation->rd_keyattr); case INDEX_ATTR_BITMAP_PRIMARY_KEY: @@ -4890,9 +4966,12 @@ restart: * won't be returned at all by RelationGetIndexList. */ indexattrs = NULL; + projindexattrs = NULL; uindexattrs = NULL; pkindexattrs = NULL; idindexattrs = NULL; + projindexes = NULL; + indexno = 0; foreach(l, indexoidlist) { Oid indexOid = lfirst_oid(l); @@ -4943,13 +5022,22 @@ restart: } } - /* Collect all attributes used in expressions, too */ - pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); - + /* Collect attributes used in expressions, too */ + if (IsProjectionFunctionalIndex(indexDesc, indexInfo)) + { + projindexes = bms_add_member(projindexes, indexno); + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &projindexattrs); + } + else + { + /* Collect all attributes used in expressions, too */ + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + } /* Collect all attributes in the index predicate, too */ pull_varattnos((Node *) indexInfo->ii_Predicate, 1, &indexattrs); index_close(indexDesc, AccessShareLock); + indexno += 1; } /* @@ -4976,6 +5064,8 @@ restart: bms_free(pkindexattrs); bms_free(idindexattrs); bms_free(indexattrs); + bms_free(projindexattrs); + bms_free(projindexes); goto restart; } @@ -4983,12 +5073,16 @@ restart: /* Don't leak the old values of these bitmaps, if any */ bms_free(relation->rd_indexattr); relation->rd_indexattr = NULL; + bms_free(relation->rd_projindexattr); + relation->rd_projindexattr = NULL; bms_free(relation->rd_keyattr); relation->rd_keyattr = NULL; bms_free(relation->rd_pkattr); relation->rd_pkattr = NULL; bms_free(relation->rd_idattr); relation->rd_idattr = NULL; + bms_free(relation->rd_projidx); + relation->rd_projidx = NULL; /* * Now save copies of the bitmaps in the relcache entry. We intentionally @@ -5002,13 +5096,17 @@ restart: relation->rd_pkattr = bms_copy(pkindexattrs); relation->rd_idattr = bms_copy(idindexattrs); relation->rd_indexattr = bms_copy(indexattrs); + relation->rd_projindexattr = bms_copy(projindexattrs); + relation->rd_projidx = bms_copy(projindexes); MemoryContextSwitchTo(oldcxt); /* We return our original working copy for caller to play with */ switch (attrKind) { - case INDEX_ATTR_BITMAP_ALL: + case INDEX_ATTR_BITMAP_HOT: return indexattrs; + case INDEX_ATTR_BITMAP_PROJ: + return projindexattrs; case INDEX_ATTR_BITMAP_KEY: return uindexattrs; case INDEX_ATTR_BITMAP_PRIMARY_KEY: @@ -5632,9 +5730,11 @@ load_relcache_init_file(bool shared) rel->rd_pkindex = InvalidOid; rel->rd_replidindex = InvalidOid; rel->rd_indexattr = NULL; + rel->rd_projindexattr = NULL; rel->rd_keyattr = NULL; rel->rd_pkattr = NULL; rel->rd_idattr = NULL; + rel->rd_projidx = NULL; rel->rd_pubactions = NULL; rel->rd_statvalid = false; rel->rd_statlist = NIL; diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 08d8ef09a4..6926ca132e 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -1855,13 +1855,13 @@ psql_completion(const char *text, int start, int end) COMPLETE_WITH_CONST("("); /* ALTER INDEX SET|RESET ( */ else if (Matches5("ALTER", "INDEX", MatchAny, "RESET", "(")) - COMPLETE_WITH_LIST6("fillfactor", + COMPLETE_WITH_LIST7("fillfactor", "recheck_on_update", "fastupdate", "gin_pending_list_limit", /* GIN */ "buffering", /* GiST */ "pages_per_range", "autosummarize" /* BRIN */ ); else if (Matches5("ALTER", "INDEX", MatchAny, "SET", "(")) - COMPLETE_WITH_LIST6("fillfactor =", + COMPLETE_WITH_LIST7("fillfactor =", "recheck_on_update =", "fastupdate =", "gin_pending_list_limit =", /* GIN */ "buffering =", /* GiST */ "pages_per_range =", "autosummarize =" /* BRIN */ diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h index b32c1e9efe..ef09611e0d 100644 --- a/src/include/access/reloptions.h +++ b/src/include/access/reloptions.h @@ -51,6 +51,7 @@ typedef enum relopt_kind RELOPT_KIND_PARTITIONED = (1 << 11), /* if you add a new kind, make sure you update "last_default" too */ RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_PARTITIONED, + RELOPT_KIND_INDEX = RELOPT_KIND_BTREE|RELOPT_KIND_HASH|RELOPT_KIND_GIN|RELOPT_KIND_SPGIST, /* some compilers treat enums as signed ints, so we can't use 1 << 31 */ RELOPT_KIND_MAX = (1 << 30) } relopt_kind; @@ -276,6 +277,7 @@ extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate); extern bytea *view_reloptions(Datum reloptions, bool validate); extern bytea *index_reloptions(amoptions_function amoptions, Datum reloptions, bool validate); +extern bytea *index_generic_reloptions(Datum reloptions, bool validate); extern bytea *attribute_reloptions(Datum reloptions, bool validate); extern bytea *tablespace_reloptions(Datum reloptions, bool validate); extern LOCKMODE AlterTableGetRelOptionsLockLevel(List *defList); diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index aa8add544a..c26c395b0b 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -141,10 +141,12 @@ typedef struct RelationData List *rd_statlist; /* list of OIDs of extended stats */ /* data managed by RelationGetIndexAttrBitmap: */ - Bitmapset *rd_indexattr; /* identifies columns used in indexes */ + Bitmapset *rd_indexattr; /* columns used in non-projection indexes */ + Bitmapset *rd_projindexattr; /* columns used in projection indexes */ Bitmapset *rd_keyattr; /* cols that can be ref'd by foreign keys */ Bitmapset *rd_pkattr; /* cols included in primary key */ Bitmapset *rd_idattr; /* included in replica identity index */ + Bitmapset *rd_projidx; /* Oids of projection indexes */ PublicationActions *rd_pubactions; /* publication actions */ @@ -245,6 +247,14 @@ typedef struct ForeignKeyCacheInfo Oid conpfeqop[INDEX_MAX_KEYS]; /* PK = FK operator OIDs */ } ForeignKeyCacheInfo; +/* + * Options common for all all indexes + */ +typedef struct GenericIndexOpts +{ + int32 vl_len_; + bool recheck_on_update; +} GenericIndexOpts; /* * StdRdOptions diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h index 8a546aba28..dbbf41b0c1 100644 --- a/src/include/utils/relcache.h +++ b/src/include/utils/relcache.h @@ -53,7 +53,8 @@ extern List *RelationGetIndexPredicate(Relation relation); typedef enum IndexAttrBitmapKind { - INDEX_ATTR_BITMAP_ALL, + INDEX_ATTR_BITMAP_HOT, + INDEX_ATTR_BITMAP_PROJ, INDEX_ATTR_BITMAP_KEY, INDEX_ATTR_BITMAP_PRIMARY_KEY, INDEX_ATTR_BITMAP_IDENTITY_KEY diff --git a/src/test/regress/expected/func_index.out b/src/test/regress/expected/func_index.out new file mode 100644 index 0000000000..e616ea2e55 --- /dev/null +++ b/src/test/regress/expected/func_index.out @@ -0,0 +1,61 @@ +create table keyvalue(id integer primary key, info jsonb); +create index nameindex on keyvalue((info->>'name')) with (recheck_on_update=false); +insert into keyvalue values (1, '{"name": "john", "data": "some data"}'); +update keyvalue set info='{"name": "john", "data": "some other data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); + pg_stat_get_xact_tuples_hot_updated +------------------------------------- + 0 +(1 row) + +drop table keyvalue; +create table keyvalue(id integer primary key, info jsonb); +create index nameindex on keyvalue((info->>'name')) with (recheck_on_update=true); +insert into keyvalue values (1, '{"name": "john", "data": "some data"}'); +update keyvalue set info='{"name": "john", "data": "some other data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); + pg_stat_get_xact_tuples_hot_updated +------------------------------------- + 1 +(1 row) + +update keyvalue set info='{"name": "smith", "data": "some other data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); + pg_stat_get_xact_tuples_hot_updated +------------------------------------- + 1 +(1 row) + +update keyvalue set info='{"name": "smith", "data": "some more data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); + pg_stat_get_xact_tuples_hot_updated +------------------------------------- + 2 +(1 row) + +drop table keyvalue; +create table keyvalue(id integer primary key, info jsonb); +create index nameindex on keyvalue((info->>'name')); +insert into keyvalue values (1, '{"name": "john", "data": "some data"}'); +update keyvalue set info='{"name": "john", "data": "some other data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); + pg_stat_get_xact_tuples_hot_updated +------------------------------------- + 1 +(1 row) + +update keyvalue set info='{"name": "smith", "data": "some other data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); + pg_stat_get_xact_tuples_hot_updated +------------------------------------- + 1 +(1 row) + +update keyvalue set info='{"name": "smith", "data": "some more data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); + pg_stat_get_xact_tuples_hot_updated +------------------------------------- + 2 +(1 row) + +drop table keyvalue; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index d308a05117..fda54d4b67 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -84,7 +84,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi # ---------- # Another group of parallel tests # ---------- -test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password +test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password func_index # ---------- # Another group of parallel tests diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule index 45147e9328..f79e31d028 100644 --- a/src/test/regress/serial_schedule +++ b/src/test/regress/serial_schedule @@ -101,6 +101,7 @@ test: portals test: arrays test: btree_index test: hash_index +test: func_index test: update test: delete test: namespace diff --git a/src/test/regress/sql/func_index.sql b/src/test/regress/sql/func_index.sql new file mode 100644 index 0000000000..b08f7544fc --- /dev/null +++ b/src/test/regress/sql/func_index.sql @@ -0,0 +1,30 @@ +create table keyvalue(id integer primary key, info jsonb); +create index nameindex on keyvalue((info->>'name')) with (recheck_on_update=false); +insert into keyvalue values (1, '{"name": "john", "data": "some data"}'); +update keyvalue set info='{"name": "john", "data": "some other data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); +drop table keyvalue; + +create table keyvalue(id integer primary key, info jsonb); +create index nameindex on keyvalue((info->>'name')) with (recheck_on_update=true); +insert into keyvalue values (1, '{"name": "john", "data": "some data"}'); +update keyvalue set info='{"name": "john", "data": "some other data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); +update keyvalue set info='{"name": "smith", "data": "some other data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); +update keyvalue set info='{"name": "smith", "data": "some more data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); +drop table keyvalue; + +create table keyvalue(id integer primary key, info jsonb); +create index nameindex on keyvalue((info->>'name')); +insert into keyvalue values (1, '{"name": "john", "data": "some data"}'); +update keyvalue set info='{"name": "john", "data": "some other data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); +update keyvalue set info='{"name": "smith", "data": "some other data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); +update keyvalue set info='{"name": "smith", "data": "some more data"}' where id=1; +select pg_stat_get_xact_tuples_hot_updated('keyvalue'::regclass); +drop table keyvalue; + +