From 715b0b12fd3d188a9bded517a5e61c3479a4c1fb Mon Sep 17 00:00:00 2001 From: Alexandra Wang Date: Mon, 3 Aug 2026 15:59:52 -0700 Subject: [PATCH v9 4/4] Improve auto-generated names for extended statistics When CREATE STATISTICS is used without an explicit name, the name is generated from the columns and expressions the object is defined on. Previously only a bare, unqualified column reference contributed its name; a column written table-qualified (t.a) or wrapped in parentheses ((a)) was treated as an expression and contributed a fixed "expr". This was especially unhelpful for join statistics, whose columns are written table-qualified: an unnamed object on (t1.b, t1.c, t2.e, t2.f) came out as t1_expr_expr_expr_expr_stat, saying nothing about the columns involved. Resolve a simple column reference to its column name regardless of how it was written, so a column written "(a)" or "t.a" contributes "a" just as "a" does; a more complex expression still contributes "expr". For a join statistics object each column is additionally qualified with its relation name, since a bare column name would not indicate which relation it came from. The join object above is now named t1_b_t1_c_t2_e_t2_f_stat. Single-table names change too where a column was parenthesized or table-qualified: for example "CREATE STATISTICS ON (a), (b) FROM t" is now t_a_b_stat instead of t_expr_expr_stat. Reported-by: Tomas Vondra Discussion: https://postgr.es/m/02dd92ff-b1ee-4e69-9f7c-abc60dfb4142@vondra.me --- src/backend/commands/statscmds.c | 62 ++++++++++++++++--- src/test/regress/expected/stats_ext.out | 25 ++++++++ .../regress/expected/stats_ext_crossrel.out | 23 +++++++ src/test/regress/sql/stats_ext.sql | 24 +++++++ src/test/regress/sql/stats_ext_crossrel.sql | 22 +++++++ 5 files changed, 148 insertions(+), 8 deletions(-) diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c index bbaef34ed8d..ab00c99917f 100644 --- a/src/backend/commands/statscmds.c +++ b/src/backend/commands/statscmds.c @@ -46,7 +46,8 @@ static char *ChooseExtendedStatisticName(const char *name1, const char *name2, const char *label, Oid namespaceid); -static char *ChooseExtendedStatisticNameAddition(List *exprs); +static char *ChooseExtendedStatisticNameAddition(List *exprs, Oid relid, + List *joinrels); /* * CREATE STATISTICS @@ -500,10 +501,25 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights) else { namespaceId = RelationGetNamespace(rel); - namestr = ChooseExtendedStatisticName(RelationGetRelationName(rel), - ChooseExtendedStatisticNameAddition(stmt->exprs), - "stat", - namespaceId); + + /* + * Join stats have no single relation to use as a name prefix, so the + * qualified column list is the whole name. + */ + if (isjoin) + namestr = ChooseExtendedStatisticName(ChooseExtendedStatisticNameAddition(stmt->exprs, + relid, + stmt->stxjoinrels), + NULL, + "stat", + namespaceId); + else + namestr = ChooseExtendedStatisticName(RelationGetRelationName(rel), + ChooseExtendedStatisticNameAddition(stmt->exprs, + relid, + NIL), + "stat", + namespaceId); } namestrcpy(&stxname, namestr); @@ -1161,6 +1177,10 @@ ChooseExtendedStatisticName(const char *name1, const char *name2, * names for it. This will be passed to ChooseExtendedStatisticName along * with the parent table name and a suitable label. * + * For a join statistics object (joinrels != NIL) each column is qualified with + * its relation name and the result is used as "name1" instead, since a bare + * column name would not identify which relation it came from. + * * We know that less than NAMEDATALEN characters will actually be used, * so we can truncate the result once we've generated that many. * @@ -1168,7 +1188,7 @@ ChooseExtendedStatisticName(const char *name1, const char *name2, * ChooseIndexNameAddition. */ static char * -ChooseExtendedStatisticNameAddition(List *exprs) +ChooseExtendedStatisticNameAddition(List *exprs, Oid relid, List *joinrels) { char buf[NAMEDATALEN * 2]; int buflen = 0; @@ -1178,6 +1198,7 @@ ChooseExtendedStatisticNameAddition(List *exprs) foreach(lc, exprs) { StatsElem *selem = (StatsElem *) lfirst(lc); + char qualbuf[NAMEDATALEN * 2]; const char *name; /* It should be one of these, but just skip if it happens not to be */ @@ -1186,8 +1207,30 @@ ChooseExtendedStatisticNameAddition(List *exprs) name = selem->name; - if (buflen > 0) - buf[buflen++] = '_'; /* insert _ between names */ + /* + * A simple column reference resolves to a bare Var; use the column + * name, qualified with the relation name for a join object. + */ + if (!name && selem->expr && IsA(selem->expr, Var)) + { + Var *var = (Var *) selem->expr; + Oid colrelid = joinrels ? list_nth_oid(joinrels, var->varno - 1) : relid; + + name = get_attname(colrelid, var->varattno, true); + + if (name && joinrels) + { + char *relname = get_rel_name(colrelid); + + if (relname) + { + snprintf(qualbuf, sizeof(qualbuf), "%s_%s", relname, name); + name = qualbuf; + } + else + name = NULL; /* defensive: fall back to "expr" */ + } + } /* * We use fixed 'expr' for expressions, which have empty column names. @@ -1198,6 +1241,9 @@ ChooseExtendedStatisticNameAddition(List *exprs) if (!name) name = "expr"; + if (buflen > 0) + buf[buflen++] = '_'; /* insert _ between names */ + /* * At this point we have buflen <= NAMEDATALEN. name should be less * than NAMEDATALEN already, but use strlcpy for paranoia. diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out index c6a52b71d57..8744f5cdbf0 100644 --- a/src/test/regress/expected/stats_ext.out +++ b/src/test/regress/expected/stats_ext.out @@ -3840,3 +3840,28 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM declared_order_grouping (1 row) DROP TABLE declared_order_grouping; +-- +-- Auto-generated statistics object names. +-- +-- A simple column reference contributes the column name, whether written +-- unqualified, table-qualified, or wrapped in parens; a more complex expression +-- contributes a fixed "expr". +CREATE TABLE auto_t1 (a INTEGER, b INTEGER, c INTEGER); +-- Unqualified columns: bare column names. +CREATE STATISTICS ON b, c FROM auto_t1; +-- Table-qualified columns: still the column names. +CREATE STATISTICS ON auto_t1.a, auto_t1.b FROM auto_t1; +-- A parenthesized column and a more complex expression: the column resolves +-- to its name, the expression uses the fixed "expr". +CREATE STATISTICS ON (a), (b + c) FROM auto_t1; +SELECT stxname FROM pg_statistic_ext + WHERE stxrelid = 'auto_t1'::regclass + ORDER BY stxname; + stxname +--------------------- + auto_t1_a_b_stat + auto_t1_a_expr_stat + auto_t1_b_c_stat +(3 rows) + +DROP TABLE auto_t1; diff --git a/src/test/regress/expected/stats_ext_crossrel.out b/src/test/regress/expected/stats_ext_crossrel.out index 63db40f1974..3aab6da7306 100644 --- a/src/test/regress/expected/stats_ext_crossrel.out +++ b/src/test/regress/expected/stats_ext_crossrel.out @@ -2058,3 +2058,26 @@ DROP OPERATOR <<< (int4, int4); DROP FUNCTION op_leak(int, int); DROP TABLE leak_anchor, leak_dim; DROP ROLE regress_join_leak_user; +-- +-- Auto-generated statistics object name for a join statistics object. +-- +-- A join statistics object qualifies each column with its relation name, since +-- a bare column name would not say which relation it came from. (Single-table +-- naming is covered in stats_ext.) +CREATE TABLE auto_t1 (a INTEGER, b INTEGER, c INTEGER); +CREATE TABLE auto_t2 (d INTEGER, e INTEGER, f INTEGER); +CREATE INDEX ON auto_t1 (a); +CREATE INDEX ON auto_t2 (d); +-- Every column is qualified with its relation, including two columns from the +-- same relation. +CREATE STATISTICS (mcv) ON auto_t1.b, auto_t1.c, auto_t2.e, auto_t2.f + FROM auto_t1 JOIN auto_t2 ON (auto_t1.a = auto_t2.d); +SELECT stxname FROM pg_statistic_ext + WHERE stxrelid IN ('auto_t1'::regclass, 'auto_t2'::regclass) + ORDER BY stxname; + stxname +---------------------------------------------- + auto_t1_b_auto_t1_c_auto_t2_e_auto_t2_f_stat +(1 row) + +DROP TABLE auto_t1, auto_t2; diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql index 33844520303..41493300c3d 100644 --- a/src/test/regress/sql/stats_ext.sql +++ b/src/test/regress/sql/stats_ext.sql @@ -1983,3 +1983,27 @@ ANALYZE declared_order_grouping; -- estimate should track the 100 distinct groups, not ndistinct(a) * ndistinct(b) SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM declared_order_grouping GROUP BY a, b'); DROP TABLE declared_order_grouping; + +-- +-- Auto-generated statistics object names. +-- +-- A simple column reference contributes the column name, whether written +-- unqualified, table-qualified, or wrapped in parens; a more complex expression +-- contributes a fixed "expr". +CREATE TABLE auto_t1 (a INTEGER, b INTEGER, c INTEGER); + +-- Unqualified columns: bare column names. +CREATE STATISTICS ON b, c FROM auto_t1; + +-- Table-qualified columns: still the column names. +CREATE STATISTICS ON auto_t1.a, auto_t1.b FROM auto_t1; + +-- A parenthesized column and a more complex expression: the column resolves +-- to its name, the expression uses the fixed "expr". +CREATE STATISTICS ON (a), (b + c) FROM auto_t1; + +SELECT stxname FROM pg_statistic_ext + WHERE stxrelid = 'auto_t1'::regclass + ORDER BY stxname; + +DROP TABLE auto_t1; diff --git a/src/test/regress/sql/stats_ext_crossrel.sql b/src/test/regress/sql/stats_ext_crossrel.sql index 8fa504044fd..7573dce7fff 100644 --- a/src/test/regress/sql/stats_ext_crossrel.sql +++ b/src/test/regress/sql/stats_ext_crossrel.sql @@ -1480,3 +1480,25 @@ DROP OPERATOR <<< (int4, int4); DROP FUNCTION op_leak(int, int); DROP TABLE leak_anchor, leak_dim; DROP ROLE regress_join_leak_user; + +-- +-- Auto-generated statistics object name for a join statistics object. +-- +-- A join statistics object qualifies each column with its relation name, since +-- a bare column name would not say which relation it came from. (Single-table +-- naming is covered in stats_ext.) +CREATE TABLE auto_t1 (a INTEGER, b INTEGER, c INTEGER); +CREATE TABLE auto_t2 (d INTEGER, e INTEGER, f INTEGER); +CREATE INDEX ON auto_t1 (a); +CREATE INDEX ON auto_t2 (d); + +-- Every column is qualified with its relation, including two columns from the +-- same relation. +CREATE STATISTICS (mcv) ON auto_t1.b, auto_t1.c, auto_t2.e, auto_t2.f + FROM auto_t1 JOIN auto_t2 ON (auto_t1.a = auto_t2.d); + +SELECT stxname FROM pg_statistic_ext + WHERE stxrelid IN ('auto_t1'::regclass, 'auto_t2'::regclass) + ORDER BY stxname; + +DROP TABLE auto_t1, auto_t2; -- 2.50.1 (Apple Git-155)