From 848b143d6ae1817602aa68710e9bcc033c061da9 Mon Sep 17 00:00:00 2001 From: Shayon Mukherjee Date: Sun, 22 Sep 2024 14:01:45 -0400 Subject: [PATCH v2] Introduce the ability to enable/disable indexes --- doc/src/sgml/catalogs.sgml | 11 + doc/src/sgml/ref/alter_index.sgml | 39 ++++ doc/src/sgml/ref/create_index.sgml | 29 +++ src/backend/bootstrap/bootparse.y | 2 + src/backend/catalog/index.c | 31 ++- src/backend/catalog/toasting.c | 2 +- src/backend/commands/indexcmds.c | 4 + src/backend/commands/tablecmds.c | 94 +++++++- src/backend/optimizer/path/indxpath.c | 12 + src/backend/optimizer/util/plancat.c | 2 + src/backend/parser/gram.y | 48 +++- src/backend/parser/parse_utilcmd.c | 3 + src/backend/utils/adt/ruleutils.c | 4 + src/backend/utils/cache/relcache.c | 1 + src/include/catalog/catversion.h | 2 +- src/include/catalog/index.h | 2 +- src/include/catalog/pg_index.h | 1 + src/include/nodes/parsenodes.h | 3 + src/include/nodes/pathnodes.h | 2 + src/test/regress/expected/create_index.out | 250 +++++++++++++++++++++ src/test/regress/sql/create_index.sql | 140 ++++++++++++ 21 files changed, 671 insertions(+), 11 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index bfb97865e1..61fbf5beb5 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -4590,6 +4590,17 @@ SCRAM-SHA-256$<iteration count>:&l partial index. + + + + indisenabled bool + + + If true, the index is currently enabled and should be used for queries. + If false, the index is disabled and should not be used for queries, + but is still maintained when the table is modified. Default is true. + + diff --git a/doc/src/sgml/ref/alter_index.sgml b/doc/src/sgml/ref/alter_index.sgml index e26efec064..613d63b747 100644 --- a/doc/src/sgml/ref/alter_index.sgml +++ b/doc/src/sgml/ref/alter_index.sgml @@ -31,6 +31,8 @@ ALTER INDEX [ IF EXISTS ] name ALTE SET STATISTICS integer ALTER INDEX ALL IN TABLESPACE name [ OWNED BY role_name [, ... ] ] SET TABLESPACE new_tablespace [ NOWAIT ] +ALTER INDEX [ IF EXISTS ] name ENABLE +ALTER INDEX [ IF EXISTS ] name DISABLE @@ -158,6 +160,29 @@ ALTER INDEX ALL IN TABLESPACE name + + ENABLE + + + Enable the specified index. The index will be used by the query planner + for query optimization. This is the default state for newly created indexes. + + + + + + DISABLE + + + Disable the specified index. A disabled index is not used by the query planner + for query optimization, but it is still maintained when the underlying table + data changes. This can be useful for testing query performance with and without + specific indexes, or for temporarily reducing the overhead of index maintenance + during bulk data loading operations. + + + + @@ -300,6 +325,20 @@ CREATE INDEX coord_idx ON measured (x, y, (z + t)); ALTER INDEX coord_idx ALTER COLUMN 3 SET STATISTICS 1000; + + To enable an index: + +ALTER INDEX idx_name ENABLE; + + + + + To disable an index: + +ALTER INDEX idx_name DISABLE; + + + diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml index 621bc0e253..f1eebfa250 100644 --- a/doc/src/sgml/ref/create_index.sgml +++ b/doc/src/sgml/ref/create_index.sgml @@ -28,6 +28,7 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] storage_parameter [= value] [, ... ] ) ] [ TABLESPACE tablespace_name ] [ WHERE predicate ] + [ DISABLE ] @@ -380,6 +381,19 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] + + DISABLE + + + Creates the index in a disabled state (default enabled). A disabled index is not used by the + query planner for query optimization, but it is still maintained when the + underlying table data changes. This can be useful when you want to create + an index without immediately impacting query performance, allowing you to + enable it later at a more convenient time. The index can be enabled later + using ALTER INDEX ... ENABLE. + + + @@ -701,6 +715,14 @@ Indexes: partitioned index is a metadata only operation. + + When creating an index with the DISABLE option, the index + will be created but not used for query planning. This can be useful for + preparing an index in advance of its use, or for testing purposes. The index + will still be maintained as the table is modified, so it can be enabled + later without needing to be rebuilt. By default all new indexes are enabled. + + @@ -980,6 +1002,13 @@ SELECT * FROM points CREATE INDEX CONCURRENTLY sales_quantity_index ON sales_table (quantity); + + To create an index on the table test_table with the default + name, but have it initially disabled: + +CREATE INDEX ON test_table (col1) DISABLE; + + diff --git a/src/backend/bootstrap/bootparse.y b/src/backend/bootstrap/bootparse.y index 73a7592fb7..6023d58490 100644 --- a/src/backend/bootstrap/bootparse.y +++ b/src/backend/bootstrap/bootparse.y @@ -302,6 +302,7 @@ Boot_DeclareIndexStmt: stmt->concurrent = false; stmt->if_not_exists = false; stmt->reset_default_tblspc = false; + stmt->isenabled = true; /* locks and races need not concern us in bootstrap mode */ relationId = RangeVarGetRelid(stmt->relation, NoLock, @@ -355,6 +356,7 @@ Boot_DeclareUniqueIndexStmt: stmt->concurrent = false; stmt->if_not_exists = false; stmt->reset_default_tblspc = false; + stmt->isenabled = true; /* locks and races need not concern us in bootstrap mode */ relationId = RangeVarGetRelid(stmt->relation, NoLock, diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index b2b3ecb524..1989cb8ce9 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -118,7 +118,8 @@ static void UpdateIndexRelation(Oid indexoid, Oid heapoid, bool isexclusion, bool immediate, bool isvalid, - bool isready); + bool isready, + bool isenabled); static void index_update_stats(Relation rel, bool hasindex, double reltuples); @@ -569,7 +570,8 @@ UpdateIndexRelation(Oid indexoid, bool isexclusion, bool immediate, bool isvalid, - bool isready) + bool isready, + bool isenabled) { int2vector *indkey; oidvector *indcollation; @@ -647,6 +649,7 @@ UpdateIndexRelation(Oid indexoid, values[Anum_pg_index_indisready - 1] = BoolGetDatum(isready); values[Anum_pg_index_indislive - 1] = BoolGetDatum(true); values[Anum_pg_index_indisreplident - 1] = BoolGetDatum(false); + values[Anum_pg_index_indisenabled - 1] = BoolGetDatum(isenabled); values[Anum_pg_index_indkey - 1] = PointerGetDatum(indkey); values[Anum_pg_index_indcollation - 1] = PointerGetDatum(indcollation); values[Anum_pg_index_indclass - 1] = PointerGetDatum(indclass); @@ -712,6 +715,8 @@ UpdateIndexRelation(Oid indexoid, * already exists. * INDEX_CREATE_PARTITIONED: * create a partitioned index (table must be partitioned) + * INDEX_CREATE_DISABLED: +* create the index as disabled if instructed, defaults to being enabled. * constr_flags: flags passed to index_constraint_create * (only if INDEX_CREATE_ADD_CONSTRAINT is set) * allow_system_table_mods: allow table to be a system catalog @@ -757,6 +762,7 @@ index_create(Relation heapRelation, bool invalid = (flags & INDEX_CREATE_INVALID) != 0; bool concurrent = (flags & INDEX_CREATE_CONCURRENT) != 0; bool partitioned = (flags & INDEX_CREATE_PARTITIONED) != 0; + bool isenabled = (flags & INDEX_CREATE_ENABLED) != 0; char relkind; TransactionId relfrozenxid; MultiXactId relminmxid; @@ -1040,13 +1046,15 @@ index_create(Relation heapRelation, * (Or, could define a rule to maintain the predicate) --Nels, Feb '92 * ---------------- */ + UpdateIndexRelation(indexRelationId, heapRelationId, parentIndexRelid, indexInfo, collationIds, opclassIds, coloptions, isprimary, is_exclusion, (constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) == 0, !concurrent && !invalid, - !concurrent); + !concurrent, + isenabled); /* * Register relcache invalidation on the indexes' heap relation, to @@ -1315,6 +1323,8 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, List *indexColNames = NIL; List *indexExprs = NIL; List *indexPreds = NIL; + Form_pg_index indexForm; + bits16 createFlags; indexRelation = index_open(oldIndexId, RowExclusiveLock); @@ -1342,6 +1352,9 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, Anum_pg_index_indoption); indcoloptions = (int2vector *) DatumGetPointer(colOptionDatum); + /* Get the enabled state of the original index */ + indexForm = (Form_pg_index) GETSTRUCT(indexTuple); + /* Fetch reloptions of index if any */ classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(oldIndexId)); if (!HeapTupleIsValid(classTuple)) @@ -1433,6 +1446,16 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, stattargets[i].isnull = isnull; } + /* + * Determine the create flags for the new index. + * We always use SKIP_BUILD and CONCURRENT for concurrent reindexing. + * If the original index was enabled, we also set the ENABLED flag + * to maintain the same state in the new index. + */ + createFlags = INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT; + if (indexForm->indisenabled) + createFlags |= INDEX_CREATE_ENABLED; + /* * Now create the new index. * @@ -1456,7 +1479,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, indcoloptions->values, stattargets, reloptionsDatum, - INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT, + createFlags, 0, true, /* allow table to be a system catalog? */ false, /* is_internal? */ diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c index 738bc46ae8..ce497eed58 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -324,7 +324,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, BTREE_AM_OID, rel->rd_rel->reltablespace, collationIds, opclassIds, NULL, coloptions, NULL, (Datum) 0, - INDEX_CREATE_IS_PRIMARY, 0, true, true, NULL); + INDEX_CREATE_IS_PRIMARY | INDEX_CREATE_ENABLED, 0, true, true, NULL); table_close(toast_rel, NoLock); diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index f99c2d2dee..7cd2041300 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -1191,6 +1191,10 @@ DefineIndex(Oid tableId, flags |= INDEX_CREATE_PARTITIONED; if (stmt->primary) flags |= INDEX_CREATE_IS_PRIMARY; + if (stmt->isenabled) + flags |= INDEX_CREATE_ENABLED; + else + flags &= ~INDEX_CREATE_ENABLED; /* * If the table is partitioned, and recursion was declined but partitions diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2d703aa22e..905a3f58a2 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -664,7 +664,7 @@ static List *GetParentedForeignKeyRefs(Relation partition); static void ATDetachCheckNoForeignKeyRefs(Relation partition); static char GetAttributeCompression(Oid atttypid, const char *compression); static char GetAttributeStorage(Oid atttypid, const char *storagemode); - +static void ATExecEnableDisableIndex(Relation rel, bool enable); /* ---------------------------------------------------------------- * DefineRelation @@ -4546,6 +4546,8 @@ AlterTableGetLockLevel(List *cmds) case AT_SetExpression: case AT_DropExpression: case AT_SetCompression: + case AT_EnableIndex: + case AT_DisableIndex: cmd_lockmode = AccessExclusiveLock; break; @@ -5123,6 +5125,12 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, /* No command-specific prep needed */ pass = AT_PASS_MISC; break; + case AT_EnableIndex: + case AT_DisableIndex: + ATSimplePermissions(cmd->subtype, rel, ATT_INDEX | ATT_PARTITIONED_INDEX); + /* No command-specific prep needed */ + pass = AT_PASS_MISC; + break; default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -5519,6 +5527,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, case AT_DetachPartitionFinalize: address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name); break; + case AT_EnableIndex: + ATExecEnableDisableIndex(rel, true); + break; + case AT_DisableIndex: + ATExecEnableDisableIndex(rel, false); + break; default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -6418,6 +6432,8 @@ alter_table_type_to_string(AlterTableType cmdtype) return "DROP COLUMN"; case AT_AddIndex: case AT_ReAddIndex: + case AT_EnableIndex: + case AT_DisableIndex: return NULL; /* not real grammar */ case AT_AddConstraint: case AT_ReAddConstraint: @@ -20214,3 +20230,79 @@ GetAttributeStorage(Oid atttypid, const char *storagemode) return cstorage; } + +/* + * ATExecEnableDisableIndex function handles the execution of enabling or disabling an index. + * It performs an in-place update to preserve the pg_index row's xmin. + */ +static void +ATExecEnableDisableIndex(Relation rel, bool enable) +{ + Oid indexOid = RelationGetRelid(rel); + Relation pg_index; + HeapTuple indexTuple; + Form_pg_index indexForm; + bool updated = false; + + /* Open pg_index */ + pg_index = table_open(IndexRelationId, RowExclusiveLock); + + /* Fetch the index's pg_index tuple */ + indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexOid)); + if (!HeapTupleIsValid(indexTuple)) + elog(ERROR, "cache lookup failed for index %u", indexOid); + + indexForm = (Form_pg_index) GETSTRUCT(indexTuple); + + /* Check if the index's current state differs from the desired state */ + if (indexForm->indisenabled != enable) + { + HeapTuple copyTuple; + + /* Create a modifiable copy of the tuple */ + copyTuple = heap_copytuple(indexTuple); + indexForm = (Form_pg_index) GETSTRUCT(copyTuple); + + indexForm->indisenabled = enable; + + /* Perform an in-place update */ + heap_inplace_update(pg_index, copyTuple); + + /* Free the copy */ + heap_freetuple(copyTuple); + + updated = true; + + /* Update relcache */ + CacheInvalidateRelcache(rel); + + /* + * Invalidate the relcache for the table, so that after we commit + * all sessions will refresh the table's index state before + * attempting to use this index. + */ + CacheInvalidateRelcache(rel); + + ereport(NOTICE, + (errmsg("index \"%s\" is now %s", + RelationGetRelationName(rel), + enable ? "enabled" : "disabled"))); + } + else + { + ereport(NOTICE, + (errmsg("index \"%s\" is already %s", + RelationGetRelationName(rel), + enable ? "enabled" : "disabled"))); + } + + /* Clean up */ + ReleaseSysCache(indexTuple); + table_close(pg_index, RowExclusiveLock); + + /* Invoke the object access hook if we updated the index */ + if (updated) + { + InvokeObjectPostAlterHook(IndexRelationId, indexOid, 0); + } +} diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c index c0fcc7d78d..843237dadf 100644 --- a/src/backend/optimizer/path/indxpath.c +++ b/src/backend/optimizer/path/indxpath.c @@ -254,6 +254,10 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel) { IndexOptInfo *index = (IndexOptInfo *) lfirst(lc); + /* Skip disabled indexes */ + if (!index->enabled) + continue; + /* Protect limited-size array in IndexClauseSets */ Assert(index->nkeycolumns <= INDEX_MAX_KEYS); @@ -715,6 +719,10 @@ get_index_paths(PlannerInfo *root, RelOptInfo *rel, bool skip_nonnative_saop = false; ListCell *lc; + /* Skip disabled indexes */ + if (!index->enabled) + return; + /* * Build simple index paths using the clauses. Allow ScalarArrayOpExpr * clauses only if the index AM supports them natively. @@ -823,6 +831,10 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel, Assert(skip_nonnative_saop != NULL || scantype == ST_BITMAPSCAN); + /* Skip disabled indexes */ + if (!index->enabled) + return NIL; + /* * Check that index supports the desired scan type(s) */ diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index b913f91ff0..30dc6353f6 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -192,6 +192,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, } } + /* * Estimate relation size --- unless it's an inheritance parent, in which * case the size we want is not the rel's own size but the size of its @@ -459,6 +460,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, info->unique = index->indisunique; info->immediate = index->indimmediate; info->hypothetical = false; + info->enabled = index->indisenabled; /* * Estimate the index size. If it's not a partial index, we lock diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ab304ca989..7e47541541 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -333,7 +333,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type add_drop opt_asc_desc opt_nulls_order %type alter_table_cmd alter_type_cmd opt_collate_clause - replica_identity partition_cmd index_partition_cmd + replica_identity partition_cmd index_partition_cmd index_alter_cmd %type alter_table_cmds alter_type_cmds %type alter_identity_column_option_list %type alter_identity_column_option @@ -496,6 +496,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type opt_unique opt_verbose opt_full %type opt_freeze opt_analyze opt_default %type opt_binary copy_delimiter +%type opt_index_enabled %type copy_from opt_program @@ -2144,6 +2145,24 @@ AlterTableStmt: n->nowait = $13; $$ = (Node *) n; } + | ALTER INDEX qualified_name index_alter_cmd + { + AlterTableStmt *n = makeNode(AlterTableStmt); + n->relation = $3; + n->cmds = list_make1($4); + n->objtype = OBJECT_INDEX; + n->missing_ok = false; + $$ = (Node *) n; + } + | ALTER INDEX IF_P EXISTS qualified_name index_alter_cmd + { + AlterTableStmt *n = makeNode(AlterTableStmt); + n->relation = $5; + n->cmds = list_make1($6); + n->objtype = OBJECT_INDEX; + n->missing_ok = true; + $$ = (Node *) n; + } | ALTER INDEX qualified_name alter_table_cmds { AlterTableStmt *n = makeNode(AlterTableStmt); @@ -2369,6 +2388,21 @@ index_partition_cmd: } ; +index_alter_cmd: + /* ALTER INDEX ENABLE|DISABLE */ + ENABLE_P + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_EnableIndex; + $$ = (Node *) n; + } + | DISABLE_P + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_DisableIndex; + $$ = (Node *) n; + } + ; alter_table_cmd: /* ALTER TABLE ADD */ ADD_P columnDef @@ -8102,7 +8136,7 @@ defacl_privilege_target: IndexStmt: CREATE opt_unique INDEX opt_concurrently opt_single_name ON relation_expr access_method_clause '(' index_params ')' - opt_include opt_unique_null_treatment opt_reloptions OptTableSpace where_clause + opt_include opt_unique_null_treatment opt_reloptions OptTableSpace where_clause opt_index_enabled { IndexStmt *n = makeNode(IndexStmt); @@ -8117,6 +8151,7 @@ IndexStmt: CREATE opt_unique INDEX opt_concurrently opt_single_name n->options = $14; n->tableSpace = $15; n->whereClause = $16; + n->isenabled = $17; n->excludeOpNames = NIL; n->idxcomment = NULL; n->indexOid = InvalidOid; @@ -8134,7 +8169,7 @@ IndexStmt: CREATE opt_unique INDEX opt_concurrently opt_single_name } | CREATE opt_unique INDEX opt_concurrently IF_P NOT EXISTS name ON relation_expr access_method_clause '(' index_params ')' - opt_include opt_unique_null_treatment opt_reloptions OptTableSpace where_clause + opt_include opt_unique_null_treatment opt_reloptions OptTableSpace where_clause opt_index_enabled { IndexStmt *n = makeNode(IndexStmt); @@ -8149,6 +8184,7 @@ IndexStmt: CREATE opt_unique INDEX opt_concurrently opt_single_name n->options = $17; n->tableSpace = $18; n->whereClause = $19; + n->isenabled = $20; n->excludeOpNames = NIL; n->idxcomment = NULL; n->indexOid = InvalidOid; @@ -8171,6 +8207,12 @@ opt_unique: | /*EMPTY*/ { $$ = false; } ; +opt_index_enabled: + ENABLE_P { $$ = true; } + | DISABLE_P { $$ = false; } + | /*EMPTY*/ { $$ = true; } + ; + access_method_clause: USING name { $$ = $2; } | /*EMPTY*/ { $$ = DEFAULT_INDEX_TYPE; } diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 1e15ce10b4..32554612ed 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -1588,6 +1588,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, index->concurrent = false; index->if_not_exists = false; index->reset_default_tblspc = false; + index->isenabled = idxrec->indisenabled; /* * We don't try to preserve the name of the source index; instead, just @@ -2214,6 +2215,8 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) index->concurrent = false; index->if_not_exists = false; index->reset_default_tblspc = constraint->reset_default_tblspc; + /* Ensure indexes for constraints are created as enabled by default */ + index->isenabled = true; /* * If it's ALTER TABLE ADD CONSTRAINT USING INDEX, look up the index and diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 2177d17e27..05b27ca232 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -1559,6 +1559,10 @@ pg_get_indexdef_worker(Oid indexrelid, int colno, else appendStringInfo(&buf, " WHERE %s", str); } + + /* Add DISABLE clause if the index is disabled */ + if (!idxrec->indisenabled) + appendStringInfoString(&buf, " DISABLE"); } /* Clean up */ diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 5b6b7b809c..7cd5902ad7 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -2344,6 +2344,7 @@ RelationReloadIndexInfo(Relation relation) relation->rd_index->indisready = index->indisready; relation->rd_index->indislive = index->indislive; relation->rd_index->indisreplident = index->indisreplident; + relation->rd_index->indisenabled = index->indisenabled; /* Copy xmin too, as that is needed to make sense of indcheckxmin */ HeapTupleHeaderSetXmin(relation->rd_indextuple->t_data, diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 10bb26f2e4..a20af83da4 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202409211 +#define CATALOG_VERSION_NO 202409220 #endif diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h index 2dea96f47c..8a47cfd174 100644 --- a/src/include/catalog/index.h +++ b/src/include/catalog/index.h @@ -65,6 +65,7 @@ extern void index_check_primary_key(Relation heapRel, #define INDEX_CREATE_IF_NOT_EXISTS (1 << 4) #define INDEX_CREATE_PARTITIONED (1 << 5) #define INDEX_CREATE_INVALID (1 << 6) +#define INDEX_CREATE_ENABLED (1 << 7) extern Oid index_create(Relation heapRelation, const char *indexRelationName, @@ -175,7 +176,6 @@ extern void RestoreReindexState(const void *reindexstate); extern void IndexSetParentIndex(Relation partitionIdx, Oid parentOid); - /* * itemptr_encode - Encode ItemPointer as int64/int8 * diff --git a/src/include/catalog/pg_index.h b/src/include/catalog/pg_index.h index 6c89639a9e..b0c7e5f365 100644 --- a/src/include/catalog/pg_index.h +++ b/src/include/catalog/pg_index.h @@ -44,6 +44,7 @@ CATALOG(pg_index,2610,IndexRelationId) BKI_SCHEMA_MACRO bool indisready; /* is this index ready for inserts? */ bool indislive; /* is this index alive at all? */ bool indisreplident; /* is this index the identity for replication? */ + bool indisenabled; /* is this index enabled for use by queries? */ /* variable-length fields start here, but we allow direct access to indkey */ int2vector indkey BKI_FORCE_NOT_NULL; /* column numbers of indexed cols, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index e62ce1b753..e5125895b0 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2413,6 +2413,8 @@ typedef enum AlterTableType AT_SetIdentity, /* SET identity column options */ AT_DropIdentity, /* DROP IDENTITY */ AT_ReAddStatistics, /* internal to commands/tablecmds.c */ + AT_EnableIndex, /* ENABLE INDEX */ + AT_DisableIndex, /* DISABLE INDEX */ } AlterTableType; typedef struct ReplicaIdentityStmt @@ -3378,6 +3380,7 @@ typedef struct IndexStmt bool if_not_exists; /* just do nothing if index already exists? */ bool reset_default_tblspc; /* reset default_tablespace prior to * executing */ + bool isenabled; /* true if ENABLE (default), false if DISABLE */ } IndexStmt; /* ---------------------- diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 07e2415398..4096c4b797 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -1188,6 +1188,8 @@ struct IndexOptInfo bool immediate; /* true if index doesn't really exist */ bool hypothetical; + /* is index enable? */ + bool enabled; /* * Remaining fields are copied from the index AM's API struct diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index d3358dfc39..57add757aa 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -2965,6 +2965,256 @@ ERROR: REINDEX SCHEMA cannot run inside a transaction block END; -- concurrently REINDEX SCHEMA CONCURRENTLY schema_to_reindex; +-- Test enable/disable functionality for indexes +-- Setup +CREATE TABLE enable_disable_test(id int primary key, data text); +INSERT INTO enable_disable_test SELECT g, 'data ' || g FROM generate_series(1, 1000) g; +-- CREATE INDEX with ENABLED/DISABLED +CREATE INDEX enable_disable_idx1 ON enable_disable_test(data) DISABLE; +CREATE INDEX enable_disable_idx2 ON enable_disable_test(data); +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + indexrelid | indisvalid | indisready | indislive | indisenabled +--------------------------+------------+------------+-----------+-------------- + enable_disable_idx1 | t | t | t | f + enable_disable_idx2 | t | t | t | t + enable_disable_test_pkey | t | t | t | t +(3 rows) + +-- ALTER INDEX ... ENABLE/DISABLE +-- Before +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + indexrelid | indisvalid | indisready | indislive | indisenabled +--------------------------+------------+------------+-----------+-------------- + enable_disable_idx1 | t | t | t | f + enable_disable_idx2 | t | t | t | t + enable_disable_test_pkey | t | t | t | t +(3 rows) + +ALTER INDEX enable_disable_idx2 DISABLE; +NOTICE: index "enable_disable_idx2" is now disabled +-- After +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + indexrelid | indisvalid | indisready | indislive | indisenabled +--------------------------+------------+------------+-----------+-------------- + enable_disable_idx1 | t | t | t | f + enable_disable_idx2 | t | t | t | f + enable_disable_test_pkey | t | t | t | t +(3 rows) + +-- Enable all indexes +ALTER INDEX enable_disable_idx2 ENABLE; +NOTICE: index "enable_disable_idx2" is now enabled +ALTER INDEX enable_disable_idx1 ENABLE; +NOTICE: index "enable_disable_idx1" is now enabled +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + indexrelid | indisvalid | indisready | indislive | indisenabled +--------------------------+------------+------------+-----------+-------------- + enable_disable_idx1 | t | t | t | t + enable_disable_idx2 | t | t | t | t + enable_disable_test_pkey | t | t | t | t +(3 rows) + +-- REINDEX TABLE +REINDEX TABLE enable_disable_test; +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + indexrelid | indisvalid | indisready | indislive | indisenabled +--------------------------+------------+------------+-----------+-------------- + enable_disable_idx1 | t | t | t | t + enable_disable_idx2 | t | t | t | t + enable_disable_test_pkey | t | t | t | t +(3 rows) + +-- REINDEX INDEX with enable/disable +ALTER INDEX enable_disable_idx1 DISABLE; +NOTICE: index "enable_disable_idx1" is now disabled +REINDEX INDEX enable_disable_idx1; +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + indexrelid | indisvalid | indisready | indislive | indisenabled +--------------------------+------------+------------+-----------+-------------- + enable_disable_idx1 | t | t | t | f + enable_disable_idx2 | t | t | t | t + enable_disable_test_pkey | t | t | t | t +(3 rows) + +-- REINDEX INDEX CONCURRENTLY with enable/disable +ALTER INDEX enable_disable_idx1 ENABLE; +NOTICE: index "enable_disable_idx1" is now enabled +REINDEX INDEX enable_disable_idx1; +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + indexrelid | indisvalid | indisready | indislive | indisenabled +--------------------------+------------+------------+-----------+-------------- + enable_disable_idx1 | t | t | t | t + enable_disable_idx2 | t | t | t | t + enable_disable_test_pkey | t | t | t | t +(3 rows) + +-- Test ENABLE/DISABLE on TOAST index +CREATE TABLE toast_test (id int primary key, data text); +INSERT INTO toast_test SELECT g, repeat('long text ', 1000) FROM generate_series(1, 10) g; +-- Check initial state of TOAST index +SELECT indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indexrelid = (SELECT indexrelid FROM pg_index WHERE indrelid = (SELECT reltoastrelid FROM pg_class WHERE oid = 'toast_test'::regclass)); + indisvalid | indisready | indislive | indisenabled +------------+------------+-----------+-------------- + t | t | t | t +(1 row) + +-- Disable TOAST index +ALTER INDEX pg_toast.pg_toast_16385_index DISABLE; +ERROR: relation "pg_toast.pg_toast_16385_index" does not exist +-- Check state after disabling TOAST index +SELECT indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indexrelid = (SELECT indexrelid FROM pg_index WHERE indrelid = (SELECT reltoastrelid FROM pg_class WHERE oid = 'toast_test'::regclass)); + indisvalid | indisready | indislive | indisenabled +------------+------------+-----------+-------------- + t | t | t | t +(1 row) + +-- Enable TOAST index +ALTER INDEX pg_toast.pg_toast_16385_index ENABLE; +ERROR: relation "pg_toast.pg_toast_16385_index" does not exist +-- Check state after enabling TOAST index +SELECT indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indexrelid = (SELECT indexrelid FROM pg_index WHERE indrelid = (SELECT reltoastrelid FROM pg_class WHERE oid = 'toast_test'::regclass)); + indisvalid | indisready | indislive | indisenabled +------------+------------+-----------+-------------- + t | t | t | t +(1 row) + +-- Test CREATE TABLE with UNIQUE constraint +CREATE TABLE unique_constraint_test (id int UNIQUE, data text); +INSERT INTO unique_constraint_test SELECT g, 'data ' || g FROM generate_series(1, 1000) g; +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'unique_constraint_test'::regclass; + indexrelid | indisvalid | indisready | indislive | indisenabled +-------------------------------+------------+------------+-----------+-------------- + unique_constraint_test_id_key | t | t | t | t +(1 row) + +-- Test that the unique constraint index is used +EXPLAIN (COSTS OFF) SELECT * FROM unique_constraint_test WHERE id = 500; + QUERY PLAN +-------------------------------------------------------------------------- + Index Scan using unique_constraint_test_id_key on unique_constraint_test + Index Cond: (id = 500) +(2 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM unique_constraint_test WHERE id IN (100, 200, 300); + QUERY PLAN +------------------------------------------------------------- + Bitmap Heap Scan on unique_constraint_test + Recheck Cond: (id = ANY ('{100,200,300}'::integer[])) + -> Bitmap Index Scan on unique_constraint_test_id_key + Index Cond: (id = ANY ('{100,200,300}'::integer[])) +(4 rows) + +-- Test CREATE TABLE with INDEX +CREATE TABLE index_test (id int, data text); +INSERT INTO index_test SELECT g, 'data ' || g FROM generate_series(1, 1000) g; +CREATE INDEX ON index_test (data); +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'index_test'::regclass; + indexrelid | indisvalid | indisready | indislive | indisenabled +---------------------+------------+------------+-----------+-------------- + index_test_data_idx | t | t | t | t +(1 row) + +-- Test that the index is used +EXPLAIN (COSTS OFF) SELECT * FROM index_test WHERE data = 'data 500'; + QUERY PLAN +------------------------------------------------ + Bitmap Heap Scan on index_test + Recheck Cond: (data = 'data 500'::text) + -> Bitmap Index Scan on index_test_data_idx + Index Cond: (data = 'data 500'::text) +(4 rows) + +-- Test index usage with joins +CREATE TABLE join_test (id int PRIMARY KEY, ref_id int); +INSERT INTO join_test SELECT g, g % 100 FROM generate_series(1, 1000) g; +EXPLAIN (COSTS OFF) +SELECT jt.id, it.data +FROM join_test jt +JOIN index_test it ON jt.ref_id = it.id +WHERE jt.id BETWEEN 100 AND 200; + QUERY PLAN +--------------------------------------------------------------- + Hash Join + Hash Cond: (it.id = jt.ref_id) + -> Seq Scan on index_test it + -> Hash + -> Bitmap Heap Scan on join_test jt + Recheck Cond: ((id >= 100) AND (id <= 200)) + -> Bitmap Index Scan on join_test_pkey + Index Cond: ((id >= 100) AND (id <= 200)) +(8 rows) + +-- Test index usage with ORDER BY +EXPLAIN (COSTS OFF) +SELECT * +FROM index_test +ORDER BY data +LIMIT 10; + QUERY PLAN +---------------------------------------------------------- + Limit + -> Index Scan using index_test_data_idx on index_test +(2 rows) + +-- Test disabling an index and its effect on query plan +ALTER INDEX index_test_data_idx DISABLE; +NOTICE: index "index_test_data_idx" is now disabled +EXPLAIN (COSTS OFF) SELECT * FROM index_test WHERE data = 'data 500'; + QUERY PLAN +------------------------------------- + Seq Scan on index_test + Filter: (data = 'data 500'::text) +(2 rows) + +-- Re-enable the index +ALTER INDEX index_test_data_idx ENABLE; +NOTICE: index "index_test_data_idx" is now enabled +EXPLAIN (COSTS OFF) SELECT * FROM index_test WHERE data = 'data 500'; + QUERY PLAN +------------------------------------------------ + Bitmap Heap Scan on index_test + Recheck Cond: (data = 'data 500'::text) + -> Bitmap Index Scan on index_test_data_idx + Index Cond: (data = 'data 500'::text) +(4 rows) + +-- Clean up +DROP TABLE enable_disable_test; +DROP TABLE toast_test; +DROP TABLE unique_constraint_test; +DROP TABLE join_test; +DROP TABLE index_test; -- Failure for unauthorized user CREATE ROLE regress_reindexuser NOLOGIN; SET SESSION ROLE regress_reindexuser; diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql index fe162cc7c3..d599717c6f 100644 --- a/src/test/regress/sql/create_index.sql +++ b/src/test/regress/sql/create_index.sql @@ -1297,6 +1297,146 @@ END; -- concurrently REINDEX SCHEMA CONCURRENTLY schema_to_reindex; +-- Test enable/disable functionality for indexes + +-- Setup +CREATE TABLE enable_disable_test(id int primary key, data text); +INSERT INTO enable_disable_test SELECT g, 'data ' || g FROM generate_series(1, 1000) g; + +-- CREATE INDEX with ENABLED/DISABLED +CREATE INDEX enable_disable_idx1 ON enable_disable_test(data) DISABLE; +CREATE INDEX enable_disable_idx2 ON enable_disable_test(data); +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + +-- ALTER INDEX ... ENABLE/DISABLE +-- Before +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + +ALTER INDEX enable_disable_idx2 DISABLE; +-- After +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + +-- Enable all indexes +ALTER INDEX enable_disable_idx2 ENABLE; +ALTER INDEX enable_disable_idx1 ENABLE; +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + +-- REINDEX TABLE +REINDEX TABLE enable_disable_test; +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + +-- REINDEX INDEX with enable/disable +ALTER INDEX enable_disable_idx1 DISABLE; +REINDEX INDEX enable_disable_idx1; +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + +-- REINDEX INDEX CONCURRENTLY with enable/disable +ALTER INDEX enable_disable_idx1 ENABLE; +REINDEX INDEX enable_disable_idx1; +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'enable_disable_test'::regclass +ORDER BY indexrelid::regclass::text; + +-- Test ENABLE/DISABLE on TOAST index +CREATE TABLE toast_test (id int primary key, data text); +INSERT INTO toast_test SELECT g, repeat('long text ', 1000) FROM generate_series(1, 10) g; + +-- Check initial state of TOAST index +SELECT indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indexrelid = (SELECT indexrelid FROM pg_index WHERE indrelid = (SELECT reltoastrelid FROM pg_class WHERE oid = 'toast_test'::regclass)); + +-- Disable TOAST index +ALTER INDEX pg_toast.pg_toast_16385_index DISABLE; + +-- Check state after disabling TOAST index +SELECT indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indexrelid = (SELECT indexrelid FROM pg_index WHERE indrelid = (SELECT reltoastrelid FROM pg_class WHERE oid = 'toast_test'::regclass)); + +-- Enable TOAST index +ALTER INDEX pg_toast.pg_toast_16385_index ENABLE; + +-- Check state after enabling TOAST index +SELECT indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indexrelid = (SELECT indexrelid FROM pg_index WHERE indrelid = (SELECT reltoastrelid FROM pg_class WHERE oid = 'toast_test'::regclass)); + +-- Test CREATE TABLE with UNIQUE constraint +CREATE TABLE unique_constraint_test (id int UNIQUE, data text); +INSERT INTO unique_constraint_test SELECT g, 'data ' || g FROM generate_series(1, 1000) g; +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'unique_constraint_test'::regclass; + +-- Test that the unique constraint index is used +EXPLAIN (COSTS OFF) SELECT * FROM unique_constraint_test WHERE id = 500; +EXPLAIN (COSTS OFF) SELECT * FROM unique_constraint_test WHERE id IN (100, 200, 300); + +-- Test CREATE TABLE with INDEX +CREATE TABLE index_test (id int, data text); +INSERT INTO index_test SELECT g, 'data ' || g FROM generate_series(1, 1000) g; +CREATE INDEX ON index_test (data); +SELECT indexrelid::regclass, indisvalid, indisready, indislive, indisenabled +FROM pg_index +WHERE indrelid = 'index_test'::regclass; + +-- Test that the index is used +EXPLAIN (COSTS OFF) SELECT * FROM index_test WHERE data = 'data 500'; + +-- Test index usage with joins +CREATE TABLE join_test (id int PRIMARY KEY, ref_id int); +INSERT INTO join_test SELECT g, g % 100 FROM generate_series(1, 1000) g; + +EXPLAIN (COSTS OFF) +SELECT jt.id, it.data +FROM join_test jt +JOIN index_test it ON jt.ref_id = it.id +WHERE jt.id BETWEEN 100 AND 200; + +-- Test index usage with ORDER BY +EXPLAIN (COSTS OFF) +SELECT * +FROM index_test +ORDER BY data +LIMIT 10; + +-- Test disabling an index and its effect on query plan +ALTER INDEX index_test_data_idx DISABLE; + +EXPLAIN (COSTS OFF) SELECT * FROM index_test WHERE data = 'data 500'; + +-- Re-enable the index +ALTER INDEX index_test_data_idx ENABLE; + +EXPLAIN (COSTS OFF) SELECT * FROM index_test WHERE data = 'data 500'; + +-- Clean up +DROP TABLE enable_disable_test; +DROP TABLE toast_test; +DROP TABLE unique_constraint_test; +DROP TABLE join_test; +DROP TABLE index_test; + -- Failure for unauthorized user CREATE ROLE regress_reindexuser NOLOGIN; SET SESSION ROLE regress_reindexuser; -- 2.37.1 (Apple Git-137.1)