From 615e99ebc45adc614366fda80f5d9d75cbaf6ea8 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Wed, 12 Aug 2026 16:06:45 +0000 Subject: [PATCH 2/2] Preserve statistics targets of indexes rebuilt by ALTER TABLE When ALTER TABLE ALTER COLUMN TYPE (or SET EXPRESSION) recreates the indexes that depend on the altered column, it replays each index's definition string through CREATE INDEX. A statistics target set on an expression column with ALTER INDEX ... ALTER COLUMN ... SET STATISTICS is not part of that definition string, so the target silently reverted to the default after the rebuild. The index of a rebuilt exclusion constraint lost its targets the same way. To fix, capture the per-column targets next to where ATPostAlterTypeParse() already captures the index's comment, carry them in a new IndexStmt field, and have DefineIndex() apply them once the index exists. DefineIndex() also propagates the targets to the indexes it creates on partitions, matching the recursion that ALTER INDEX ... SET STATISTICS performs on partitioned indexes. Targets set directly on a partition's index are still not preserved when the rebuild recurses from the parent, mirroring the pre-existing behavior for such indexes' comments. --- src/backend/commands/indexcmds.c | 70 +++++++++++++++++++++++ src/backend/commands/tablecmds.c | 60 +++++++++++++++++++ src/include/nodes/parsenodes.h | 1 + src/test/regress/expected/alter_table.out | 36 ++++++++++++ src/test/regress/sql/alter_table.sql | 26 +++++++++ 5 files changed, 193 insertions(+) diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 5d62fa7605b..d2b826e6b66 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -29,6 +29,7 @@ #include "catalog/indexing.h" #include "catalog/namespace.h" #include "catalog/pg_am.h" +#include "catalog/pg_attribute.h" #include "catalog/pg_authid.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" @@ -83,6 +84,7 @@ typedef struct CIEN_context /* non-export function prototypes */ static bool CompareOpclassOptions(const Datum *opts1, const Datum *opts2, int natts); static void CheckPredicate(Expr *predicate); +static void SetIndexStatTargets(Oid indexOid, List *stattargets); static void ComputeIndexAttrs(ParseState *pstate, IndexInfo *indexInfo, Oid *typeOids, @@ -1324,6 +1326,10 @@ DefineIndex(ParseState *pstate, CreateComments(indexRelationId, RelationRelationId, 0, stmt->idxcomment); + /* Apply any requested per-column statistics targets */ + if (stmt->stattargets != NIL) + SetIndexStatTargets(indexRelationId, stmt->stattargets); + if (partitioned) { PartitionDesc partdesc; @@ -1546,6 +1552,13 @@ DefineIndex(ParseState *pstate, attmap, NULL); + /* + * Propagate any per-column statistics targets to the + * child index, matching the recursion that ALTER INDEX + * ... SET STATISTICS performs on partitioned indexes. + */ + childStmt->stattargets = stmt->stattargets; + /* * Recurse as the starting user ID. Callee will use that * for permission checks, then switch again. @@ -1864,6 +1877,63 @@ DefineIndex(ParseState *pstate, return address; } +/* + * SetIndexStatTargets + * Apply per-column statistics targets to a just-created index. + * + * The list contains one integer per index column, with -1 meaning the + * target is not set for that column. It is captured from a previous + * incarnation of the index by ATPostAlterTypeParse(), so the column + * structure is known to match. + */ +static void +SetIndexStatTargets(Oid indexOid, List *stattargets) +{ + Relation attrelation; + AttrNumber attnum = 0; + ListCell *lc; + + attrelation = table_open(AttributeRelationId, RowExclusiveLock); + + foreach(lc, stattargets) + { + int target = lfirst_int(lc); + HeapTuple tuple, + newtuple; + Datum repl_val[Natts_pg_attribute]; + bool repl_null[Natts_pg_attribute]; + bool repl_repl[Natts_pg_attribute]; + + attnum++; + + if (target < 0) + continue; + + tuple = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(indexOid), + Int16GetDatum(attnum)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, indexOid); + + memset(repl_val, 0, sizeof(repl_val)); + memset(repl_null, false, sizeof(repl_null)); + memset(repl_repl, false, sizeof(repl_repl)); + + repl_val[Anum_pg_attribute_attstattarget - 1] = Int16GetDatum(target); + repl_repl[Anum_pg_attribute_attstattarget - 1] = true; + + newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrelation), + repl_val, repl_null, repl_repl); + CatalogTupleUpdate(attrelation, &tuple->t_self, newtuple); + + heap_freetuple(newtuple); + ReleaseSysCache(tuple); + } + + table_close(attrelation, RowExclusiveLock); +} + /* * CheckPredicate diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 16e89e848d9..e7c294cd0f0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -699,6 +699,7 @@ static void RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterT static void RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab); static void RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab); static void RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab); +static List *GetIndexStatTargets(Oid indexOid); static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode); static void ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, Oid ownerId, @@ -16361,6 +16362,61 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) */ } +/* + * Subroutine for ATPostAlterTypeParse(). Fetch the per-column statistics + * targets of the given index, as a list of integers with -1 standing for + * columns whose target is not set. Returns NIL if no column has a target, + * which is the common case. + */ +static List * +GetIndexStatTargets(Oid indexOid) +{ + List *result = NIL; + bool found = false; + HeapTuple reltup; + int natts; + + reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(indexOid)); + if (!HeapTupleIsValid(reltup)) + elog(ERROR, "cache lookup failed for relation %u", indexOid); + natts = ((Form_pg_class) GETSTRUCT(reltup))->relnatts; + ReleaseSysCache(reltup); + + for (AttrNumber attnum = 1; attnum <= natts; attnum++) + { + HeapTuple tuple; + Datum dat; + bool isnull; + + tuple = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(indexOid), + Int16GetDatum(attnum)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, indexOid); + + dat = SysCacheGetAttr(ATTNUM, tuple, + Anum_pg_attribute_attstattarget, &isnull); + if (isnull) + result = lappend_int(result, -1); + else + { + result = lappend_int(result, (int) DatumGetInt16(dat)); + found = true; + } + + ReleaseSysCache(tuple); + } + + if (!found) + { + list_free(result); + return NIL; + } + + return result; +} + /* * Parse the previously-saved definition string for a constraint, index or * statistics object against the newly-established column data type(s), and @@ -16453,6 +16509,8 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, Oid ownerId, stmt->reset_default_tblspc = true; /* keep the index's comment */ stmt->idxcomment = GetComment(oldId, RelationRelationId, 0); + /* keep any per-column statistics targets */ + stmt->stattargets = GetIndexStatTargets(oldId); newcmd = makeNode(AlterTableCmd); newcmd->subtype = AT_ReAddIndex; @@ -16482,6 +16540,8 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, Oid ownerId, /* keep any comment on the index */ indstmt->idxcomment = GetComment(indoid, RelationRelationId, 0); + /* keep any per-column statistics targets */ + indstmt->stattargets = GetIndexStatTargets(indoid); indstmt->reset_default_tblspc = true; cmd->subtype = AT_ReAddIndex; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index b99ac94cb3e..85f487f17ed 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3641,6 +3641,7 @@ typedef struct IndexStmt Node *whereClause; /* qualification (partial-index predicate) */ List *excludeOpNames; /* exclusion operator names, or NIL if none */ char *idxcomment; /* comment to apply to index, or NULL */ + List *stattargets; /* per-column statistics targets, or NIL */ Oid indexOid; /* OID of an existing index, if any */ RelFileNumber oldNumber; /* relfilenumber of existing storage, if any */ SubTransactionId oldCreateSubid; /* rd_createSubid of oldNumber */ diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index e167a41ce79..8f9a30d999c 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -119,6 +119,42 @@ HINT: Alter statistics on table column instead. ALTER INDEX attmp_idx ALTER COLUMN 4 SET STATISTICS 1000; ERROR: column number 4 of relation "attmp_idx" does not exist ALTER INDEX attmp_idx ALTER COLUMN 2 SET STATISTICS -1; +DROP TABLE attmp; +-- Check that index column statistics targets survive the index rebuilds +-- caused by ALTER TABLE ... ALTER COLUMN TYPE, including the rebuild of +-- an exclusion constraint's index and recursion to partitions. +CREATE TABLE attmp (a int, b int, EXCLUDE ((a + b) WITH =)); +CREATE INDEX attmp_expr_idx ON attmp ((a * b)); +ALTER INDEX attmp_expr_idx ALTER COLUMN 1 SET STATISTICS 100; +ALTER INDEX attmp_a_b_excl ALTER COLUMN 1 SET STATISTICS 200; +ALTER TABLE attmp ALTER COLUMN a TYPE bigint; +SELECT c.relname, a.attstattarget + FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid + WHERE c.relname IN ('attmp_expr_idx', 'attmp_a_b_excl') AND a.attnum = 1 + ORDER BY c.relname; + relname | attstattarget +----------------+--------------- + attmp_a_b_excl | 200 + attmp_expr_idx | 100 +(2 rows) + +DROP TABLE attmp; +CREATE TABLE attmp (a int, b int) PARTITION BY RANGE (b); +CREATE TABLE attmp1 PARTITION OF attmp FOR VALUES FROM (0) TO (100); +CREATE INDEX attmp_part_idx ON attmp ((a + 1)); +ALTER INDEX attmp_part_idx ALTER COLUMN 1 SET STATISTICS 100; +ALTER TABLE attmp ALTER COLUMN a TYPE bigint; +SELECT c.relname, a.attstattarget + FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid + WHERE a.attrelid IN ('attmp_part_idx'::regclass, 'attmp1_a_1_idx'::regclass) + AND a.attnum = 1 + ORDER BY c.relname; + relname | attstattarget +----------------+--------------- + attmp1_a_1_idx | 100 + attmp_part_idx | 100 +(2 rows) + DROP TABLE attmp; -- -- rename - check on both non-temp and temp tables diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index 9f6c2a4bb08..11d931a7b99 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -158,6 +158,32 @@ ALTER INDEX attmp_idx ALTER COLUMN 2 SET STATISTICS -1; DROP TABLE attmp; +-- Check that index column statistics targets survive the index rebuilds +-- caused by ALTER TABLE ... ALTER COLUMN TYPE, including the rebuild of +-- an exclusion constraint's index and recursion to partitions. +CREATE TABLE attmp (a int, b int, EXCLUDE ((a + b) WITH =)); +CREATE INDEX attmp_expr_idx ON attmp ((a * b)); +ALTER INDEX attmp_expr_idx ALTER COLUMN 1 SET STATISTICS 100; +ALTER INDEX attmp_a_b_excl ALTER COLUMN 1 SET STATISTICS 200; +ALTER TABLE attmp ALTER COLUMN a TYPE bigint; +SELECT c.relname, a.attstattarget + FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid + WHERE c.relname IN ('attmp_expr_idx', 'attmp_a_b_excl') AND a.attnum = 1 + ORDER BY c.relname; +DROP TABLE attmp; + +CREATE TABLE attmp (a int, b int) PARTITION BY RANGE (b); +CREATE TABLE attmp1 PARTITION OF attmp FOR VALUES FROM (0) TO (100); +CREATE INDEX attmp_part_idx ON attmp ((a + 1)); +ALTER INDEX attmp_part_idx ALTER COLUMN 1 SET STATISTICS 100; +ALTER TABLE attmp ALTER COLUMN a TYPE bigint; +SELECT c.relname, a.attstattarget + FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid + WHERE a.attrelid IN ('attmp_part_idx'::regclass, 'attmp1_a_1_idx'::regclass) + AND a.attnum = 1 + ORDER BY c.relname; +DROP TABLE attmp; + -- -- rename - check on both non-temp and temp tables -- 2.54.0