Allow HOT updates for some expression indexes

If the value of an index expression is unchanged after UPDATE,
allow HOT updates where previously we disallowed them, giving
a significant performance boost in those cases.

Particularly useful for indexes such as JSON->>field where the
JSON value changes but the indexed value does not.

Submitted as "surjective indexes" patch, now enabled by use
of new "recheck_on_update" parameter.

Author: Konstantin Knizhnik
Reviewer: Simon Riggs, with much wordsmithing and some cleanup
This commit is contained in:
Simon Riggs 2018-03-27 19:57:02 +01:00
parent 1944cdc982
commit c203d6cf81
13 changed files with 395 additions and 22 deletions

View File

@ -309,8 +309,41 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
<para>
The optional <literal>WITH</literal> clause specifies <firstterm>storage
parameters</firstterm> 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:
</para>
<variablelist>
<varlistentry>
<term><literal>recheck_on_update</literal></term>
<listitem>
<para>
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.
</para>
<para>
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 <literal>SUBSTR()</literal> or accessing a single
field in a JSON document using an expression such as
<literal>(bookinfo-&gt;&gt;'isbn')</literal>. 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.
</para>
</listitem>
</varlistentry>
</variablelist>
<para>
The B-tree, hash, GiST and SP-GiST index methods all accept this parameter:
</para>
<variablelist>

View File

@ -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
*/

View File

@ -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.
*

View File

@ -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();
{

View File

@ -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;

View File

@ -1855,13 +1855,13 @@ psql_completion(const char *text, int start, int end)
COMPLETE_WITH_CONST("(");
/* ALTER INDEX <foo> 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 */

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -101,6 +101,7 @@ test: portals
test: arrays
test: btree_index
test: hash_index
test: func_index
test: update
test: delete
test: namespace

View File

@ -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;