diff --git a/CN/modules/ROOT/nav.adoc b/CN/modules/ROOT/nav.adoc index 473ba5eb..05f57f8d 100644 --- a/CN/modules/ROOT/nav.adoc +++ b/CN/modules/ROOT/nav.adoc @@ -30,6 +30,7 @@ ** xref:master/oracle_compatibility/with_function_procedure.adoc[21、WITH FUNCTION/PROCEDURE] ** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、索引 ONLINE 参数] ** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG 函数] +** xref:master/oracle_compatibility/compat_alter_index_unusable.adoc[24、禁用索引] * 容器化与云服务 ** 容器化指南 *** xref:master/containerization/k8s_deployment.adoc[K8S部署] @@ -107,6 +108,7 @@ **** xref:master/compatibility_features_design/read_only_view.adoc[视图只读] **** xref:master/compatibility_features_design/with_function_procedure_impl.adoc[WITH FUNCTION/PROCEDURE] **** xref:master/compatibility_features_design/create_index_online.adoc[索引 ONLINE 参数] +**** xref:master/compatibility_features_design/alter_index_unusable_impl.adoc[禁用索引] *** 内置函数 **** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] **** xref:master/oracle_builtin_functions/userenv.adoc[userenv] diff --git a/CN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl.adoc b/CN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl.adoc new file mode 100644 index 00000000..742ff647 --- /dev/null +++ b/CN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl.adoc @@ -0,0 +1,348 @@ +:sectnums: +:sectnumlevels: 5 + += ALTER INDEX ... UNUSABLE 实现说明 + +== 目的 + +本文档详细说明 IvorySQL 中 `ALTER INDEX ... UNUSABLE` 功能的实现原理。该功能允许在 Oracle 兼容模式下手动禁用一个索引,使其不再被规划器选用、不再被 DML 维护,实现 Oracle 数据库 `ALTER INDEX` 语句的 `UNUSABLE` 子句。 + +== 实现说明 + +=== 系统分层架构 + +`ALTER INDEX ... UNUSABLE` 的实现分为五个层次: + +``` +┌───────────────────────────────────────────────────────────────────────┐ +│ Layer 1: Oracle 解析器 (ora_gram.y + ora_kwlist.h) │ +│ ─ 新增 UNUSABLE 关键字(UNRESERVED_KEYWORD, BARE_LABEL) │ +│ ─ ALTER INDEX qualified_name UNUSABLE → OraAlterIndexUnusableStmt │ +└───────────────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────────────┐ +│ Layer 2: 语句分发 (tcop/utility.c) │ +│ ─ T_OraAlterIndexUnusableStmt 与既有 T_OraAlterIndexRebuildStmt │ +│ 并列出现在 4 处:只读事务分类 / ProcessUtilitySlow / │ +│ CreateCommandTag / GetCommandLogLevel │ +└───────────────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────────────┐ +│ Layer 3: 执行层 (commands/indexcmds.c) │ +│ ─ ExecOraAlterIndexUnusable():权限检查、解析锁定目标索引、 │ +│ 拒绝分区索引、调用 index_set_unusable(indexOid, true) │ +└───────────────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────────────┐ +│ Layer 4: 目录层 (catalog/index.c + include/catalog/pg_index.h) │ +│ ─ 新增 pg_index.indisunusable 列 │ +│ ─ index_set_unusable():与 index_set_state_flags() 同构的独立 │ +│ catalog 更新函数 │ +│ ─ index_create() 的 values[] 数组补充该列初始值(false) │ +│ ─ reindex_index() 在既有"修复索引状态"分支中一并清除该标记 │ +└───────────────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────────────┐ +│ Layer 5: 规划器 / 执行器 (plancat.c + BuildIndexInfo) │ +│ ─ get_relation_info() 跳过 indisunusable 的索引 │ +│ ─ BuildIndexInfo() 令 ii_ReadyForInserts 一并反映该标记, │ +│ execIndexing.c 既有的跳过逻辑自然覆盖,无需改动执行器本身 │ +└───────────────────────────────────────────────────────────────────────┘ +``` + +=== 语法与解析 + +==== 关键字注册 + +`src/include/oracle_parser/ora_kwlist.h`: + +[source,c] +---- +PG_KEYWORD("until", UNTIL, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unusable", UNUSABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("update", UPDATE, UNRESERVED_KEYWORD, BARE_LABEL) +---- + +与 `REBUILD`/`ONLINE`/`TABLESPACE`/`PARALLEL` 同属非保留关键字,按字母序插入。`UNUSABLE` 同时加入 `%token` 列表、`unreserved_keyword` 与 `bare_label_keyword` 两处产生式,使其可作为裸标识符/列标签使用。 + +==== 语法规则扩展 + +`src/backend/oracle_parser/ora_gram.y` 中紧邻既有的 `REBUILD` 规则新增: + +[source,yacc] +---- +| ALTER INDEX qualified_name REBUILD rebuild_index_opt_list + { + OraAlterIndexRebuildStmt *n = makeNode(OraAlterIndexRebuildStmt); + n->relation = $3; + n->options = $5; + $$ = (Node *) n; + } +| ALTER INDEX qualified_name UNUSABLE + { + OraAlterIndexUnusableStmt *n = makeNode(OraAlterIndexUnusableStmt); + n->relation = $3; + $$ = (Node *) n; + } +---- + +与 REBUILD 不同,`UNUSABLE` 不带任何选项列表——Oracle 的 `UNUSABLE` 子句本身不接受参数。 + +=== AST 节点设计 + +`src/include/nodes/parsenodes.h` 中新增: + +[source,c] +---- +/* + * Oracle-compatible ALTER INDEX ... UNUSABLE Statement + * + * 标记索引不可用:规划器跳过、DML 不再维护,直到通过 REBUILD(或 + * 普通 REINDEX)重建后恢复。 + */ +typedef struct OraAlterIndexUnusableStmt +{ + NodeTag type; + RangeVar *relation; /* index to mark unusable */ +} OraAlterIndexUnusableStmt; +---- + +copy/equal/out/read 函数由 `gen_node_support.pl` 从该结构体自动生成,无需手动维护(与既有 `OraAlterIndexRebuildStmt` 相同的处理方式)。 + +=== 目录层设计 + +==== 为什么新增 `pg_index` 列,而非 reloption 或复用 `indisvalid`/`indisready` + +- **reloption 方案不可行**:PostgreSQL 的索引 reloption 按访问方法分别声明(btree/gin/hash 各自维护私有选项表),不存在通用的"索引级" reloption kind;绕开 AM 校验直接写 `pg_class.reloptions` 会导致 `pg_dump` 恢复时因未声明的 key 而报错。 +- **不能复用 `indisvalid`/`indisready`**:两者与 `CREATE/DROP INDEX CONCURRENTLY` 的多阶段状态机强耦合(`index_set_state_flags()` 对状态转换有严格 `Assert`),重载语义会破坏并发建索引的不变式。 + +最终方案:新增独立的 `bool indisunusable` 列,`src/include/catalog/pg_index.h`: + +[source,c] +---- +bool indislive; /* is this index alive at all? */ +bool indisreplident; /* is this index the identity for replication? */ +bool indisunusable; /* manually disabled via ALTER INDEX ... + * UNUSABLE (Oracle compat); skipped by the + * planner and unmaintained by DML until + * REBUILD */ +---- + +对应 `catversion.h` 的 catalog 版本号升级。IvorySQL 此前已有直接扩展核心 catalog 的先例(`pg_class.relhasrowid` 用于 ROWID 特性、`pg_attribute.attisinvisible` 用于不可见列特性),本次沿用同样的模式。 + +`indisunusable` 是瞬态索引*状态*而非定义性属性,与 `indisvalid`/`indisready` 同类,`pg_dump` 从不导出这类状态列,因此不需要像 `attisinvisible` 那样额外增加 `pg_dump` 支持代码。 + +==== `index_set_unusable()`:独立的状态变更函数 + +`src/backend/catalog/index.c`,紧邻既有的 `index_set_state_flags()`: + +[source,c] +---- +/* + * index_set_unusable - set or clear pg_index.indisunusable + * + * Oracle-compatible companion to index_set_state_flags(): flips the + * "manually disabled via ALTER INDEX ... UNUSABLE" bit. Unlike the + * indisvalid/indisready/indislive trio, this flag is not part of the + * CREATE/DROP INDEX CONCURRENTLY state machine, so no transition + * invariants need to be asserted here. + */ +void +index_set_unusable(Oid indexId, bool unusable) +{ + Relation pg_index; + HeapTuple indexTuple; + Form_pg_index indexForm; + + pg_index = table_open(IndexRelationId, RowExclusiveLock); + + indexTuple = SearchSysCacheCopy1(INDEXRELID, ObjectIdGetDatum(indexId)); + if (!HeapTupleIsValid(indexTuple)) + elog(ERROR, "cache lookup failed for index %u", indexId); + indexForm = (Form_pg_index) GETSTRUCT(indexTuple); + + if (indexForm->indisunusable != unusable) + { + indexForm->indisunusable = unusable; + CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple); + } + + table_close(pg_index, RowExclusiveLock); +} +---- + +`CatalogTupleUpdate()` 自带 cache invalidation,无需手动调用 `CacheInvalidateRelcache`。 + +=== 执行层设计 + +==== `ExecOraAlterIndexUnusable()` + +`src/backend/commands/indexcmds.c`,紧邻既有的 `ExecOraAlterIndexRebuild()`: + +[source,c] +---- +void +ExecOraAlterIndexUnusable(ParseState *pstate, const OraAlterIndexUnusableStmt *stmt) +{ + ReindexParams params = {0}; + struct ReindexIndexCallbackState cbstate; + Oid indexOid; + char relkind; + + if (ORA_PARSER != compatible_db) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ALTER INDEX ... UNUSABLE is only available when compatible_db is oracle"))); + + /* + * 复用 ExecOraAlterIndexRebuild() 非 CONCURRENTLY 路径同样的解析/加锁 + * 方式:索引本身 AccessExclusiveLock,堆表 ShareLock(由共享回调决定), + * 权限检查也由该回调完成。 + */ + cbstate.params = params; + cbstate.locked_table_oid = InvalidOid; + + indexOid = RangeVarGetRelidExtended(stmt->relation, + AccessExclusiveLock, + 0, + RangeVarCallbackForReindexIndex, + &cbstate); + + relkind = get_rel_relkind(indexOid); + if (relkind == RELKIND_PARTITIONED_INDEX) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ALTER INDEX ... UNUSABLE is not supported for partitioned indexes"), + errhint("Mark each partition's leaf index unusable individually."))); + + index_set_unusable(indexOid, true); +} +---- + +`RangeVarCallbackForReindexIndex` 是既有 REINDEX/REBUILD 基础设施中的共享回调,直接复用意味着权限检查(`pg_class_aclcheck(..., ACL_MAINTAIN)`)、relkind 校验、堆表加锁顺序都与 REBUILD/REINDEX 完全一致,不需要重新实现。 + +==== 规划器:跳过 unusable 索引 + +`src/backend/optimizer/util/plancat.c`,`get_relation_info()`: + +[source,c] +---- +if (!index->indisvalid) +{ + index_close(indexRelation, NoLock); + continue; +} + +/* + * Ignore indexes manually disabled via the Oracle-compatible + * ALTER INDEX ... UNUSABLE. This check is unconditional (not + * gated on the current session's compatible_db): indisunusable + * is a catalog fact about the index, not a per-session parser + * choice, and every session must honor it consistently to avoid + * planning against a stale/unmaintained index. + */ +if (index->indisunusable) +{ + index_close(indexRelation, NoLock); + continue; +} +---- + +==== 执行器:跳过 unusable 索引的维护 + +`src/backend/catalog/index.c`,`BuildIndexInfo()`: + +[source,c] +---- +ii = makeIndexInfo(indexStruct->indnatts, + indexStruct->indnkeyatts, + index->rd_rel->relam, + RelationGetIndexExpressions(index), + RelationGetIndexPredicate(index), + indexStruct->indisunique, + indexStruct->indnullsnotdistinct, + /* + * A manually UNUSABLE index (Oracle compat) is treated + * as not ready for inserts, so DML stops maintaining + * it, same as an in-progress CREATE INDEX CONCURRENTLY. + */ + indexStruct->indisready && !indexStruct->indisunusable, + false, + index->rd_indam->amsummarizing, + indexStruct->indisexclusion && indexStruct->indisunique); +---- + +`ii_ReadyForInserts` 一旦为 false,`execIndexing.c` 里既有的 `if (!indexInfo->ii_ReadyForInserts) continue;` 逻辑自然跳过该索引的插入/更新维护,**执行器本身不需要任何改动**。这也是唯一/主键索引 UNUSABLE 后唯一性检查自动停止的原因——检查发生在索引自身的 `aminsert` 路径内部,不需要额外联动 `pg_constraint`。 + +==== REBUILD / REINDEX 收尾:清除 UNUSABLE 标记 + +`src/backend/catalog/index.c`,`reindex_index()` 中"发现索引状态需要修复"的既有分支: + +[source,c] +---- +index_bad = (!indexForm->indisvalid || + !indexForm->indisready || + !indexForm->indislive || + indexForm->indisunusable); +if (index_bad || + (indexForm->indcheckxmin && !indexInfo->ii_BrokenHotChain)) +{ + ... + indexForm->indisvalid = true; + indexForm->indisready = true; + indexForm->indislive = true; + /* a completed non-concurrent rebuild always clears UNUSABLE */ + indexForm->indisunusable = false; + CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple); +} +---- + +这个分支覆盖:普通 `REINDEX INDEX`、`ALTER INDEX ... REBUILD`(非 `ONLINE`)、`REBUILD PARTITION`(非 `ONLINE`)。`REBUILD ONLINE`(并发路径,走 `ReindexRelationConcurrently()`,从不调用 `reindex_index()`)**不会**清除该标记,见"已知限制"。 + +=== 与 PG_PARSER 隔离策略 + +按项目惯例(`if (ORA_PARSER == compatible_db) { ... }`),本功能在两个层面隔离: + +1. **语法层天然隔离**:`UNUSABLE` 语法只存在于 `ora_gram.y`,`PG_PARSER` 会话完全没有语法入口。 +2. **命令入口纵深防御**:`ExecOraAlterIndexUnusable()` 额外用 `if (ORA_PARSER != compatible_db) ereport(ERROR, ...)` 兜底。 + +=== 错误处理 + +==== 分区索引拒绝 + +[source,sql] +---- +ALTER INDEX idx_sales_id UNUSABLE; -- idx_sales_id 是分区父索引 +-- ERROR: ALTER INDEX ... UNUSABLE is not supported for partitioned indexes +-- HINT: Mark each partition's leaf index unusable individually. +---- +错误由 `ExecOraAlterIndexUnusable()` 中 `relkind == RELKIND_PARTITIONED_INDEX` 分支主动抛出。 + +==== PG_PARSER 模式拒绝 + +[source,sql] +---- +SET compatible_db = PG_PARSER; +ALTER INDEX idx_emp_email UNUSABLE; +-- ERROR: syntax error at or near "UNUSABLE" +---- +语法层面在 `PG_PARSER` 下天然不存在该产生式,无需运行时判断即报错。 + +==== 重复维护重建失败(唯一性冲突) + +[source,sql] +---- +ALTER INDEX idx_emp_email UNUSABLE; +INSERT INTO emp VALUES (999, 'a@example.com'); -- 与已有行重复,成功 +ALTER INDEX idx_emp_email REBUILD; +-- ERROR: could not create unique index "idx_emp_email" +-- DETAIL: Key (email)=(a@example.com) is duplicated. +---- +错误来自 `reindex_index()` 内部 `index_build()` 的标准唯一性校验,非本功能新增逻辑,属于自然继承的行为。 + +=== 已知限制 + +1. **不支持 `USABLE` 子句**:与 Oracle 行为一致,REBUILD 是唯一恢复路径; +2. **`REBUILD ONLINE` 不清除 UNUSABLE**:并发重建路径的多事务快照生命周期与在其后追加一次独立目录更新不兼容; +3. **只有根/父分区索引本身不支持**:`RELKIND_PARTITIONED_INDEX` 上执行会报错;每个分区自己的叶子物理索引不受限制; +4. **`SKIP_UNUSABLE_INDEXES` 会话参数**:Oracle 允许通过该参数控制查询遇到 unusable 唯一索引时是否报错,不支持; diff --git a/CN/modules/ROOT/pages/master/oracle_compatibility/compat_alter_index_unusable.adoc b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_alter_index_unusable.adoc new file mode 100644 index 00000000..0b77fdf6 --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_alter_index_unusable.adoc @@ -0,0 +1,275 @@ +:sectnums: +:sectnumlevels: 5 + += ALTER INDEX ... UNUSABLE + +== 目的 + +本文档解释 IvorySQL 中 `ALTER INDEX ... UNUSABLE` 的用途,实现 Oracle 风格的索引手动禁用功能。 + +`UNUSABLE` 是 Oracle 数据库 `ALTER INDEX` 语句的一个子句,用于将索引标记为不可用:索引对象仍保留在数据字典中,但停止参与查询优化和 DML 维护。典型用途是在批量数据装载前禁用索引维护以提升写入性能,装载完成后通过 `ALTER INDEX ... REBUILD` 统一重建。 + +== 功能说明 + +=== 基本语法 + +在 Oracle 兼容模式(`compatible_db = ORA_PARSER`)下,`ALTER INDEX` 支持以下扩展语法: + +[source,sql] +---- +ALTER INDEX index_name UNUSABLE; +---- + +真实 Oracle **没有 `ALTER INDEX ... USABLE` 子句**——恢复可用状态唯一记录在案的方式是 `ALTER INDEX ... REBUILD`(IvorySQL 已支持,见另文)。IvorySQL 与此保持一致,不提供 `USABLE` 子句。 + +=== 核心特性 + +- **规划器排除**:标记为 UNUSABLE 的索引不再被规划器选为访问路径,即使显式 `SET enable_seqscan = off` 也不例外——它会被彻底排除,而不是仅仅被降权 +- **DML 停止维护**:INSERT/UPDATE 不再更新该索引的物理结构 +- **唯一性检查随之停止**:若该索引是唯一索引或主键索引,由于唯一性检查发生在索引自身的插入路径中,维护停止后重复值可以被静默插入(与 Oracle 行为一致) +- **索引定义与数据不删除**:索引对象、其定义、已有的物理页面均保留,只是不再被读写 +- **仅 REBUILD 可恢复**:`ALTER INDEX ... REBUILD`(非 `ONLINE`)或普通 `REINDEX INDEX` 会重新物理构建索引并自动清除 UNUSABLE 标记,恢复索引的规划器可见性和 DML 维护 +- **不支持分区索引**:对 `RELKIND_PARTITIONED_INDEX` 执行 `UNUSABLE` 会报错,需要对各叶子分区索引分别操作 + +== 语法示例 + +=== 基本用法 + +[source,sql] +---- +CREATE TABLE emp (id int PRIMARY KEY, email text); +CREATE UNIQUE INDEX idx_emp_email ON emp(email); + +ALTER INDEX idx_emp_email UNUSABLE; +-- 之后:查询不再使用该索引;INSERT 重复 email 也不会报错 +---- + +=== 批量装载工作流 + +[source,sql] +---- +-- 装载前禁用索引维护 +ALTER INDEX idx_emp_email UNUSABLE; + +-- 批量装载数据(跳过索引维护开销) +INSERT INTO emp SELECT * FROM staging_emp; + +-- 装载完成后重建索引,恢复可用 +ALTER INDEX idx_emp_email REBUILD; +---- + +=== 规划器排除效果 + +[source,sql] +---- +SET enable_seqscan = off; +EXPLAIN (COSTS OFF) SELECT * FROM emp WHERE email = 'a@example.com'; +-- QUERY PLAN +-- ------------------------------------------------------ +-- Index Only Scan using idx_emp_email on emp +-- Index Cond: (email = 'a@example.com'::text) + +ALTER INDEX idx_emp_email UNUSABLE; + +EXPLAIN (COSTS OFF) SELECT * FROM emp WHERE email = 'a@example.com'; +-- QUERY PLAN +-- ---------------------------------- +-- Seq Scan on emp +-- Disabled: true +-- Filter: (email = 'a@example.com'::text) +-- 即使 Seq Scan 已被 enable_seqscan=off 禁用,规划器也没有其它可选路径 +RESET enable_seqscan; +---- + +=== 唯一性检查停止 + +[source,sql] +---- +ALTER INDEX idx_emp_email UNUSABLE; +INSERT INTO emp VALUES (999, 'a@example.com'); -- 与已有行重复 +-- 成功,无 ERROR(维护已停止,未检测到冲突) +---- + +=== REBUILD 恢复可用 + +[source,sql] +---- +-- 如果 UNUSABLE 期间产生了违反唯一性的数据,REBUILD 会失败 +ALTER INDEX idx_emp_email REBUILD; +-- ERROR: could not create unique index "idx_emp_email" +-- DETAIL: Key (email)=(a@example.com) is duplicated. + +-- 清理重复数据后重建成功 +DELETE FROM emp WHERE id = 999; +ALTER INDEX idx_emp_email REBUILD; +-- 之后:索引重新可用,唯一性重新生效 +---- + +=== 普通 REINDEX 同样可以恢复 + +[source,sql] +---- +ALTER INDEX idx_emp_email UNUSABLE; +REINDEX INDEX idx_emp_email; +-- 无需使用 Oracle 专用语法,普通 REINDEX 也会清除 UNUSABLE 标记 +---- + +=== CLUSTER / REPLICA IDENTITY 拒绝使用 unusable 索引 + +一个已停止维护的索引可能缺失禁用期间发生变更的行,因此不能再被信任用作聚簇排序依据或复制身份识别索引: + +[source,sql] +---- +ALTER TABLE emp CLUSTER ON idx_emp_email; +ALTER INDEX idx_emp_email UNUSABLE; + +CLUSTER emp; +-- ERROR: cannot cluster on unusable index "idx_emp_email" + +ALTER INDEX idx_emp_email REBUILD; +CLUSTER emp; -- 恢复后重新可用 +---- + +同理,若某索引被 `ALTER TABLE ... REPLICA IDENTITY USING INDEX` 指定为复制身份索引,标记为 UNUSABLE 期间逻辑复制会跳过该索引(旧行镜像不再包含旧的主键/唯一键值,而不是给出陈旧或错误的值),REBUILD 后自动恢复正常。 + +== 错误处理 + +=== 只有父分区索引本身不支持——叶子分区索引照常可用 + +[source,sql] +---- +CREATE TABLE sales (id int, region text) PARTITION BY RANGE (id); +CREATE TABLE sales_p1 PARTITION OF sales FOR VALUES FROM (1) TO (1001); +CREATE INDEX idx_sales_id ON sales(id); -- 自动在 sales_p1 上创建并 attach 叶子索引 + +-- 对父索引本身操作会报错: +ALTER INDEX idx_sales_id UNUSABLE; +-- ERROR: ALTER INDEX ... UNUSABLE is not supported for partitioned indexes +-- HINT: Mark each partition's leaf index unusable individually. + +-- 但对叶子索引单独操作是设计上支持的,正常生效: +ALTER INDEX sales_p1_id_idx UNUSABLE; +ALTER INDEX sales_p1_id_idx REBUILD; +---- + +NOTE: 只有 `relkind = RELKIND_PARTITIONED_INDEX` 的根/父索引会被拒绝;每个分区自己的叶子物理索引(`relkind = RELKIND_INDEX`)完全不受此限制,按分区逐个禁用/重建正是官方错误提示推荐的用法。 + +=== 索引不存在 + +[source,sql] +---- +ALTER INDEX no_such_index UNUSABLE; +-- ERROR: relation "no_such_index" does not exist +---- + +=== 目标不是索引 + +[source,sql] +---- +ALTER INDEX emp UNUSABLE; -- emp 是表名而非索引名 +-- ERROR: "emp" is not an index +---- + +=== 在 PG_PARSER 模式下使用 + +[source,sql] +---- +SET compatible_db = PG_PARSER; +ALTER INDEX idx_emp_email UNUSABLE; +-- ERROR: syntax error at or near "UNUSABLE" +---- + +=== CLUSTER 拒绝使用 unusable 聚簇索引 + +[source,sql] +---- +ALTER TABLE emp CLUSTER ON idx_emp_email; +ALTER INDEX idx_emp_email UNUSABLE; +CLUSTER emp; +-- ERROR: cannot cluster on unusable index "idx_emp_email" +---- + +=== TOAST 表索引不允许 UNUSABLE + +一个表的 TOAST 索引用于查找超长字段的存储位置;禁用它会导致新插入的大字段值查不到,且没有任何提示。因此直接禁止: + +[source,sql] +---- +ALTER INDEX pg_toast.pg_toast_16537_index UNUSABLE; +-- ERROR: ALTER INDEX ... UNUSABLE is not supported for TOAST table indexes +---- + +=== ON CONFLICT 遇到 unusable 唯一索引时报错,而不是内部崩溃 + +[source,sql] +---- +CREATE TABLE t (id int UNIQUE, val text); +INSERT INTO t VALUES (1, 'a'); +ALTER INDEX t_id_key UNUSABLE; + +INSERT INTO t VALUES (1, 'b') ON CONFLICT (id) DO UPDATE SET val = excluded.val; +-- ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification + +ALTER INDEX t_id_key REBUILD; +INSERT INTO t VALUES (1, 'c') ON CONFLICT (id) DO UPDATE SET val = excluded.val; +-- 恢复正常 +---- + +=== 新建外键不能绑定到 unusable 的引用键 + +[source,sql] +---- +CREATE TABLE parent (id int PRIMARY KEY, val text); +ALTER INDEX parent_pkey UNUSABLE; + +CREATE TABLE child (id int REFERENCES parent); +-- ERROR: cannot use an unusable primary key for referenced table "parent" + +ALTER INDEX parent_pkey REBUILD; +CREATE TABLE child (id int REFERENCES parent); -- 恢复正常 +---- + +显式指定引用列(走命名唯一约束匹配而非主键查找)同样会被拒绝: + +[source,sql] +---- +CREATE TABLE parent2 (id int, email text UNIQUE); +ALTER INDEX parent2_email_key UNUSABLE; + +CREATE TABLE child2 (email text REFERENCES parent2(email)); +-- ERROR: there is no unique constraint matching given keys for referenced table "parent2" +---- + +=== USING INDEX 建约束拒绝 unusable 索引 + +[source,sql] +---- +CREATE TABLE t2 (id int, val text); +CREATE UNIQUE INDEX idx_t2_val ON t2(val); +ALTER INDEX idx_t2_val UNUSABLE; + +ALTER TABLE t2 ADD CONSTRAINT uq_val UNIQUE USING INDEX idx_t2_val; +-- ERROR: index "idx_t2_val" is unusable +---- + +=== amcheck 拒绝检查 unusable 索引(避免假阳性报告) + +[source,sql] +---- +CREATE TABLE t3 (id int PRIMARY KEY, val text); +ALTER INDEX t3_pkey UNUSABLE; + +SELECT bt_index_check('t3_pkey'); +-- ERROR: cannot check index "t3_pkey" +-- DETAIL: Index is unusable. +---- + +== 清理 + +[source,sql] +---- +-- 恢复索引可用状态: +ALTER INDEX idx_emp_email REBUILD; +-- 或者: +REINDEX INDEX idx_emp_email; +---- diff --git a/EN/modules/ROOT/nav.adoc b/EN/modules/ROOT/nav.adoc index 8194fcde..3c4c5676 100644 --- a/EN/modules/ROOT/nav.adoc +++ b/EN/modules/ROOT/nav.adoc @@ -30,6 +30,7 @@ ** xref:master/oracle_compatibility/with_function_procedure_en.adoc[21、WITH FUNCTION/PROCEDURE] ** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、ONLINE Parameter for CREATE INDEX] ** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG function] +** xref:master/oracle_compatibility/compat_alter_index_unusable_en.adoc[24、Alter Index Unusable] * Containerization and Cloud Service ** Containerization *** xref:master/containerization/k8s_deployment.adoc[K8S deployment] @@ -107,6 +108,7 @@ *** xref:master/compatibility_features_design/read_only_view.adoc[Read Only View] *** xref:master/compatibility_features_design/with_function_procedure_impl_en.adoc[WITH FUNCTION/PROCEDURE] *** xref:master/compatibility_features_design/create_index_online.adoc[ONLINE Parameter for CREATE INDEX] +*** xref:master/compatibility_features_design/alter_index_unusable_impl_en.adoc[Alter Index Unusable] ** Built-in Functions *** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] *** xref:master/oracle_builtin_functions/userenv.adoc[userenv] diff --git a/EN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl_en.adoc b/EN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl_en.adoc new file mode 100644 index 00000000..639ecd16 --- /dev/null +++ b/EN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl_en.adoc @@ -0,0 +1,355 @@ +:sectnums: +:sectnumlevels: 5 + += ALTER INDEX ... UNUSABLE Implementation Notes + +== Purpose + +This document explains in detail how the `ALTER INDEX ... UNUSABLE` feature in IvorySQL is implemented. This feature allows manually disabling an index in Oracle compatibility mode so that it is no longer chosen by the planner and no longer maintained by DML, implementing the `UNUSABLE` clause of the Oracle database `ALTER INDEX` statement. + +== Implementation Notes + +=== System Layering + +The implementation of `ALTER INDEX ... UNUSABLE` is divided into five layers: + +``` +┌───────────────────────────────────────────────────────────────────────┐ +│ Layer 1: Oracle Parser (ora_gram.y + ora_kwlist.h) │ +│ ─ Adds the UNUSABLE keyword (UNRESERVED_KEYWORD, BARE_LABEL) │ +│ ─ ALTER INDEX qualified_name UNUSABLE → OraAlterIndexUnusableStmt │ +└───────────────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────────────┐ +│ Layer 2: Statement Dispatch (tcop/utility.c) │ +│ ─ T_OraAlterIndexUnusableStmt appears alongside the existing │ +│ T_OraAlterIndexRebuildStmt in 4 places: read-only transaction │ +│ classification / ProcessUtilitySlow / CreateCommandTag / │ +│ GetCommandLogLevel │ +└───────────────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────────────┐ +│ Layer 3: Execution Layer (commands/indexcmds.c) │ +│ ─ ExecOraAlterIndexUnusable(): permission check, resolves and │ +│ locks the target index, rejects partitioned indexes, calls │ +│ index_set_unusable(indexOid, true) │ +└───────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ Layer 4: Catalog Layer (catalog/index.c + include/catalog/pg_index.h) │ +│ ─ Adds the pg_index.indisunusable column │ +│ ─ index_set_unusable(): a standalone catalog update function │ +│ structurally mirroring index_set_state_flags() │ +│ ─ index_create()'s values[] array is extended with an initial │ +│ value (false) for this column │ +│ ─ reindex_index() clears this flag as part of its existing │ +│ "repair index state" branch │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────────────┐ +│ Layer 5: Planner / Executor (plancat.c + BuildIndexInfo) │ +│ ─ get_relation_info() skips indexes with indisunusable set │ +│ ─ BuildIndexInfo() makes ii_ReadyForInserts reflect this flag as │ +│ well, so the existing skip logic in execIndexing.c naturally │ +│ covers it — no changes to the executor itself are needed │ +└───────────────────────────────────────────────────────────────────────┘ +``` + +=== Grammar and Parsing + +==== Keyword Registration + +`src/include/oracle_parser/ora_kwlist.h`: + +[source,c] +---- +PG_KEYWORD("until", UNTIL, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unusable", UNUSABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("update", UPDATE, UNRESERVED_KEYWORD, BARE_LABEL) +---- + +`UNUSABLE` is an unreserved keyword, inserted alphabetically alongside `REBUILD`/`ONLINE`/`TABLESPACE`/`PARALLEL`. It is added to the `%token` list as well as to both the `unreserved_keyword` and `bare_label_keyword` productions, allowing it to be used as a bare identifier/column label. + +==== Grammar Rule Extension + +In `src/backend/oracle_parser/ora_gram.y`, immediately next to the existing `REBUILD` rule, the following is added: + +[source,yacc] +---- +| ALTER INDEX qualified_name REBUILD rebuild_index_opt_list + { + OraAlterIndexRebuildStmt *n = makeNode(OraAlterIndexRebuildStmt); + n->relation = $3; + n->options = $5; + $$ = (Node *) n; + } +| ALTER INDEX qualified_name UNUSABLE + { + OraAlterIndexUnusableStmt *n = makeNode(OraAlterIndexUnusableStmt); + n->relation = $3; + $$ = (Node *) n; + } +---- + +Unlike REBUILD, `UNUSABLE` does not take an options list — Oracle's `UNUSABLE` clause itself accepts no parameters. + +=== AST Node Design + +Added to `src/include/nodes/parsenodes.h`: + +[source,c] +---- +/* + * Oracle-compatible ALTER INDEX ... UNUSABLE Statement + * + * Marks an index unusable: skipped by the planner and no longer + * maintained by DML, until restored by a REBUILD (or a plain REINDEX). + */ +typedef struct OraAlterIndexUnusableStmt +{ + NodeTag type; + RangeVar *relation; /* index to mark unusable */ +} OraAlterIndexUnusableStmt; +---- + +The copy/equal/out/read functions are auto-generated from this struct by `gen_node_support.pl` and require no manual maintenance (the same treatment as the existing `OraAlterIndexRebuildStmt`). + +=== Catalog Layer Design + +==== Why Add a New `pg_index` Column, Rather Than a reloption or Reusing `indisvalid`/`indisready` + +- **A reloption approach is not viable**: PostgreSQL's index reloptions are declared per access method (btree/gin/hash each maintain their own private option table); there is no generic "index-level" reloption kind. Bypassing AM validation and writing directly to `pg_class.reloptions` would cause `pg_dump` restores to fail on an undeclared key. +- **`indisvalid`/`indisready` cannot be reused**: both are tightly coupled to the multi-phase state machine of `CREATE/DROP INDEX CONCURRENTLY` (`index_set_state_flags()` has strict `Assert`s on state transitions); overloading their semantics would break the invariants of concurrent index builds. + +The final approach: add an independent `bool indisunusable` column, in `src/include/catalog/pg_index.h`: + +[source,c] +---- +bool indislive; /* is this index alive at all? */ +bool indisreplident; /* is this index the identity for replication? */ +bool indisunusable; /* manually disabled via ALTER INDEX ... + * UNUSABLE (Oracle compat); skipped by the + * planner and unmaintained by DML until + * REBUILD */ +---- + +The corresponding catalog version number in `catversion.h` is bumped. IvorySQL already has precedent for extending core catalogs directly (`pg_class.relhasrowid` for the ROWID feature, `pg_attribute.attisinvisible` for the invisible-column feature); this change follows the same pattern. + +`indisunusable` is a transient index *state* rather than a defining property, in the same category as `indisvalid`/`indisready`; `pg_dump` never exports state columns of this kind, so there is no need for extra `pg_dump` support code the way `attisinvisible` required. + +==== `index_set_unusable()`: A Standalone State-Change Function + +`src/backend/catalog/index.c`, right next to the existing `index_set_state_flags()`: + +[source,c] +---- +/* + * index_set_unusable - set or clear pg_index.indisunusable + * + * Oracle-compatible companion to index_set_state_flags(): flips the + * "manually disabled via ALTER INDEX ... UNUSABLE" bit. Unlike the + * indisvalid/indisready/indislive trio, this flag is not part of the + * CREATE/DROP INDEX CONCURRENTLY state machine, so no transition + * invariants need to be asserted here. + */ +void +index_set_unusable(Oid indexId, bool unusable) +{ + Relation pg_index; + HeapTuple indexTuple; + Form_pg_index indexForm; + + pg_index = table_open(IndexRelationId, RowExclusiveLock); + + indexTuple = SearchSysCacheCopy1(INDEXRELID, ObjectIdGetDatum(indexId)); + if (!HeapTupleIsValid(indexTuple)) + elog(ERROR, "cache lookup failed for index %u", indexId); + indexForm = (Form_pg_index) GETSTRUCT(indexTuple); + + if (indexForm->indisunusable != unusable) + { + indexForm->indisunusable = unusable; + CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple); + } + + table_close(pg_index, RowExclusiveLock); +} +---- + +`CatalogTupleUpdate()` handles cache invalidation on its own, so there is no need to call `CacheInvalidateRelcache` manually. + +=== Execution Layer Design + +==== `ExecOraAlterIndexUnusable()` + +`src/backend/commands/indexcmds.c`, right next to the existing `ExecOraAlterIndexRebuild()`: + +[source,c] +---- +void +ExecOraAlterIndexUnusable(ParseState *pstate, const OraAlterIndexUnusableStmt *stmt) +{ + ReindexParams params = {0}; + struct ReindexIndexCallbackState cbstate; + Oid indexOid; + char relkind; + + if (ORA_PARSER != compatible_db) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ALTER INDEX ... UNUSABLE is only available when compatible_db is oracle"))); + + /* + * Reuse the same parsing/locking approach as the non-CONCURRENTLY + * path of ExecOraAlterIndexRebuild(): AccessExclusiveLock on the + * index itself, ShareLock on the heap table (decided by the shared + * callback), with the permission check also handled by that + * callback. + */ + cbstate.params = params; + cbstate.locked_table_oid = InvalidOid; + + indexOid = RangeVarGetRelidExtended(stmt->relation, + AccessExclusiveLock, + 0, + RangeVarCallbackForReindexIndex, + &cbstate); + + relkind = get_rel_relkind(indexOid); + if (relkind == RELKIND_PARTITIONED_INDEX) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ALTER INDEX ... UNUSABLE is not supported for partitioned indexes"), + errhint("Mark each partition's leaf index unusable individually."))); + + index_set_unusable(indexOid, true); +} +---- + +`RangeVarCallbackForReindexIndex` is the shared callback already used by the existing REINDEX/REBUILD infrastructure; reusing it directly means the permission check (`pg_class_aclcheck(..., ACL_MAINTAIN)`), relkind validation, and heap-table lock ordering are all fully consistent with REBUILD/REINDEX, with no need to reimplement any of it. + +==== Planner: Skipping Unusable Indexes + +`src/backend/optimizer/util/plancat.c`, `get_relation_info()`: + +[source,c] +---- +if (!index->indisvalid) +{ + index_close(indexRelation, NoLock); + continue; +} + +/* + * Ignore indexes manually disabled via the Oracle-compatible + * ALTER INDEX ... UNUSABLE. This check is unconditional (not + * gated on the current session's compatible_db): indisunusable + * is a catalog fact about the index, not a per-session parser + * choice, and every session must honor it consistently to avoid + * planning against a stale/unmaintained index. + */ +if (index->indisunusable) +{ + index_close(indexRelation, NoLock); + continue; +} +---- + +==== Executor: Skipping Maintenance of Unusable Indexes + +`src/backend/catalog/index.c`, `BuildIndexInfo()`: + +[source,c] +---- +ii = makeIndexInfo(indexStruct->indnatts, + indexStruct->indnkeyatts, + index->rd_rel->relam, + RelationGetIndexExpressions(index), + RelationGetIndexPredicate(index), + indexStruct->indisunique, + indexStruct->indnullsnotdistinct, + /* + * A manually UNUSABLE index (Oracle compat) is treated + * as not ready for inserts, so DML stops maintaining + * it, same as an in-progress CREATE INDEX CONCURRENTLY. + */ + indexStruct->indisready && !indexStruct->indisunusable, + false, + index->rd_indam->amsummarizing, + indexStruct->indisexclusion && indexStruct->indisunique); +---- + +Once `ii_ReadyForInserts` is false, the existing `if (!indexInfo->ii_ReadyForInserts) continue;` logic in `execIndexing.c` naturally skips insert/update maintenance for that index — **no changes to the executor itself are required**. This is also why uniqueness checking automatically stops once a unique/primary key index becomes UNUSABLE: the check happens inside the index's own `aminsert` path and requires no additional coordination with `pg_constraint`. + +==== REBUILD / REINDEX Cleanup: Clearing the UNUSABLE Flag + +`src/backend/catalog/index.c`, in the existing "index state needs repair" branch of `reindex_index()`: + +[source,c] +---- +index_bad = (!indexForm->indisvalid || + !indexForm->indisready || + !indexForm->indislive || + indexForm->indisunusable); +if (index_bad || + (indexForm->indcheckxmin && !indexInfo->ii_BrokenHotChain)) +{ + ... + indexForm->indisvalid = true; + indexForm->indisready = true; + indexForm->indislive = true; + /* a completed non-concurrent rebuild always clears UNUSABLE */ + indexForm->indisunusable = false; + CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple); +} +---- + +This branch covers: a plain `REINDEX INDEX`, `ALTER INDEX ... REBUILD` (non-`ONLINE`), and `REBUILD PARTITION` (non-`ONLINE`). `REBUILD ONLINE` (the concurrent path, which goes through `ReindexRelationConcurrently()` and never calls `reindex_index()`) **does not** clear the flag — see "Known Limitations". + +=== Isolation Strategy from PG_PARSER + +Following the project's convention (`if (ORA_PARSER == compatible_db) { ... }`), this feature is isolated at two levels: + +1. **Natural isolation at the grammar level**: the `UNUSABLE` syntax exists only in `ora_gram.y`; `PG_PARSER` sessions have no grammar entry point for it at all. +2. **Defense in depth at the command entry point**: `ExecOraAlterIndexUnusable()` additionally guards with `if (ORA_PARSER != compatible_db) ereport(ERROR, ...)` as a backstop. + +=== Error Handling + +==== Partitioned Index Rejection + +[source,sql] +---- +ALTER INDEX idx_sales_id UNUSABLE; -- idx_sales_id is a partitioned parent index +-- ERROR: ALTER INDEX ... UNUSABLE is not supported for partitioned indexes +-- HINT: Mark each partition's leaf index unusable individually. +---- +The error is raised proactively by the `relkind == RELKIND_PARTITIONED_INDEX` branch in `ExecOraAlterIndexUnusable()`. + +==== Rejection Under PG_PARSER Mode + +[source,sql] +---- +SET compatible_db = PG_PARSER; +ALTER INDEX idx_emp_email UNUSABLE; +-- ERROR: syntax error at or near "UNUSABLE" +---- +At the grammar level, this production simply does not exist under `PG_PARSER`, so the error is raised without any runtime check. + +==== Rebuild Failure After Maintenance Was Skipped (Uniqueness Conflict) + +[source,sql] +---- +ALTER INDEX idx_emp_email UNUSABLE; +INSERT INTO emp VALUES (999, 'a@example.com'); -- duplicates an existing row, succeeds +ALTER INDEX idx_emp_email REBUILD; +-- ERROR: could not create unique index "idx_emp_email" +-- DETAIL: Key (email)=(a@example.com) is duplicated. +---- +This error comes from the standard uniqueness validation inside `index_build()`, called from within `reindex_index()` — it is not new logic added for this feature, but naturally inherited behavior. + +=== Known Limitations + +1. **No `USABLE` clause is supported**: consistent with Oracle's behavior, REBUILD is the only path to restoration; +2. **`REBUILD ONLINE` does not clear UNUSABLE**: the multi-transaction snapshot lifecycle of the concurrent rebuild path is incompatible with appending one additional standalone catalog update afterward; +3. **Only the root/parent partitioned index itself is unsupported**: running against `RELKIND_PARTITIONED_INDEX` raises an error; each partition's own leaf physical index is unaffected; +4. **The `SKIP_UNUSABLE_INDEXES` session parameter**: Oracle allows this parameter to control whether a query errors out when it encounters an unusable unique index; this is not supported. diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/compat_alter_index_unusable_en.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_alter_index_unusable_en.adoc new file mode 100644 index 00000000..ec6a0df6 --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_alter_index_unusable_en.adoc @@ -0,0 +1,275 @@ +:sectnums: +:sectnumlevels: 5 + += ALTER INDEX ... UNUSABLE + +== Purpose + +This document explains the purpose of `ALTER INDEX ... UNUSABLE` in IvorySQL, which implements Oracle-style manual index disabling. + +`UNUSABLE` is a clause of the Oracle database `ALTER INDEX` statement used to mark an index as unusable: the index object remains in the data dictionary, but stops participating in query optimization and DML maintenance. A typical use case is disabling index maintenance before a bulk data load to improve write performance, then rebuilding the index with `ALTER INDEX ... REBUILD` once the load is complete. + +== Feature Description + +=== Basic Syntax + +Under Oracle compatibility mode (`compatible_db = ORA_PARSER`), `ALTER INDEX` supports the following extended syntax: + +[source,sql] +---- +ALTER INDEX index_name UNUSABLE; +---- + +Real Oracle **has no `ALTER INDEX ... USABLE` clause** — the only documented way to restore usability is `ALTER INDEX ... REBUILD` (already supported by IvorySQL; see the separate document). IvorySQL follows this behavior and does not provide a `USABLE` clause. + +=== Core Characteristics + +- **Excluded by the planner**: An index marked UNUSABLE is no longer chosen by the planner as an access path — even with `SET enable_seqscan = off` explicitly set, it is completely excluded rather than merely deprioritized +- **DML maintenance stops**: INSERT/UPDATE no longer update the index's physical structure +- **Uniqueness checking stops accordingly**: If the index is a unique or primary key index, since the uniqueness check happens within the index's own insert path, duplicate values can be silently inserted once maintenance stops (consistent with Oracle's behavior) +- **Index definition and data are not dropped**: The index object, its definition, and its existing physical pages are all retained; they are simply no longer read or written +- **Only REBUILD restores it**: `ALTER INDEX ... REBUILD` (non-`ONLINE`) or a plain `REINDEX INDEX` physically rebuilds the index and automatically clears the UNUSABLE flag, restoring both planner visibility and DML maintenance +- **Partitioned indexes are not supported**: Running `UNUSABLE` against a `RELKIND_PARTITIONED_INDEX` raises an error; each leaf partition index must be operated on individually + +== Syntax Examples + +=== Basic Usage + +[source,sql] +---- +CREATE TABLE emp (id int PRIMARY KEY, email text); +CREATE UNIQUE INDEX idx_emp_email ON emp(email); + +ALTER INDEX idx_emp_email UNUSABLE; +-- Afterward: queries no longer use this index; INSERTing a duplicate email no longer raises an error +---- + +=== Bulk Load Workflow + +[source,sql] +---- +-- Disable index maintenance before loading +ALTER INDEX idx_emp_email UNUSABLE; + +-- Bulk load data (skips index maintenance overhead) +INSERT INTO emp SELECT * FROM staging_emp; + +-- Rebuild the index after loading to restore usability +ALTER INDEX idx_emp_email REBUILD; +---- + +=== Planner Exclusion Effect + +[source,sql] +---- +SET enable_seqscan = off; +EXPLAIN (COSTS OFF) SELECT * FROM emp WHERE email = 'a@example.com'; +-- QUERY PLAN +-- ------------------------------------------------------ +-- Index Only Scan using idx_emp_email on emp +-- Index Cond: (email = 'a@example.com'::text) + +ALTER INDEX idx_emp_email UNUSABLE; + +EXPLAIN (COSTS OFF) SELECT * FROM emp WHERE email = 'a@example.com'; +-- QUERY PLAN +-- ---------------------------------- +-- Seq Scan on emp +-- Disabled: true +-- Filter: (email = 'a@example.com'::text) +-- Even though Seq Scan itself is disabled via enable_seqscan=off, the planner has no other path available +RESET enable_seqscan; +---- + +=== Uniqueness Checking Stops + +[source,sql] +---- +ALTER INDEX idx_emp_email UNUSABLE; +INSERT INTO emp VALUES (999, 'a@example.com'); -- duplicates an existing row +-- Succeeds, no ERROR (maintenance has stopped, no conflict is detected) +---- + +=== REBUILD Restores Usability + +[source,sql] +---- +-- If data violating uniqueness was inserted while UNUSABLE, REBUILD will fail +ALTER INDEX idx_emp_email REBUILD; +-- ERROR: could not create unique index "idx_emp_email" +-- DETAIL: Key (email)=(a@example.com) is duplicated. + +-- After cleaning up the duplicate data, the rebuild succeeds +DELETE FROM emp WHERE id = 999; +ALTER INDEX idx_emp_email REBUILD; +-- Afterward: the index is usable again and uniqueness is enforced again +---- + +=== A Plain REINDEX Also Restores Usability + +[source,sql] +---- +ALTER INDEX idx_emp_email UNUSABLE; +REINDEX INDEX idx_emp_email; +-- No need for Oracle-specific syntax; a plain REINDEX also clears the UNUSABLE flag +---- + +=== CLUSTER / REPLICA IDENTITY Refuse to Use an Unusable Index + +An index whose maintenance has stopped may be missing rows changed while it was disabled, so it can no longer be trusted as a basis for cluster ordering or as the replica identity index: + +[source,sql] +---- +ALTER TABLE emp CLUSTER ON idx_emp_email; +ALTER INDEX idx_emp_email UNUSABLE; + +CLUSTER emp; +-- ERROR: cannot cluster on unusable index "idx_emp_email" + +ALTER INDEX idx_emp_email REBUILD; +CLUSTER emp; -- usable again after rebuilding +---- + +Likewise, if an index has been designated as the replica identity index via `ALTER TABLE ... REPLICA IDENTITY USING INDEX`, logical replication skips that index while it is marked UNUSABLE (the old-row image no longer includes the old primary/unique key values, rather than supplying stale or incorrect ones), and normal behavior resumes automatically after REBUILD. + +== Error Handling + +=== Only the Parent Partitioned Index Itself Is Unsupported — Leaf Partition Indexes Work as Usual + +[source,sql] +---- +CREATE TABLE sales (id int, region text) PARTITION BY RANGE (id); +CREATE TABLE sales_p1 PARTITION OF sales FOR VALUES FROM (1) TO (1001); +CREATE INDEX idx_sales_id ON sales(id); -- automatically creates and attaches a leaf index on sales_p1 + +-- Operating on the parent index itself raises an error: +ALTER INDEX idx_sales_id UNUSABLE; +-- ERROR: ALTER INDEX ... UNUSABLE is not supported for partitioned indexes +-- HINT: Mark each partition's leaf index unusable individually. + +-- But operating on a leaf index individually is supported by design and works normally: +ALTER INDEX sales_p1_id_idx UNUSABLE; +ALTER INDEX sales_p1_id_idx REBUILD; +---- + +NOTE: Only the root/parent index with `relkind = RELKIND_PARTITIONED_INDEX` is rejected; each partition's own leaf physical index (`relkind = RELKIND_INDEX`) is entirely unaffected by this restriction — disabling/rebuilding per partition is exactly the usage recommended by the official error hint. + +=== Index Does Not Exist + +[source,sql] +---- +ALTER INDEX no_such_index UNUSABLE; +-- ERROR: relation "no_such_index" does not exist +---- + +=== Target Is Not an Index + +[source,sql] +---- +ALTER INDEX emp UNUSABLE; -- emp is a table name, not an index name +-- ERROR: "emp" is not an index +---- + +=== Using It Under PG_PARSER Mode + +[source,sql] +---- +SET compatible_db = PG_PARSER; +ALTER INDEX idx_emp_email UNUSABLE; +-- ERROR: syntax error at or near "UNUSABLE" +---- + +=== CLUSTER Refuses to Use an Unusable Cluster Index + +[source,sql] +---- +ALTER TABLE emp CLUSTER ON idx_emp_email; +ALTER INDEX idx_emp_email UNUSABLE; +CLUSTER emp; +-- ERROR: cannot cluster on unusable index "idx_emp_email" +---- + +=== TOAST Table Indexes Do Not Allow UNUSABLE + +A table's TOAST index is used to locate the storage position of oversized field values; disabling it would make newly inserted large field values unretrievable, with no warning at all. It is therefore disallowed outright: + +[source,sql] +---- +ALTER INDEX pg_toast.pg_toast_16537_index UNUSABLE; +-- ERROR: ALTER INDEX ... UNUSABLE is not supported for TOAST table indexes +---- + +=== ON CONFLICT Raises an Error on an Unusable Unique Index, Instead of Crashing Internally + +[source,sql] +---- +CREATE TABLE t (id int UNIQUE, val text); +INSERT INTO t VALUES (1, 'a'); +ALTER INDEX t_id_key UNUSABLE; + +INSERT INTO t VALUES (1, 'b') ON CONFLICT (id) DO UPDATE SET val = excluded.val; +-- ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification + +ALTER INDEX t_id_key REBUILD; +INSERT INTO t VALUES (1, 'c') ON CONFLICT (id) DO UPDATE SET val = excluded.val; +-- Back to normal +---- + +=== A New Foreign Key Cannot Bind to an Unusable Referenced Key + +[source,sql] +---- +CREATE TABLE parent (id int PRIMARY KEY, val text); +ALTER INDEX parent_pkey UNUSABLE; + +CREATE TABLE child (id int REFERENCES parent); +-- ERROR: cannot use an unusable primary key for referenced table "parent" + +ALTER INDEX parent_pkey REBUILD; +CREATE TABLE child (id int REFERENCES parent); -- back to normal +---- + +Explicitly specifying the referenced column (which goes through named unique constraint matching rather than primary key lookup) is likewise rejected: + +[source,sql] +---- +CREATE TABLE parent2 (id int, email text UNIQUE); +ALTER INDEX parent2_email_key UNUSABLE; + +CREATE TABLE child2 (email text REFERENCES parent2(email)); +-- ERROR: there is no unique constraint matching given keys for referenced table "parent2" +---- + +=== USING INDEX Constraint Creation Refuses an Unusable Index + +[source,sql] +---- +CREATE TABLE t2 (id int, val text); +CREATE UNIQUE INDEX idx_t2_val ON t2(val); +ALTER INDEX idx_t2_val UNUSABLE; + +ALTER TABLE t2 ADD CONSTRAINT uq_val UNIQUE USING INDEX idx_t2_val; +-- ERROR: index "idx_t2_val" is unusable +---- + +=== amcheck Refuses to Check an Unusable Index (Avoiding False-Positive Reports) + +[source,sql] +---- +CREATE TABLE t3 (id int PRIMARY KEY, val text); +ALTER INDEX t3_pkey UNUSABLE; + +SELECT bt_index_check('t3_pkey'); +-- ERROR: cannot check index "t3_pkey" +-- DETAIL: Index is unusable. +---- + +== Cleanup + +[source,sql] +---- +-- Restore the index to a usable state: +ALTER INDEX idx_emp_email REBUILD; +-- Or: +REINDEX INDEX idx_emp_email; +----