From 33c32daa99511c0e177f39e4cd8f95015192b0e5 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Tue, 11 Aug 2026 21:59:44 +0000 Subject: [PATCH 1/2] Preserve statistics target of extended statistics rebuilt by ALTER TABLE When ALTER TABLE ALTER COLUMN TYPE (or SET EXPRESSION) recreates the extended statistics objects that depend on the altered column, it does so by capturing each object's definition string and replaying it through CREATE STATISTICS. A statistics target set with ALTER STATISTICS ... SET STATISTICS is not part of that definition string, and CreateStatistics() always stored a null stxstattarget, so the target silently reverted to the default after the rebuild. This is the same class of problem as the statistics owner reset that commit 6713a6e04cb fixed, and the fix follows the same pattern: have RememberStatisticsForRebuilding() capture stxstattarget along with the owner, thread it through ATPostAlterTypeParse() into the recreated CreateStatsStmt, and make CreateStatistics() store it. CreateStatsStmt gains a stxstattarget field, with -1 meaning the default (null) target. --- src/backend/commands/statscmds.c | 6 +++- src/backend/commands/tablecmds.c | 37 +++++++++++++++++-------- src/backend/parser/gram.y | 2 ++ src/backend/parser/parse_utilcmd.c | 1 + src/include/nodes/parsenodes.h | 1 + src/test/regress/expected/stats_ext.out | 20 +++++++------ src/test/regress/sql/stats_ext.sql | 8 ++++-- 7 files changed, 51 insertions(+), 24 deletions(-) diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c index c4b5b478644..ee358c7dd92 100644 --- a/src/backend/commands/statscmds.c +++ b/src/backend/commands/statscmds.c @@ -563,7 +563,11 @@ CreateStatistics(List *relids, CreateStatsStmt *stmt, bool check_rights) values[Anum_pg_statistic_ext_stxnamespace - 1] = ObjectIdGetDatum(namespaceId); values[Anum_pg_statistic_ext_stxowner - 1] = ObjectIdGetDatum(stxowner); values[Anum_pg_statistic_ext_stxkeys - 1] = PointerGetDatum(stxkeys); - nulls[Anum_pg_statistic_ext_stxstattarget - 1] = true; + if (stmt->stxstattarget >= 0) + values[Anum_pg_statistic_ext_stxstattarget - 1] = + Int16GetDatum(stmt->stxstattarget); + else + nulls[Anum_pg_statistic_ext_stxstattarget - 1] = true; values[Anum_pg_statistic_ext_stxkind - 1] = PointerGetDatum(stxkind); values[Anum_pg_statistic_ext_stxexprs - 1] = exprsDatum; diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 6857315c071..16e89e848d9 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -212,6 +212,7 @@ typedef struct AlteredTableInfo List *changedStatisticsOids; /* OIDs of statistics to rebuild */ List *changedStatisticsDefs; /* string definitions of same */ List *changedStatisticsOwners; /* owners of same */ + List *changedStatisticsTargets; /* stxstattarget of same, or -1 */ } AlteredTableInfo; /* Struct describing one new constraint to check in Phase 3 scan */ @@ -701,8 +702,8 @@ static void RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab); static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode); static void ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, Oid ownerId, - char *cmd, List **wqueue, LOCKMODE lockmode, - bool rewrite); + int stxstattarget, char *cmd, List **wqueue, + LOCKMODE lockmode, bool rewrite); static void RebuildConstraintComment(AlteredTableInfo *tab, AlterTablePass pass, Oid objid, Relation rel, List *domname, const char *conname); @@ -16134,6 +16135,8 @@ RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab) char *defstring = pg_get_statisticsobjdef_string(stxoid); HeapTuple tup; Form_pg_statistic_ext statext; + Datum target; + bool targetisnull; tup = SearchSysCache1(STATEXTOID, ObjectIdGetDatum(stxoid)); @@ -16142,6 +16145,10 @@ RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab) statext = (Form_pg_statistic_ext) GETSTRUCT(tup); + target = SysCacheGetAttr(STATEXTOID, tup, + Anum_pg_statistic_ext_stxstattarget, + &targetisnull); + tab->changedStatisticsOids = lappend_oid(tab->changedStatisticsOids, stxoid); tab->changedStatisticsDefs = lappend(tab->changedStatisticsDefs, @@ -16150,6 +16157,10 @@ RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab) tab->changedStatisticsOwners = lappend_oid(tab->changedStatisticsOwners, statext->stxowner); + tab->changedStatisticsTargets = + lappend_int(tab->changedStatisticsTargets, + targetisnull ? -1 : (int) DatumGetInt16(target)); + ReleaseSysCache(tup); } } @@ -16169,6 +16180,7 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) ListCell *def_item; ListCell *oid_item; ListCell *owner_item; + ListCell *target_item; /* * Collect all the constraints and indexes to drop so we can process them @@ -16242,7 +16254,7 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) if (relid != tab->relid) LockRelationOid(relid, AccessExclusiveLock); - ATPostAlterTypeParse(oldId, relid, confrelid, InvalidOid, + ATPostAlterTypeParse(oldId, relid, confrelid, InvalidOid, -1, (char *) lfirst(def_item), wqueue, lockmode, tab->rewrite); } @@ -16261,7 +16273,7 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) if (relid != tab->relid) LockRelationOid(relid, AccessExclusiveLock); - ATPostAlterTypeParse(oldId, relid, InvalidOid, InvalidOid, + ATPostAlterTypeParse(oldId, relid, InvalidOid, InvalidOid, -1, (char *) lfirst(def_item), wqueue, lockmode, tab->rewrite); @@ -16270,9 +16282,10 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) } /* add dependencies for new statistics */ - forthree(oid_item, tab->changedStatisticsOids, - def_item, tab->changedStatisticsDefs, - owner_item, tab->changedStatisticsOwners) + forfour(oid_item, tab->changedStatisticsOids, + def_item, tab->changedStatisticsDefs, + owner_item, tab->changedStatisticsOwners, + target_item, tab->changedStatisticsTargets) { Oid oldId = lfirst_oid(oid_item); Oid relid; @@ -16293,8 +16306,9 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) LockRelationOid(relid, ShareUpdateExclusiveLock); ATPostAlterTypeParse(oldId, relid, InvalidOid, lfirst_oid(owner_item), - (char *) lfirst(def_item), - wqueue, lockmode, tab->rewrite); + lfirst_int(target_item), + (char *) lfirst(def_item), wqueue, lockmode, + tab->rewrite); ObjectAddressSet(obj, StatisticExtRelationId, oldId); add_exact_object_address(&obj, objects); @@ -16357,8 +16371,8 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) */ static void ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, Oid ownerId, - char *cmd, List **wqueue, LOCKMODE lockmode, - bool rewrite) + int stxstattarget, char *cmd, List **wqueue, + LOCKMODE lockmode, bool rewrite) { List *raw_parsetree_list; List *querytree_list; @@ -16403,6 +16417,7 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, Oid ownerId, csstmt = transformStatsStmt(oldRelId, (CreateStatsStmt *) stmt, cmd); csstmt->owner = ownerId; + csstmt->stxstattarget = stxstattarget; querytree_list = lappend(querytree_list, csstmt); } diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 17035fb4d15..d3370055ec3 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -4900,6 +4900,7 @@ CreateStatsStmt: n->relations = $8; n->stxcomment = NULL; n->if_not_exists = false; + n->stxstattarget = -1; $$ = (Node *) n; } | CREATE STATISTICS IF_P NOT EXISTS any_name @@ -4913,6 +4914,7 @@ CreateStatsStmt: n->relations = $11; n->stxcomment = NULL; n->if_not_exists = true; + n->stxstattarget = -1; $$ = (Node *) n; } ; diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 2fe7b86761d..565ff2b848d 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -2172,6 +2172,7 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid, stats->stxcomment = NULL; stats->transformed = true; /* don't need transformStatsStmt again */ stats->if_not_exists = false; + stats->stxstattarget = -1; /* Clean up */ ReleaseSysCache(ht_stats); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8a9df884276..b99ac94cb3e 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3675,6 +3675,7 @@ typedef struct CreateStatsStmt bool transformed; /* true when transformStatsStmt is finished */ bool if_not_exists; /* do nothing if stats name already exists */ Oid owner; /* OID of owner, or InvalidOid for default */ + int stxstattarget; /* statistics target, or -1 for default */ } CreateStatsStmt; /* diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out index 4512670d525..bb878a33b93 100644 --- a/src/test/regress/expected/stats_ext.out +++ b/src/test/regress/expected/stats_ext.out @@ -3589,18 +3589,20 @@ CREATE TABLE stats_ext_tbl (a int, b int); ALTER TABLE stats_ext_tbl OWNER TO regress_relowner; CREATE STATISTICS tst ON a, b FROM stats_ext_tbl; ALTER STATISTICS tst OWNER TO regress_stxowner; -SELECT stxowner::regrole FROM pg_statistic_ext WHERE stxname = 'tst'; - stxowner ------------------- - regress_stxowner +ALTER STATISTICS tst SET STATISTICS 1000; +SELECT stxowner::regrole, stxstattarget FROM pg_statistic_ext WHERE stxname = 'tst'; + stxowner | stxstattarget +------------------+--------------- + regress_stxowner | 1000 (1 row) --- re-creating statistics via ALTER TABLE preserve the statistics owner. +-- re-creating statistics via ALTER TABLE preserve the statistics owner +-- and target. ALTER TABLE stats_ext_tbl ALTER COLUMN a TYPE bigint; -SELECT stxowner::regrole FROM pg_statistic_ext WHERE stxname = 'tst'; - stxowner ------------------- - regress_stxowner +SELECT stxowner::regrole, stxstattarget FROM pg_statistic_ext WHERE stxname = 'tst'; + stxowner | stxstattarget +------------------+--------------- + regress_stxowner | 1000 (1 row) -- Tidy up diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql index a9176e74f3c..a9bdb3a62df 100644 --- a/src/test/regress/sql/stats_ext.sql +++ b/src/test/regress/sql/stats_ext.sql @@ -1839,11 +1839,13 @@ CREATE TABLE stats_ext_tbl (a int, b int); ALTER TABLE stats_ext_tbl OWNER TO regress_relowner; CREATE STATISTICS tst ON a, b FROM stats_ext_tbl; ALTER STATISTICS tst OWNER TO regress_stxowner; -SELECT stxowner::regrole FROM pg_statistic_ext WHERE stxname = 'tst'; +ALTER STATISTICS tst SET STATISTICS 1000; +SELECT stxowner::regrole, stxstattarget FROM pg_statistic_ext WHERE stxname = 'tst'; --- re-creating statistics via ALTER TABLE preserve the statistics owner. +-- re-creating statistics via ALTER TABLE preserve the statistics owner +-- and target. ALTER TABLE stats_ext_tbl ALTER COLUMN a TYPE bigint; -SELECT stxowner::regrole FROM pg_statistic_ext WHERE stxname = 'tst'; +SELECT stxowner::regrole, stxstattarget FROM pg_statistic_ext WHERE stxname = 'tst'; -- Tidy up DROP TABLE stats_ext_tbl; -- 2.54.0