v8-0001-Remove-stxkeys-from-pg_statistic_ext-unify-into-s.patch
application/octet-stream
Filename: v8-0001-Remove-stxkeys-from-pg_statistic_ext-unify-into-s.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v8-0001
Subject: Remove stxkeys from pg_statistic_ext, unify into stxexprs
| File | + | − |
|---|---|---|
| doc/src/sgml/catalogs.sgml | 3 | 15 |
| doc/src/sgml/perform.sgml | 55 | 53 |
| doc/src/sgml/system-views.sgml | 2 | 11 |
| src/backend/catalog/system_views.sql | 1 | 6 |
| src/backend/commands/statscmds.c | 61 | 126 |
| src/backend/optimizer/util/plancat.c | 24 | 48 |
| src/backend/parser/parse_utilcmd.c | 26 | 39 |
| src/backend/statistics/dependencies.c | 6 | 27 |
| src/backend/statistics/extended_stats.c | 73 | 34 |
| src/backend/statistics/extended_stats_funcs.c | 33 | 55 |
| src/backend/statistics/mvdistinct.c | 4 | 25 |
| src/backend/utils/adt/ruleutils.c | 150 | 133 |
| src/bin/psql/describe.c | 17 | 3 |
| src/include/catalog/pg_proc.dat | 2 | 2 |
| src/include/catalog/pg_statistic_ext.h | 4 | 10 |
| src/include/statistics/extended_stats_internal.h | 5 | 2 |
| src/include/statistics/statistics.h | 2 | 0 |
| src/test/regress/expected/create_table_like.out | 2 | 7 |
| src/test/regress/expected/oidjoins.out | 0 | 1 |
| src/test/regress/expected/rules.out | 1 | 4 |
| src/test/regress/expected/stats_ext.out | 34 | 5 |
| src/test/regress/expected/stats_import.out | 70 | 2 |
| src/test/regress/sql/create_table_like.sql | 2 | 7 |
| src/test/regress/sql/stats_ext.sql | 15 | 1 |
| src/test/regress/sql/stats_import.sql | 60 | 0 |
From 2591a15ccf957fba91cc7ef00220802a0bd86788 Mon Sep 17 00:00:00 2001
From: Alexandra Wang <alexandra.wang.oss@gmail.com>
Date: Mon, 8 Jun 2026 12:00:00 -0700
Subject: [PATCH v8 1/2] Remove stxkeys from pg_statistic_ext, unify into
stxexprs
Remove the stxkeys int2vector column from pg_statistic_ext. All target
columns are now represented as Var nodes in stxexprs alongside complex
expressions. This simplifies the catalog representation and preserves
the user-written column order, which was previously lost because columns
were sorted by attnum.
The pg_stats_ext view is updated accordingly: the old attnames column is
removed, and the exprs column (now text[]) shows all targets via a
revised pg_get_statisticsobjdef_columns() function. Documentation in
catalogs.sgml, perform.sgml, and system-views.sgml updated to match.
At planning, ANALYZE, and stats-import time, the unified stxexprs list
is split back into a keys Bitmapset (plain Var nodes) and an exprs list
(everything else). Virtual generated columns are an exception: although
stored as Vars, they are classified as expressions because they receive
per-expression statistics.
This is a prerequisite refactor for the join statistics patch.
Suggested-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/711247.1779913876@sss.pgh.pa.us
---
doc/src/sgml/catalogs.sgml | 18 +-
doc/src/sgml/perform.sgml | 108 +++----
doc/src/sgml/system-views.sgml | 13 +-
src/backend/catalog/system_views.sql | 7 +-
src/backend/commands/statscmds.c | 187 ++++--------
src/backend/optimizer/util/plancat.c | 72 ++---
src/backend/parser/parse_utilcmd.c | 65 ++--
src/backend/statistics/dependencies.c | 33 +-
src/backend/statistics/extended_stats.c | 107 ++++---
src/backend/statistics/extended_stats_funcs.c | 88 ++----
src/backend/statistics/mvdistinct.c | 29 +-
src/backend/utils/adt/ruleutils.c | 283 ++++++++++--------
src/bin/psql/describe.c | 20 +-
src/include/catalog/pg_proc.dat | 4 +-
src/include/catalog/pg_statistic_ext.h | 14 +-
.../statistics/extended_stats_internal.h | 7 +-
src/include/statistics/statistics.h | 2 +
.../regress/expected/create_table_like.out | 9 +-
src/test/regress/expected/oidjoins.out | 1 -
src/test/regress/expected/rules.out | 5 +-
src/test/regress/expected/stats_ext.out | 39 ++-
src/test/regress/expected/stats_import.out | 72 ++++-
src/test/regress/sql/create_table_like.sql | 9 +-
src/test/regress/sql/stats_ext.sql | 16 +-
src/test/regress/sql/stats_import.sql | 60 ++++
25 files changed, 652 insertions(+), 616 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4b474c13917..f6f376d5281 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -8323,18 +8323,6 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para></entry>
</row>
- <row>
- <entry role="catalog_table_entry"><para role="column_definition">
- <structfield>stxkeys</structfield> <type>int2vector</type>
- (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
- </para>
- <para>
- An array of attribute numbers, indicating which table columns are
- covered by this statistics object;
- for example a value of <literal>1 3</literal> would
- mean that the first and the third table columns are covered
- </para></entry>
- </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
@@ -8373,9 +8361,9 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para>
<para>
Expression trees (in <function>nodeToString()</function>
- representation) for statistics object attributes that are not simple
- column references. This is a list with one element per expression.
- Null if all statistics object attributes are simple references.
+ representation) for all statistics object targets. Both column
+ references and expressions are stored here, with one element per
+ column or expression, in user-written order.
</para></entry>
</row>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 604e8578a8d..c1410a9e1e3 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -1541,27 +1541,28 @@ CREATE STATISTICS stts (dependencies) ON city, zip FROM zipcodes;
ANALYZE zipcodes;
-SELECT stxkeys AS k, jsonb_pretty(stxddependencies::text::jsonb) AS dep
+SELECT pg_get_statisticsobjdef_columns(oid) AS cols,
+ jsonb_pretty(stxddependencies::text::jsonb) AS dep
FROM pg_statistic_ext JOIN pg_statistic_ext_data ON (oid = stxoid)
WHERE stxname = 'stts';
--[ RECORD 1 ]--------------------
-k | 1 5
-dep | [ +
- | { +
- | "degree": 1.000000,+
- | "attributes": [ +
- | 1 +
- | ], +
- | "dependency": 5 +
- | }, +
- | { +
- | "degree": 0.423130,+
- | "attributes": [ +
- | 5 +
- | ], +
- | "dependency": 1 +
- | } +
- | ]
+-[ RECORD 1 ]---------------------
+cols | {city,zip}
+dep | [ +
+ | { +
+ | "degree": 1.000000, +
+ | "attributes": [ +
+ | 1 +
+ | ], +
+ | "dependency": 5 +
+ | }, +
+ | { +
+ | "degree": 0.423130, +
+ | "attributes": [ +
+ | 5 +
+ | ], +
+ | "dependency": 1 +
+ | } +
+ | ]
(1 row)
</programlisting>
Here it can be seen that column 1 (zip code) fully determines column
@@ -1647,42 +1648,43 @@ CREATE STATISTICS stts2 (ndistinct) ON city, state, zip FROM zipcodes;
ANALYZE zipcodes;
-SELECT stxkeys AS k, jsonb_pretty(stxdndistinct::text::jsonb) AS nd
+SELECT pg_get_statisticsobjdef_columns(oid) AS cols,
+ jsonb_pretty(stxdndistinct::text::jsonb) AS nd
FROM pg_statistic_ext JOIN pg_statistic_ext_data on (oid = stxoid)
WHERE stxname = 'stts2';
--[ RECORD 1 ]-------------------
-k | 1 2 5
-nd | [ +
- | { +
- | "ndistinct": 33178,+
- | "attributes": [ +
- | 1, +
- | 2 +
- | ] +
- | }, +
- | { +
- | "ndistinct": 33178,+
- | "attributes": [ +
- | 1, +
- | 5 +
- | ] +
- | }, +
- | { +
- | "ndistinct": 27435,+
- | "attributes": [ +
- | 2, +
- | 5 +
- | ] +
- | }, +
- | { +
- | "ndistinct": 33178,+
- | "attributes": [ +
- | 1, +
- | 2, +
- | 5 +
- | ] +
- | } +
- | ]
+-[ RECORD 1 ]--------------------
+cols | {city,state,zip}
+nd | [ +
+ | { +
+ | "ndistinct": 33178,+
+ | "attributes": [ +
+ | 1, +
+ | 2 +
+ | ] +
+ | }, +
+ | { +
+ | "ndistinct": 33178,+
+ | "attributes": [ +
+ | 1, +
+ | 5 +
+ | ] +
+ | }, +
+ | { +
+ | "ndistinct": 27435,+
+ | "attributes": [ +
+ | 2, +
+ | 5 +
+ | ] +
+ | }, +
+ | { +
+ | "ndistinct": 33178,+
+ | "attributes": [ +
+ | 1, +
+ | 2, +
+ | 5 +
+ | ] +
+ | } +
+ | ]
(1 row)
</programlisting>
This indicates that there are three combinations of columns that
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 2ebec6928d5..98e4cab396a 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -4736,22 +4736,13 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
</para></entry>
</row>
- <row>
- <entry role="catalog_table_entry"><para role="column_definition">
- <structfield>attnames</structfield> <type>name[]</type>
- (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attname</structfield>)
- </para>
- <para>
- Names of the columns included in the extended statistics object
- </para></entry>
- </row>
-
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>exprs</structfield> <type>text[]</type>
</para>
<para>
- Expressions included in the extended statistics object
+ All target columns and expressions included in the extended
+ statistics object, in user-written order
</para></entry>
</row>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 8f129baec90..233e902f2b9 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -285,12 +285,7 @@ CREATE VIEW pg_stats_ext WITH (security_barrier) AS
s.stxname AS statistics_name,
s.oid AS statistics_id,
pg_get_userbyid(s.stxowner) AS statistics_owner,
- ( SELECT array_agg(a.attname ORDER BY a.attnum)
- FROM unnest(s.stxkeys) k
- JOIN pg_attribute a
- ON (a.attrelid = s.stxrelid AND a.attnum = k)
- ) AS attnames,
- pg_get_statisticsobjdef_expressions(s.oid) as exprs,
+ pg_get_statisticsobjdef_columns(s.oid) AS exprs,
s.stxkind AS kinds,
sd.stxdinherit AS inherited,
sd.stxdndistinct AS n_distinct,
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index b354723be44..30b69be1f06 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -45,26 +45,12 @@ static char *ChooseExtendedStatisticName(const char *name1, const char *name2,
const char *label, Oid namespaceid);
static char *ChooseExtendedStatisticNameAddition(List *exprs);
-
-/* qsort comparator for the attnums in CreateStatistics */
-static int
-compare_int16(const void *a, const void *b)
-{
- int av = *(const int16 *) a;
- int bv = *(const int16 *) b;
-
- /* this can't overflow if int is wider than int16 */
- return (av - bv);
-}
-
/*
* CREATE STATISTICS
*/
ObjectAddress
CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
{
- int16 attnums[STATS_MAX_DIMENSIONS];
- int nattnums = 0;
int numcols;
char *namestr;
NameData stxname;
@@ -74,8 +60,8 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
HeapTuple htup;
Datum values[Natts_pg_statistic_ext];
bool nulls[Natts_pg_statistic_ext];
- int2vector *stxkeys;
List *stxexprs = NIL;
+ char *exprsString;
Datum exprsDatum;
Relation statrel;
Relation rel = NULL;
@@ -89,8 +75,8 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
bool build_dependencies;
bool build_mcv;
bool build_expressions;
+ bool have_vars = false;
bool requested_type = false;
- int i;
ListCell *cell;
ListCell *cell2;
@@ -230,8 +216,8 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
STATS_MAX_DIMENSIONS)));
/*
- * Convert the expression list to a simple array of attnums, but also keep
- * a list of more complex expressions. While at it, enforce some
+ * Convert the expression list to a list of expression trees. Simple
+ * column references are stored as Var nodes. While at it, enforce some
* constraints - we don't allow extended statistics on system attributes,
* and we require the data type to have a less-than operator, if we're
* building multivariate statistics.
@@ -286,24 +272,14 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
format_type_be(attForm->atttypid))));
}
- /* Treat virtual generated columns as expressions */
- if (attForm->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
- {
- Node *expr;
-
- expr = (Node *) makeVar(1,
- attForm->attnum,
- attForm->atttypid,
- attForm->atttypmod,
- attForm->attcollation,
- 0);
- stxexprs = lappend(stxexprs, expr);
- }
- else
- {
- attnums[nattnums] = attForm->attnum;
- nattnums++;
- }
+ stxexprs = lappend(stxexprs,
+ (Node *) makeVar(1,
+ attForm->attnum,
+ attForm->atttypid,
+ attForm->atttypmod,
+ attForm->attcollation,
+ 0));
+ have_vars = true;
ReleaseSysCache(atttuple);
}
else if (IsA(selem->expr, Var)) /* column reference in parens */
@@ -333,16 +309,8 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
format_type_be(var->vartype))));
}
- /* Treat virtual generated columns as expressions */
- if (get_attgenerated(relid, var->varattno) == ATTRIBUTE_GENERATED_VIRTUAL)
- {
- stxexprs = lappend(stxexprs, (Node *) var);
- }
- else
- {
- attnums[nattnums] = var->varattno;
- nattnums++;
- }
+ stxexprs = lappend(stxexprs, (Node *) var);
+ have_vars = true;
}
else /* expression */
{
@@ -393,20 +361,23 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
* that we're building statistics on a single expression (or virtual
* generated column).
*/
- if (numcols < 2 && list_length(stxexprs) != 1)
- ereport(ERROR,
- errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("cannot create extended statistics on a single non-virtual column"),
- errdetail("Univariate statistics are already built for each individual non-virtual table column."));
+ if (numcols == 1)
+ {
+ Node *single = (Node *) linitial(stxexprs);
- /*
- * Parse the statistics kinds (not allowed when building univariate
- * statistics).
- */
- if (numcols == 1 && stmt->stat_types != NIL)
- ereport(ERROR,
- errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("cannot specify statistics kinds when building univariate statistics"));
+ if (IsA(single, Var) &&
+ get_attgenerated(relid, ((Var *) single)->varattno) != ATTRIBUTE_GENERATED_VIRTUAL)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot create extended statistics on a single non-virtual column"),
+ errdetail("Univariate statistics are already built for each individual non-virtual table column."));
+
+ /* statistics kinds are not allowed with univariate statistics */
+ if (stmt->stat_types != NIL)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot specify statistics kinds when building univariate statistics"));
+ }
build_ndistinct = false;
build_dependencies = false;
@@ -449,43 +420,29 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
}
/*
- * When there are non-trivial expressions, build the expression stats
- * automatically. This allows calculating good estimates for stats that
- * consider per-clause estimates (e.g. functional dependencies).
- */
- build_expressions = (stxexprs != NIL);
-
- /*
- * Sort the attnums, which makes detecting duplicates somewhat easier, and
- * it does not hurt (it does not matter for the contents, unlike for
- * indexes, for example).
- */
- qsort(attnums, nattnums, sizeof(int16), compare_int16);
-
- /*
- * Check for duplicates in the list of columns. The attnums are sorted so
- * just check consecutive elements.
+ * When there are non-trivial expressions, or virtual generated columns
+ * (which are also stored as Var nodes but treated as expressions for
+ * statistics purposes), build the expression stats automatically. This
+ * allows calculating good estimates for stats that consider per-clause
+ * estimates (e.g. functional dependencies).
*/
- for (i = 1; i < nattnums; i++)
+ build_expressions = false;
+ foreach(cell, stxexprs)
{
- if (attnums[i] == attnums[i - 1])
- ereport(ERROR,
- (errcode(ERRCODE_DUPLICATE_COLUMN),
- errmsg("duplicate column name in statistics definition")));
+ Node *expr = (Node *) lfirst(cell);
+
+ if (!IsA(expr, Var) ||
+ get_attgenerated(relid, ((Var *) expr)->varattno) == ATTRIBUTE_GENERATED_VIRTUAL)
+ {
+ build_expressions = true;
+ break;
+ }
}
/*
- * Check for duplicate expressions. We do two loops, counting the
- * occurrences of each expression. This is O(N^2) but we only allow small
- * number of expressions and it's not executed often.
- *
- * XXX We don't cross-check attributes and expressions, because it does
- * not seem worth it. In principle we could check that expressions don't
- * contain trivial attribute references like "(a)", but the reasoning is
- * similar to why we don't bother with extracting columns from
- * expressions. It's either expensive or very easy to defeat for
- * determined user, and there's no risk if we allow such statistics (the
- * statistics is useless, but harmless).
+ * Check for duplicates. We use equal() to compare all entries (both
+ * simple Var references and complex expressions). This is O(N^2) but we
+ * only allow a small number of entries and it's not executed often.
*/
foreach(cell, stxexprs)
{
@@ -509,9 +466,6 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
errmsg("duplicate expression in statistics definition")));
}
- /* Form an int2vector representation of the sorted column list */
- stxkeys = buildint2vector(attnums, nattnums);
-
/* construct the char array of enabled statistic types */
ntypes = 0;
if (build_ndistinct)
@@ -525,17 +479,10 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
Assert(ntypes > 0 && ntypes <= lengthof(types));
stxkind = construct_array_builtin(types, ntypes, CHAROID);
- /* convert the expressions (if any) to a text datum */
- if (stxexprs != NIL)
- {
- char *exprsString;
-
- exprsString = nodeToString(stxexprs);
- exprsDatum = CStringGetTextDatum(exprsString);
- pfree(exprsString);
- }
- else
- exprsDatum = (Datum) 0;
+ /* convert the expression list to a text datum */
+ exprsString = nodeToString(stxexprs);
+ exprsDatum = CStringGetTextDatum(exprsString);
+ pfree(exprsString);
statrel = table_open(StatisticExtRelationId, RowExclusiveLock);
@@ -552,13 +499,9 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
values[Anum_pg_statistic_ext_stxname - 1] = NameGetDatum(&stxname);
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;
values[Anum_pg_statistic_ext_stxkind - 1] = PointerGetDatum(stxkind);
-
values[Anum_pg_statistic_ext_stxexprs - 1] = exprsDatum;
- if (exprsDatum == (Datum) 0)
- nulls[Anum_pg_statistic_ext_stxexprs - 1] = true;
/* insert it into pg_statistic_ext */
htup = heap_form_tuple(statrel->rd_att, values, nulls);
@@ -588,25 +531,18 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
*/
ObjectAddressSet(myself, StatisticExtRelationId, statoid);
- /* add dependencies for plain column references */
- for (i = 0; i < nattnums; i++)
- {
- ObjectAddressSubSet(parentobject, RelationRelationId, relid, attnums[i]);
- recordDependencyOn(&myself, &parentobject, DEPENDENCY_AUTO);
- }
-
/*
* If there are no dependencies on a column, give the statistics object an
* auto dependency on the whole table. In most cases, this will be
- * redundant, but it might not be if the statistics expressions contain no
- * Vars (which might seem strange but possible). This is consistent with
- * what we do for indexes in index_create.
+ * redundant, but it might not be if the expressions contain no Vars or
+ * only whole-row Vars. This is consistent with what we do for indexes in
+ * index_create.
*
* XXX We intentionally don't consider the expressions before adding this
* dependency, because recordDependencyOnSingleRelExpr may not create any
* dependencies for whole-row Vars.
*/
- if (!nattnums)
+ if (!have_vars)
{
ObjectAddressSet(parentobject, RelationRelationId, relid);
recordDependencyOn(&myself, &parentobject, DEPENDENCY_AUTO);
@@ -616,12 +552,11 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
* Store dependencies on anything mentioned in statistics expressions,
* just like we do for index expressions.
*/
- if (stxexprs)
- recordDependencyOnSingleRelExpr(&myself,
- (Node *) stxexprs,
- relid,
- DEPENDENCY_NORMAL,
- DEPENDENCY_AUTO, false);
+ recordDependencyOnSingleRelExpr(&myself,
+ (Node *) stxexprs,
+ relid,
+ DEPENDENCY_NORMAL,
+ DEPENDENCY_AUTO, false);
/*
* Also add dependencies on namespace and owner. These are required
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 7c4be174869..8eb5e4af69a 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1748,28 +1748,18 @@ get_relation_statistics(PlannerInfo *root, RelOptInfo *rel,
foreach(l, statoidlist)
{
Oid statOid = lfirst_oid(l);
- Form_pg_statistic_ext staForm;
HeapTuple htup;
Bitmapset *keys = NULL;
List *exprs = NIL;
- int i;
htup = SearchSysCache1(STATEXTOID, ObjectIdGetDatum(statOid));
if (!HeapTupleIsValid(htup))
elog(ERROR, "cache lookup failed for statistics object %u", statOid);
- staForm = (Form_pg_statistic_ext) GETSTRUCT(htup);
/*
- * First, build the array of columns covered. This is ultimately
- * wasted if no stats within the object have actually been built, but
- * it doesn't seem worth troubling over that case.
- */
- for (i = 0; i < staForm->stxkeys.dim1; i++)
- keys = bms_add_member(keys, staForm->stxkeys.values[i]);
-
- /*
- * Preprocess expressions (if any). We read the expressions, fix the
- * varnos, and run them through eval_const_expressions.
+ * Preprocess stxexprs. We read all entries, separate simple column
+ * references (into the keys Bitmapset) from complex expressions, fix
+ * the varnos, and run expressions through eval_const_expressions.
*
* XXX We don't know yet if there are any data for this stats object,
* with either stxdinherit value. But it's reasonable to assume there
@@ -1777,44 +1767,30 @@ get_relation_statistics(PlannerInfo *root, RelOptInfo *rel,
* keys and expressions here.
*/
{
- bool isnull;
- Datum datum;
+ statext_decode_stxexprs(htup, relation, &keys, &exprs);
- /* decode expression (if any) */
- datum = SysCacheGetAttr(STATEXTOID, htup,
- Anum_pg_statistic_ext_stxexprs, &isnull);
+ /*
+ * Modify the copies we obtain from the relcache to have the
+ * correct varno for the parent relation, so that they match up
+ * correctly against qual clauses.
+ *
+ * This must be done before const-simplification because
+ * eval_const_expressions reduces NullTest for Vars based on
+ * varno.
+ */
+ if (exprs != NIL && varno != 1)
+ ChangeVarNodes((Node *) exprs, 1, varno, 0);
- if (!isnull)
+ /*
+ * Run the expressions through eval_const_expressions. This is not
+ * just an optimization, but is necessary, because the planner
+ * will be comparing them to similarly-processed qual clauses, and
+ * may fail to detect valid matches without this. We must not use
+ * canonicalize_qual, however, since these aren't qual
+ * expressions.
+ */
+ if (exprs != NIL)
{
- char *exprsString;
-
- exprsString = TextDatumGetCString(datum);
- exprs = (List *) stringToNode(exprsString);
- pfree(exprsString);
-
- /* Expand virtual generated columns in the expressions */
- exprs = (List *) expand_generated_columns_in_expr((Node *) exprs, relation, 1);
-
- /*
- * Modify the copies we obtain from the relcache to have the
- * correct varno for the parent relation, so that they match
- * up correctly against qual clauses.
- *
- * This must be done before const-simplification because
- * eval_const_expressions reduces NullTest for Vars based on
- * varno.
- */
- if (varno != 1)
- ChangeVarNodes((Node *) exprs, 1, varno, 0);
-
- /*
- * Run the expressions through eval_const_expressions. This is
- * not just an optimization, but is necessary, because the
- * planner will be comparing them to similarly-processed qual
- * clauses, and may fail to detect valid matches without this.
- * We must not use canonicalize_qual, however, since these
- * aren't qual expressions.
- */
exprs = (List *) eval_const_expressions(root, (Node *) exprs);
/* May as well fix opfuncids too */
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index a049cc67ed6..d6ce6005004 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2042,9 +2042,9 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
* extended statistic "source_statsid", for the rel identified by heapRel and
* heapRelid.
*
- * stxkeys in the source statistic holds attribute numbers from the parent
+ * stxexprs in the source statistic holds Var nodes referencing the parent
* relation. Those attnums, along with the attribute numbers referenced by
- * Vars inside the expression tree, are remapped to the new relation's
+ * Vars inside complex expressions, are remapped to the new relation's
* numbering according to attmap.
*/
static CreateStatsStmt *
@@ -2052,15 +2052,16 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
Oid source_statsid, const AttrMap *attmap)
{
HeapTuple ht_stats;
- Form_pg_statistic_ext statsrec;
CreateStatsStmt *stats;
List *stat_types = NIL;
List *def_names = NIL;
- bool isnull;
Datum datum;
ArrayType *arr;
char *enabled;
int i;
+ ListCell *lc;
+ List *exprs = NIL;
+ char *exprsString;
Assert(OidIsValid(heapRelid));
Assert(heapRel != NULL);
@@ -2071,7 +2072,6 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
ht_stats = SearchSysCache1(STATEXTOID, ObjectIdGetDatum(source_statsid));
if (!HeapTupleIsValid(ht_stats))
elog(ERROR, "cache lookup failed for statistics object %u", source_statsid);
- statsrec = (Form_pg_statistic_ext) GETSTRUCT(ht_stats);
/* Determine which statistics types exist */
datum = SysCacheGetAttrNotNull(STATEXTOID, ht_stats,
@@ -2097,44 +2097,31 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
elog(ERROR, "unrecognized statistics kind %c", enabled[i]);
}
- /* Determine which columns the statistics are on */
- for (i = 0; i < statsrec->stxkeys.dim1; i++)
- {
- StatsElem *selem = makeNode(StatsElem);
- AttrNumber attnum = statsrec->stxkeys.values[i];
-
- selem->name =
- get_attname(heapRelid, attmap->attnums[attnum - 1], false);
- selem->expr = NULL;
-
- def_names = lappend(def_names, selem);
- }
-
/*
- * Now handle expressions, if there are any. The order (with respect to
- * regular attributes) does not really matter for extended stats, so we
- * simply append them after simple column references.
- *
- * XXX Some places during build/estimation treat expressions as if they
- * are before attributes, but for the CREATE command that's entirely
- * irrelevant.
+ * Decode stxexprs to reconstruct the column/expression list. Simple Var
+ * nodes represent plain column references; other nodes are complex
+ * expressions.
*/
- datum = SysCacheGetAttr(STATEXTOID, ht_stats,
- Anum_pg_statistic_ext_stxexprs, &isnull);
+ datum = SysCacheGetAttrNotNull(STATEXTOID, ht_stats,
+ Anum_pg_statistic_ext_stxexprs);
+ exprsString = TextDatumGetCString(datum);
+ exprs = (List *) stringToNode(exprsString);
- if (!isnull)
+ foreach(lc, exprs)
{
- ListCell *lc;
- List *exprs = NIL;
- char *exprsString;
+ Node *expr = (Node *) lfirst(lc);
+ StatsElem *selem = makeNode(StatsElem);
- exprsString = TextDatumGetCString(datum);
- exprs = (List *) stringToNode(exprsString);
+ if (IsA(expr, Var) && ((Var *) expr)->varattno > 0)
+ {
+ AttrNumber attnum = ((Var *) expr)->varattno;
- foreach(lc, exprs)
+ selem->name =
+ get_attname(heapRelid, attmap->attnums[attnum - 1], false);
+ selem->expr = NULL;
+ }
+ else
{
- Node *expr = (Node *) lfirst(lc);
- StatsElem *selem = makeNode(StatsElem);
bool found_whole_row;
/* Adjust Vars to match new table's column numbering */
@@ -2146,13 +2133,13 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
selem->name = NULL;
selem->expr = expr;
-
- def_names = lappend(def_names, selem);
}
- pfree(exprsString);
+ def_names = lappend(def_names, selem);
}
+ pfree(exprsString);
+
/* finally, build the output node */
stats = makeNode(CreateStatsStmt);
stats->defnames = NULL;
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index 95dcc218978..59ef192c2f6 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -393,7 +393,7 @@ statext_dependencies_build(StatsBuildData *data)
d = (MVDependency *) palloc0(offsetof(MVDependency, attributes)
+ k * sizeof(AttrNumber));
- /* copy the dependency (and keep the indexes into stxkeys) */
+ /* copy the dependency */
d->degree = degree;
d->nattributes = k;
for (i = 0; i < k; i++)
@@ -598,17 +598,15 @@ statext_dependencies_free(MVDependencies *dependencies)
* attributes list correspond to attnums/expressions defined by the
* extended statistics object.
*
- * Positive attnums are attributes which must be found in the stxkeys, while
- * negative attnums correspond to an expression number, no attribute number
- * can be below (0 - numexprs).
+ * Positive attnums correspond to table columns (excluding virtual generated
+ * columns), while negative attnums correspond to expressions. No attribute
+ * number can be below (0 - numexprs).
*/
bool
statext_dependencies_validate(const MVDependencies *dependencies,
- const int2vector *stxkeys,
+ Bitmapset *keys,
int numexprs, int elevel)
{
- int attnum_expr_lowbound = 0 - numexprs;
-
/* Scan through each dependency entry */
for (int i = 0; i < dependencies->ndeps; i++)
{
@@ -621,27 +619,8 @@ statext_dependencies_validate(const MVDependencies *dependencies,
for (int j = 0; j < dep->nattributes; j++)
{
AttrNumber attnum = dep->attributes[j];
- bool ok = false;
-
- if (attnum > 0)
- {
- /* attribute number in stxkeys */
- for (int k = 0; k < stxkeys->dim1; k++)
- {
- if (attnum == stxkeys->values[k])
- {
- ok = true;
- break;
- }
- }
- }
- else if ((attnum < 0) && (attnum >= attnum_expr_lowbound))
- {
- /* attribute number for an expression */
- ok = true;
- }
- if (!ok)
+ if (!statext_is_valid_attnum(attnum, keys, numexprs))
{
ereport(elevel,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 2b83355d26e..b2ea8d8c11a 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -447,6 +447,77 @@ statext_is_kind_built(HeapTuple htup, char type)
return !heap_attisnull(htup, attnum, NULL);
}
+/*
+ * statext_decode_stxexprs
+ * Decode the stxexprs field of a pg_statistic_ext tuple.
+ *
+ * Deserializes the expression list, expands virtual generated columns,
+ * separates simple Var references (into keys Bitmapset) from complex
+ * expressions (into exprs list), and const-folds the expressions (as
+ * RelationGetIndexExpressions does).
+ */
+void
+statext_decode_stxexprs(HeapTuple htup, Relation rel,
+ Bitmapset **keys, List **exprs)
+{
+ Datum datum;
+ char *exprsString;
+ List *allexprs;
+ ListCell *lc;
+
+ datum = SysCacheGetAttrNotNull(STATEXTOID, htup,
+ Anum_pg_statistic_ext_stxexprs);
+ exprsString = TextDatumGetCString(datum);
+ allexprs = (List *) stringToNode(exprsString);
+ pfree(exprsString);
+
+ /* Expand virtual generated columns in the expressions */
+ allexprs = (List *) expand_generated_columns_in_expr((Node *) allexprs, rel, 1);
+
+ foreach(lc, allexprs)
+ {
+ Node *expr = (Node *) lfirst(lc);
+
+ if (IsA(expr, Var) && ((Var *) expr)->varattno > 0)
+ *keys = bms_add_member(*keys, ((Var *) expr)->varattno);
+ else
+ *exprs = lappend(*exprs, expr);
+ }
+
+ if (*exprs != NIL)
+ {
+ /*
+ * Run the expressions through eval_const_expressions. This is not
+ * just an optimization, but is necessary, because the planner will be
+ * comparing them to similarly-processed qual clauses, and may fail to
+ * detect valid matches without this. We must not use
+ * canonicalize_qual, however, since these aren't qual expressions.
+ */
+ *exprs = (List *) eval_const_expressions(NULL, (Node *) *exprs);
+
+ /* May as well fix opfuncids too */
+ fix_opfuncids((Node *) *exprs);
+ }
+}
+
+/*
+ * statext_is_valid_attnum
+ * Is attnum a valid reference within an extended statistics object?
+ *
+ * A positive attnum is a simple column and must be a member of the object's
+ * keys; a negative attnum references an expression and must be within the
+ * range [-numexprs, -1]. Used to validate imported stats against the object
+ * definition.
+ */
+bool
+statext_is_valid_attnum(AttrNumber attnum, Bitmapset *keys, int numexprs)
+{
+ if (attnum > 0)
+ return bms_is_member(attnum, keys);
+
+ return (attnum < 0) && (attnum >= 0 - numexprs);
+}
+
/*
* Return a list (of StatExtEntry) of statistics objects for the given relation.
*/
@@ -487,11 +558,6 @@ fetch_statentries_for_relation(Relation pg_statext, Relation rel)
entry->statOid = staForm->oid;
entry->schema = get_namespace_name(staForm->stxnamespace);
entry->name = pstrdup(NameStr(staForm->stxname));
- for (i = 0; i < staForm->stxkeys.dim1; i++)
- {
- entry->columns = bms_add_member(entry->columns,
- staForm->stxkeys.values[i]);
- }
datum = SysCacheGetAttr(STATEXTOID, htup, Anum_pg_statistic_ext_stxstattarget, &isnull);
entry->stattarget = isnull ? -1 : DatumGetInt16(datum);
@@ -514,35 +580,8 @@ fetch_statentries_for_relation(Relation pg_statext, Relation rel)
entry->types = lappend_int(entry->types, (int) enabled[i]);
}
- /* decode expression (if any) */
- datum = SysCacheGetAttr(STATEXTOID, htup,
- Anum_pg_statistic_ext_stxexprs, &isnull);
-
- if (!isnull)
- {
- char *exprsString;
-
- exprsString = TextDatumGetCString(datum);
- exprs = (List *) stringToNode(exprsString);
-
- pfree(exprsString);
-
- /* Expand virtual generated columns in the expressions */
- exprs = (List *) expand_generated_columns_in_expr((Node *) exprs, rel, 1);
-
- /*
- * Run the expressions through eval_const_expressions. This is not
- * just an optimization, but is necessary, because the planner
- * will be comparing them to similarly-processed qual clauses, and
- * may fail to detect valid matches without this. We must not use
- * canonicalize_qual, however, since these aren't qual
- * expressions.
- */
- exprs = (List *) eval_const_expressions(NULL, (Node *) exprs);
-
- /* May as well fix opfuncids too */
- fix_opfuncids((Node *) exprs);
- }
+ /* Decode stxexprs into entry->columns and exprs (const-folded) */
+ statext_decode_stxexprs(htup, rel, &entry->columns, &exprs);
entry->exprs = exprs;
diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index 2cb3942056f..5eb09c1fa91 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -17,6 +17,7 @@
#include "postgres.h"
#include "access/heapam.h"
+#include "access/table.h"
#include "catalog/indexing.h"
#include "catalog/namespace.h"
#include "catalog/pg_collation_d.h"
@@ -331,9 +332,9 @@ extended_statistics_update(FunctionCallInfo fcinfo)
bool nulls[Natts_pg_statistic_ext_data] = {0};
bool replaces[Natts_pg_statistic_ext_data] = {0};
bool success = true;
- Datum exprdatum;
- bool isnull;
+ Relation rel;
List *exprs = NIL;
+ Bitmapset *keys = NULL;
int numattnums = 0;
int numexprs = 0;
int numattrs = 0;
@@ -439,38 +440,15 @@ extended_statistics_update(FunctionCallInfo fcinfo)
/* Find out what extended statistics kinds we should expect. */
expand_stxkind(tup, &enabled);
- numattnums = stxform->stxkeys.dim1;
-
- /* decode expression (if any) */
- exprdatum = SysCacheGetAttr(STATEXTOID,
- tup,
- Anum_pg_statistic_ext_stxexprs,
- &isnull);
- if (!isnull)
- {
- char *s;
- s = TextDatumGetCString(exprdatum);
- exprs = (List *) stringToNode(s);
- pfree(s);
-
- /*
- * Run the expressions through eval_const_expressions(). This is not
- * just an optimization, but is necessary, because the planner will be
- * comparing them to similarly-processed qual clauses, and may fail to
- * detect valid matches without this.
- *
- * We must not use canonicalize_qual(), however, since these are not
- * qual expressions.
- */
- exprs = (List *) eval_const_expressions(NULL, (Node *) exprs);
+ /* Decode stxexprs into keys and exprs (const-folded) */
+ rel = table_open(relid, NoLock);
+ statext_decode_stxexprs(tup, rel, &keys, &exprs);
+ table_close(rel, NoLock);
- /* May as well fix opfuncids too */
- fix_opfuncids((Node *) exprs);
+ numattnums = bms_num_members(keys);
- /* Compute the number of expression, for input validation. */
- numexprs = list_length(exprs);
- }
+ numexprs = list_length(exprs);
numattrs = numattnums + numexprs;
@@ -580,54 +558,54 @@ extended_statistics_update(FunctionCallInfo fcinfo)
*/
if (has.mcv || has.expressions)
{
+ int idx;
+ int k;
+
atttypids = palloc0_array(Oid, numattrs);
atttypmods = palloc0_array(int32, numattrs);
atttypcolls = palloc0_array(Oid, numattrs);
/*
- * The leading stxkeys are attribute numbers up through numattnums.
- * These keys must be in ascending AttrNumber order, but we do not
- * rely on that.
+ * Get type info for plain column attributes (from the keys
+ * Bitmapset).
*/
- for (int i = 0; i < numattnums; i++)
+ idx = 0;
+ k = -1;
+ while ((k = bms_next_member(keys, k)) >= 0)
{
- AttrNumber attnum = stxform->stxkeys.values[i];
+ AttrNumber attnum = (AttrNumber) k;
HeapTuple atup = SearchSysCache2(ATTNUM,
ObjectIdGetDatum(relid),
Int16GetDatum(attnum));
-
Form_pg_attribute attr;
/* Attribute not found */
if (!HeapTupleIsValid(atup))
- elog(ERROR, "stxkeys references nonexistent attnum %d", attnum);
+ elog(ERROR, "stxexprs references nonexistent attnum %d", attnum);
attr = (Form_pg_attribute) GETSTRUCT(atup);
if (attr->attisdropped)
- elog(ERROR, "stxkeys references dropped attnum %d", attnum);
+ elog(ERROR, "stxexprs references dropped attnum %d", attnum);
- atttypids[i] = attr->atttypid;
- atttypmods[i] = attr->atttypmod;
- atttypcolls[i] = attr->attcollation;
+ atttypids[idx] = attr->atttypid;
+ atttypmods[idx] = attr->atttypmod;
+ atttypcolls[idx] = attr->attcollation;
ReleaseSysCache(atup);
+ idx++;
}
/*
- * After all the positive number attnums in stxkeys come the negative
- * numbers (if any) which represent expressions in the order that they
- * appear in stxdexpr. Because the expressions are always
- * monotonically decreasing from -1, there is no point in looking at
- * the values in stxkeys, it's enough to know how many of them there
- * are.
+ * Get type info for expressions.
*/
- for (int i = numattnums; i < numattrs; i++)
+ for (int i = 0; i < numexprs; i++)
{
- Node *expr = list_nth(exprs, i - numattnums);
+ Node *expr = list_nth(exprs, i);
- atttypids[i] = exprType(expr);
- atttypmods[i] = exprTypmod(expr);
- atttypcolls[i] = exprCollation(expr);
+ atttypids[idx] = exprType(expr);
+ atttypmods[idx] = exprTypmod(expr);
+ atttypcolls[idx] = exprCollation(expr);
+ idx++;
}
}
@@ -659,7 +637,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
bytea *data = DatumGetByteaPP(ndistinct_datum);
MVNDistinct *ndistinct = statext_ndistinct_deserialize(data);
- if (statext_ndistinct_validate(ndistinct, &stxform->stxkeys,
+ if (statext_ndistinct_validate(ndistinct, keys,
numexprs, WARNING))
{
values[Anum_pg_statistic_ext_data_stxdndistinct - 1] = ndistinct_datum;
@@ -678,7 +656,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
bytea *data = DatumGetByteaPP(dependencies_datum);
MVDependencies *dependencies = statext_dependencies_deserialize(data);
- if (statext_dependencies_validate(dependencies, &stxform->stxkeys,
+ if (statext_dependencies_validate(dependencies, keys,
numexprs, WARNING))
{
values[Anum_pg_statistic_ext_data_stxddependencies - 1] = dependencies_datum;
diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 4f8f578a22f..431d7440b9e 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -344,17 +344,15 @@ statext_ndistinct_free(MVNDistinct *ndistinct)
* attributes list correspond to attnums/expressions defined by the extended
* statistics object.
*
- * Positive attnums are attributes which must be found in the stxkeys,
- * while negative attnums correspond to an expression number, no attribute
+ * Positive attnums correspond to table columns (excluding virtual generated
+ * columns), while negative attnums correspond to expressions. No attribute
* number can be below (0 - numexprs).
*/
bool
statext_ndistinct_validate(const MVNDistinct *ndistinct,
- const int2vector *stxkeys,
+ Bitmapset *keys,
int numexprs, int elevel)
{
- int attnum_expr_lowbound = 0 - numexprs;
-
/* Scan through each MVNDistinct entry */
for (int i = 0; i < ndistinct->nitems; i++)
{
@@ -367,27 +365,8 @@ statext_ndistinct_validate(const MVNDistinct *ndistinct,
for (int j = 0; j < item.nattributes; j++)
{
AttrNumber attnum = item.attributes[j];
- bool ok = false;
-
- if (attnum > 0)
- {
- /* attribute number in stxkeys */
- for (int k = 0; k < stxkeys->dim1; k++)
- {
- if (attnum == stxkeys->values[k])
- {
- ok = true;
- break;
- }
- }
- }
- else if ((attnum < 0) && (attnum >= attnum_expr_lowbound))
- {
- /* attribute number for an expression */
- ok = true;
- }
- if (!ok)
+ if (!statext_is_valid_attnum(attnum, keys, numexprs))
{
ereport(elevel,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..e889e848763 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -369,7 +369,7 @@ static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
static void make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind);
static void make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid);
static void make_propgraphdef_properties(StringInfo buf, Oid ellabelid, Oid elrelid);
-static char *pg_get_statisticsobj_worker(Oid statextid, bool columns_only,
+static char *pg_get_statisticsobj_worker(Oid statextid,
bool missing_ok);
static char *pg_get_partkeydef_worker(Oid relid, int prettyFlags,
bool attrsOnly, bool missing_ok);
@@ -1969,7 +1969,7 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, false, true);
+ res = pg_get_statisticsobj_worker(statextid, true);
if (res == NULL)
PG_RETURN_NULL();
@@ -1984,32 +1984,91 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
char *
pg_get_statisticsobjdef_string(Oid statextid)
{
- return pg_get_statisticsobj_worker(statextid, false, false);
+ return pg_get_statisticsobj_worker(statextid, false);
+}
+
+/*
+ * Deparse a single statistics target (column or expression) into a string.
+ * Plain column Vars are emitted as quoted identifiers; complex expressions
+ * are deparsed and wrapped in parentheses unless they look like function calls.
+ */
+static char *
+deparse_stat_target(Node *expr, Oid relid, List *context)
+{
+ char *str;
+
+ if (IsA(expr, Var) && ((Var *) expr)->varattno > 0)
+ return pstrdup(quote_identifier(
+ get_attname(relid, ((Var *) expr)->varattno, false)));
+
+ str = deparse_expression_pretty(expr, context, false, false,
+ PRETTYFLAG_PAREN, 0);
+ if (looks_like_function(expr))
+ return str;
+ return psprintf("(%s)", str);
}
/*
* pg_get_statisticsobjdef_columns
- * Get columns and expressions for an extended statistics object
+ * Get all target columns and expressions for an extended statistics
+ * object as a text array. Plain columns are emitted as column names,
+ * complex expressions are deparsed.
*/
Datum
pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
{
Oid statextid = PG_GETARG_OID(0);
- char *res;
+ Form_pg_statistic_ext statextrec;
+ HeapTuple statexttup;
+ Datum datum;
+ List *allexprs = NIL;
+ char *tmp;
+ List *context;
+ ListCell *lc;
+ ArrayBuildState *astate = NULL;
- res = pg_get_statisticsobj_worker(statextid, true, true);
+ statexttup = SearchSysCache1(STATEXTOID, ObjectIdGetDatum(statextid));
- if (res == NULL)
+ if (!HeapTupleIsValid(statexttup))
PG_RETURN_NULL();
- PG_RETURN_TEXT_P(string_to_text(res));
+ statextrec = (Form_pg_statistic_ext) GETSTRUCT(statexttup);
+
+ datum = SysCacheGetAttrNotNull(STATEXTOID, statexttup,
+ Anum_pg_statistic_ext_stxexprs);
+ tmp = TextDatumGetCString(datum);
+ allexprs = (List *) stringToNode(tmp);
+ pfree(tmp);
+
+ context = deparse_context_for(get_relation_name(statextrec->stxrelid),
+ statextrec->stxrelid);
+
+ foreach(lc, allexprs)
+ {
+ Node *expr = (Node *) lfirst(lc);
+ char *str;
+
+ str = deparse_stat_target(expr, statextrec->stxrelid, context);
+ astate = accumArrayResult(astate,
+ PointerGetDatum(cstring_to_text(str)),
+ false,
+ TEXTOID,
+ CurrentMemoryContext);
+ }
+
+ ReleaseSysCache(statexttup);
+
+ if (astate == NULL)
+ PG_RETURN_NULL();
+
+ PG_RETURN_DATUM(makeArrayResult(astate, CurrentMemoryContext));
}
/*
* Internal workhorse to decompile an extended statistics object.
*/
static char *
-pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
+pg_get_statisticsobj_worker(Oid statextid, bool missing_ok)
{
Form_pg_statistic_ext statextrec;
HeapTuple statexttup;
@@ -2019,6 +2078,7 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
ArrayType *arr;
char *enabled;
Datum datum;
+ char *exprsString;
bool ndistinct_enabled;
bool dependencies_enabled;
bool mcv_enabled;
@@ -2026,7 +2086,6 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
List *context;
ListCell *lc;
List *exprs = NIL;
- bool has_exprs;
int ncolumns;
statexttup = SearchSysCache1(STATEXTOID, ObjectIdGetDatum(statextid));
@@ -2038,151 +2097,111 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
elog(ERROR, "cache lookup failed for statistics object %u", statextid);
}
- /* has the statistics expressions? */
- has_exprs = !heap_attisnull(statexttup, Anum_pg_statistic_ext_stxexprs, NULL);
-
statextrec = (Form_pg_statistic_ext) GETSTRUCT(statexttup);
/*
- * Get the statistics expressions, if any. (NOTE: we do not use the
- * relcache versions of the expressions, because we want to display
- * non-const-folded expressions.)
+ * Get all statistics expressions. (NOTE: we do not use the relcache
+ * versions, because we want to display non-const-folded expressions.)
*/
- if (has_exprs)
- {
- Datum exprsDatum;
- char *exprsString;
-
- exprsDatum = SysCacheGetAttrNotNull(STATEXTOID, statexttup,
- Anum_pg_statistic_ext_stxexprs);
- exprsString = TextDatumGetCString(exprsDatum);
- exprs = (List *) stringToNode(exprsString);
- pfree(exprsString);
- }
- else
- exprs = NIL;
+ datum = SysCacheGetAttrNotNull(STATEXTOID, statexttup,
+ Anum_pg_statistic_ext_stxexprs);
+ exprsString = TextDatumGetCString(datum);
+ exprs = (List *) stringToNode(exprsString);
+ pfree(exprsString);
- /* count the number of columns (attributes and expressions) */
- ncolumns = statextrec->stxkeys.dim1 + list_length(exprs);
+ ncolumns = list_length(exprs);
initStringInfo(&buf);
- if (!columns_only)
- {
- nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
- appendStringInfo(&buf, "CREATE STATISTICS %s",
- quote_qualified_identifier(nsp,
- NameStr(statextrec->stxname)));
-
- /*
- * Decode the stxkind column so that we know which stats types to
- * print.
- */
- datum = SysCacheGetAttrNotNull(STATEXTOID, statexttup,
- Anum_pg_statistic_ext_stxkind);
- arr = DatumGetArrayTypeP(datum);
- if (ARR_NDIM(arr) != 1 ||
- ARR_HASNULL(arr) ||
- ARR_ELEMTYPE(arr) != CHAROID)
- elog(ERROR, "stxkind is not a 1-D char array");
- enabled = (char *) ARR_DATA_PTR(arr);
+ nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_qualified_identifier(nsp,
+ NameStr(statextrec->stxname)));
- ndistinct_enabled = false;
- dependencies_enabled = false;
- mcv_enabled = false;
-
- for (i = 0; i < ARR_DIMS(arr)[0]; i++)
- {
- if (enabled[i] == STATS_EXT_NDISTINCT)
- ndistinct_enabled = true;
- else if (enabled[i] == STATS_EXT_DEPENDENCIES)
- dependencies_enabled = true;
- else if (enabled[i] == STATS_EXT_MCV)
- mcv_enabled = true;
+ /*
+ * Decode the stxkind column so that we know which stats types to print.
+ */
+ datum = SysCacheGetAttrNotNull(STATEXTOID, statexttup,
+ Anum_pg_statistic_ext_stxkind);
+ arr = DatumGetArrayTypeP(datum);
+ if (ARR_NDIM(arr) != 1 ||
+ ARR_HASNULL(arr) ||
+ ARR_ELEMTYPE(arr) != CHAROID)
+ elog(ERROR, "stxkind is not a 1-D char array");
+ enabled = (char *) ARR_DATA_PTR(arr);
- /* ignore STATS_EXT_EXPRESSIONS (it's built automatically) */
- }
+ ndistinct_enabled = false;
+ dependencies_enabled = false;
+ mcv_enabled = false;
- /*
- * If any option is disabled, then we'll need to append the types
- * clause to show which options are enabled. We omit the types clause
- * on purpose when all options are enabled, so a pg_dump/pg_restore
- * will create all statistics types on a newer postgres version, if
- * the statistics had all options enabled on the original version.
- *
- * But if the statistics is defined on just a single column, it has to
- * be an expression statistics. In that case we don't need to specify
- * kinds.
- */
- if ((!ndistinct_enabled || !dependencies_enabled || !mcv_enabled) &&
- (ncolumns > 1))
- {
- bool gotone = false;
-
- appendStringInfoString(&buf, " (");
+ for (i = 0; i < ARR_DIMS(arr)[0]; i++)
+ {
+ if (enabled[i] == STATS_EXT_NDISTINCT)
+ ndistinct_enabled = true;
+ else if (enabled[i] == STATS_EXT_DEPENDENCIES)
+ dependencies_enabled = true;
+ else if (enabled[i] == STATS_EXT_MCV)
+ mcv_enabled = true;
- if (ndistinct_enabled)
- {
- appendStringInfoString(&buf, "ndistinct");
- gotone = true;
- }
+ /* ignore STATS_EXT_EXPRESSIONS (it's built automatically) */
+ }
- if (dependencies_enabled)
- {
- appendStringInfo(&buf, "%sdependencies", gotone ? ", " : "");
- gotone = true;
- }
+ /*
+ * If any option is disabled, then we'll need to append the types clause
+ * to show which options are enabled. We omit the types clause on purpose
+ * when all options are enabled, so a pg_dump/pg_restore will create all
+ * statistics types on a newer postgres version, if the statistics had all
+ * options enabled on the original version.
+ *
+ * But if the statistics is defined on just a single column, it has to be
+ * an expression statistics. In that case we don't need to specify kinds.
+ */
+ if ((!ndistinct_enabled || !dependencies_enabled || !mcv_enabled) &&
+ (ncolumns > 1))
+ {
+ bool gotone = false;
- if (mcv_enabled)
- appendStringInfo(&buf, "%smcv", gotone ? ", " : "");
+ appendStringInfoString(&buf, " (");
- appendStringInfoChar(&buf, ')');
+ if (ndistinct_enabled)
+ {
+ appendStringInfoString(&buf, "ndistinct");
+ gotone = true;
}
- appendStringInfoString(&buf, " ON ");
- }
-
- /* decode simple column references */
- for (colno = 0; colno < statextrec->stxkeys.dim1; colno++)
- {
- AttrNumber attnum = statextrec->stxkeys.values[colno];
- char *attname;
-
- if (colno > 0)
- appendStringInfoString(&buf, ", ");
+ if (dependencies_enabled)
+ {
+ appendStringInfo(&buf, "%sdependencies", gotone ? ", " : "");
+ gotone = true;
+ }
- attname = get_attname(statextrec->stxrelid, attnum, false);
+ if (mcv_enabled)
+ appendStringInfo(&buf, "%smcv", gotone ? ", " : "");
- appendStringInfoString(&buf, quote_identifier(attname));
+ appendStringInfoChar(&buf, ')');
}
+ appendStringInfoString(&buf, " ON ");
+
context = deparse_context_for(get_relation_name(statextrec->stxrelid),
statextrec->stxrelid);
+ colno = 0;
foreach(lc, exprs)
{
Node *expr = (Node *) lfirst(lc);
- char *str;
- int prettyFlags = PRETTYFLAG_PAREN;
-
- str = deparse_expression_pretty(expr, context, false, false,
- prettyFlags, 0);
if (colno > 0)
appendStringInfoString(&buf, ", ");
- /* Need parens if it's not a bare function call */
- if (looks_like_function(expr))
- appendStringInfoString(&buf, str);
- else
- appendStringInfo(&buf, "(%s)", str);
-
+ appendStringInfoString(&buf,
+ deparse_stat_target(expr, statextrec->stxrelid,
+ context));
colno++;
}
- if (!columns_only)
- appendStringInfo(&buf, " FROM %s",
- generate_relation_name(statextrec->stxrelid, NIL));
+ appendStringInfo(&buf, " FROM %s",
+ generate_relation_name(statextrec->stxrelid, NIL));
ReleaseSysCache(statexttup);
@@ -2202,7 +2221,6 @@ pg_get_statisticsobjdef_expressions(PG_FUNCTION_ARGS)
List *context;
ListCell *lc;
List *exprs = NIL;
- bool has_exprs;
char *tmp;
ArrayBuildState *astate = NULL;
@@ -2211,20 +2229,11 @@ pg_get_statisticsobjdef_expressions(PG_FUNCTION_ARGS)
if (!HeapTupleIsValid(statexttup))
PG_RETURN_NULL();
- /* Does the stats object have expressions? */
- has_exprs = !heap_attisnull(statexttup, Anum_pg_statistic_ext_stxexprs, NULL);
-
- /* no expressions? we're done */
- if (!has_exprs)
- {
- ReleaseSysCache(statexttup);
- PG_RETURN_NULL();
- }
-
statextrec = (Form_pg_statistic_ext) GETSTRUCT(statexttup);
/*
- * Get the statistics expressions, and deparse them into text values.
+ * Get all statistics expressions, and deparse the non-trivial ones (skip
+ * simple Var references which represent plain columns).
*/
datum = SysCacheGetAttrNotNull(STATEXTOID, statexttup,
Anum_pg_statistic_ext_stxexprs);
@@ -2241,6 +2250,11 @@ pg_get_statisticsobjdef_expressions(PG_FUNCTION_ARGS)
char *str;
int prettyFlags = PRETTYFLAG_INDENT;
+ /* Skip plain column references (but not virtual generated columns) */
+ if (IsA(expr, Var) && ((Var *) expr)->varattno > 0 &&
+ get_attgenerated(statextrec->stxrelid, ((Var *) expr)->varattno) != ATTRIBUTE_GENERATED_VIRTUAL)
+ continue;
+
str = deparse_expression_pretty(expr, context, false, false,
prettyFlags, 0);
@@ -2253,6 +2267,9 @@ pg_get_statisticsobjdef_expressions(PG_FUNCTION_ARGS)
ReleaseSysCache(statexttup);
+ if (astate == NULL)
+ PG_RETURN_NULL();
+
PG_RETURN_DATUM(makeArrayResult(astate, CurrentMemoryContext));
}
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index af3935b0078..1c55c52c101 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2915,8 +2915,15 @@ describeOneTableDetails(const char *schemaname,
"SELECT oid, "
"stxrelid::pg_catalog.regclass, "
"stxnamespace::pg_catalog.regnamespace::pg_catalog.text AS nsp, "
- "stxname,\n"
- "pg_catalog.pg_get_statisticsobjdef_columns(oid) AS columns,\n"
+ "stxname,\n");
+ /* TODO: update threshold to 200000 when PG20 version is assigned */
+ if (pset.sversion >= 190000)
+ appendPQExpBufferStr(&buf,
+ "pg_catalog.array_to_string(pg_catalog.pg_get_statisticsobjdef_columns(oid), ', ') AS columns,\n");
+ else
+ appendPQExpBufferStr(&buf,
+ "pg_catalog.pg_get_statisticsobjdef_columns(oid) AS columns,\n");
+ appendPQExpBuffer(&buf,
" " CppAsString2(STATS_EXT_NDISTINCT) " = any(stxkind) AS ndist_enabled,\n"
" " CppAsString2(STATS_EXT_DEPENDENCIES) " = any(stxkind) AS deps_enabled,\n"
" " CppAsString2(STATS_EXT_MCV) " = any(stxkind) AS mcv_enabled,\n"
@@ -5133,7 +5140,14 @@ listExtendedStats(const char *pattern, bool verbose)
gettext_noop("Schema"),
gettext_noop("Name"));
- if (pset.sversion >= 140000)
+ /* TODO: update threshold to 200000 when PG20 version is assigned */
+ if (pset.sversion >= 190000)
+ appendPQExpBuffer(&buf,
+ "pg_catalog.format('%%s FROM %%s', \n"
+ " pg_catalog.array_to_string(pg_catalog.pg_get_statisticsobjdef_columns(es.oid), ', '), \n"
+ " es.stxrelid::pg_catalog.regclass) AS \"%s\"",
+ gettext_noop("Definition"));
+ else if (pset.sversion >= 140000)
appendPQExpBuffer(&buf,
"pg_catalog.format('%%s FROM %%s', \n"
" pg_catalog.pg_get_statisticsobjdef_columns(es.oid), \n"
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..7b6821ff16a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4001,9 +4001,9 @@
proname => 'pg_get_statisticsobjdef', provolatile => 's',
prorettype => 'text', proargtypes => 'oid',
prosrc => 'pg_get_statisticsobjdef' },
-{ oid => '6174', descr => 'extended statistics columns',
+{ oid => '6174', descr => 'extended statistics columns and expressions',
proname => 'pg_get_statisticsobjdef_columns', provolatile => 's',
- prorettype => 'text', proargtypes => 'oid',
+ prorettype => '_text', proargtypes => 'oid',
prosrc => 'pg_get_statisticsobjdef_columns' },
{ oid => '6173', descr => 'extended statistics expressions',
proname => 'pg_get_statisticsobjdef_expressions', provolatile => 's',
diff --git a/src/include/catalog/pg_statistic_ext.h b/src/include/catalog/pg_statistic_ext.h
index e4a0cb4d41c..e8757bc6d76 100644
--- a/src/include/catalog/pg_statistic_ext.h
+++ b/src/include/catalog/pg_statistic_ext.h
@@ -46,19 +46,14 @@ CATALOG(pg_statistic_ext,3381,StatisticExtRelationId)
Oid stxowner BKI_LOOKUP(pg_authid); /* statistics object's owner */
- /*
- * variable-length/nullable fields start here, but we allow direct access
- * to stxkeys
- */
- int2vector stxkeys BKI_FORCE_NOT_NULL; /* array of column keys */
-
#ifdef CATALOG_VARLEN
int16 stxstattarget BKI_DEFAULT(_null_) BKI_FORCE_NULL; /* statistics target */
char stxkind[1] BKI_FORCE_NOT_NULL; /* statistics kinds requested
* to build */
- pg_node_tree stxexprs; /* A list of expression trees for stats
- * attributes that are not simple column
- * references. */
+ pg_node_tree stxexprs BKI_FORCE_NOT_NULL; /* A list of expression trees
+ * for all stats attributes,
+ * including simple column
+ * references as Var nodes */
#endif
} FormData_pg_statistic_ext;
@@ -81,7 +76,6 @@ DECLARE_INDEX(pg_statistic_ext_relid_index, 3379, StatisticExtRelidIndexId, pg_s
MAKE_SYSCACHE(STATEXTOID, pg_statistic_ext_oid_index, 4);
MAKE_SYSCACHE(STATEXTNAMENSP, pg_statistic_ext_name_index, 4);
-DECLARE_ARRAY_FOREIGN_KEY((stxrelid, stxkeys), pg_attribute, (attrelid, attnum));
#ifdef EXPOSE_TO_CLIENT_CODE
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index c775442f2ee..d92f735cec1 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -73,7 +73,7 @@ extern MVNDistinct *statext_ndistinct_build(double totalrows, StatsBuildData *da
extern bytea *statext_ndistinct_serialize(MVNDistinct *ndistinct);
extern MVNDistinct *statext_ndistinct_deserialize(bytea *data);
extern bool statext_ndistinct_validate(const MVNDistinct *ndistinct,
- const int2vector *stxkeys,
+ Bitmapset *keys,
int numexprs, int elevel);
extern void statext_ndistinct_free(MVNDistinct *ndistinct);
@@ -81,10 +81,13 @@ extern MVDependencies *statext_dependencies_build(StatsBuildData *data);
extern bytea *statext_dependencies_serialize(MVDependencies *dependencies);
extern MVDependencies *statext_dependencies_deserialize(bytea *data);
extern bool statext_dependencies_validate(const MVDependencies *dependencies,
- const int2vector *stxkeys,
+ Bitmapset *keys,
int numexprs, int elevel);
extern void statext_dependencies_free(MVDependencies *dependencies);
+extern bool statext_is_valid_attnum(AttrNumber attnum, Bitmapset *keys,
+ int numexprs);
+
extern MCVList *statext_mcv_build(StatsBuildData *data,
double totalrows, int stattarget);
extern bytea *statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats);
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 8f9b9d237fd..a6c5a07dca6 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,5 +127,7 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
List **clause_exprs,
int nclauses);
extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
+extern void statext_decode_stxexprs(HeapTuple htup, Relation rel,
+ Bitmapset **keys, List **exprs);
#endif /* STATISTICS_H */
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index 76069bde756..057fcf91469 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -699,7 +699,7 @@ SELECT attname, attcompression FROM pg_attribute
(5 rows)
-- LIKE ... INCLUDING STATISTICS with dropped columns in the parent,
--- so stxkeys attnums are not contiguous.
+-- so column attnums are not contiguous.
CREATE TABLE ctl_stats3_parent (a int, b int, c int);
ALTER TABLE ctl_stats3_parent DROP COLUMN b;
CREATE STATISTICS ctl_stats3_stat ON a, c FROM ctl_stats3_parent;
@@ -709,15 +709,10 @@ ALTER TABLE ctl_stats4_parent DROP COLUMN b;
CREATE STATISTICS ctl_stats4_stat ON a, c FROM ctl_stats4_parent;
CREATE TABLE ctl_stats4_child (LIKE ctl_stats4_parent INCLUDING STATISTICS);
SELECT s.stxrelid::regclass AS relation,
- array_agg(a.attname ORDER BY u.ord) AS stats_columns
+ pg_get_statisticsobjdef_columns(s.oid) AS stats_columns
FROM pg_statistic_ext s
-CROSS JOIN LATERAL
- unnest(s.stxkeys::int2[]) WITH ORDINALITY AS u(attnum, ord)
-JOIN pg_attribute a
- ON a.attrelid = s.stxrelid AND a.attnum = u.attnum
WHERE s.stxrelid IN ('ctl_stats3_child'::regclass,
'ctl_stats4_child'::regclass)
-GROUP BY s.stxrelid
ORDER BY s.stxrelid::regclass::text;
relation | stats_columns
------------------+---------------
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index d64169b7bf0..0e6bce84a60 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -166,7 +166,6 @@ NOTICE: checking pg_statistic {starelid,staattnum} => pg_attribute {attrelid,at
NOTICE: checking pg_statistic_ext {stxrelid} => pg_class {oid}
NOTICE: checking pg_statistic_ext {stxnamespace} => pg_namespace {oid}
NOTICE: checking pg_statistic_ext {stxowner} => pg_authid {oid}
-NOTICE: checking pg_statistic_ext {stxrelid,stxkeys} => pg_attribute {attrelid,attnum}
NOTICE: checking pg_statistic_ext_data {stxoid} => pg_statistic_ext {oid}
NOTICE: checking pg_rewrite {ev_class} => pg_class {oid}
NOTICE: checking pg_trigger {tgrelid} => pg_class {oid}
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..e0746360263 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2704,10 +2704,7 @@ pg_stats_ext| SELECT cn.nspname AS schemaname,
s.stxname AS statistics_name,
s.oid AS statistics_id,
pg_get_userbyid(s.stxowner) AS statistics_owner,
- ( SELECT array_agg(a.attname ORDER BY a.attnum) AS array_agg
- FROM (unnest(s.stxkeys) k(k)
- JOIN pg_attribute a ON (((a.attrelid = s.stxrelid) AND (a.attnum = k.k))))) AS attnames,
- pg_get_statisticsobjdef_expressions(s.oid) AS exprs,
+ pg_get_statisticsobjdef_columns(s.oid) AS exprs,
s.stxkind AS kinds,
sd.stxdinherit AS inherited,
sd.stxdndistinct AS n_distinct,
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 37070c1a896..3f8d69446ff 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -43,7 +43,7 @@ ERROR: relation "nonexistent" does not exist
CREATE STATISTICS tst ON a, b FROM ext_stats_test;
ERROR: column "a" does not exist
CREATE STATISTICS tst ON x, x, y FROM ext_stats_test;
-ERROR: duplicate column name in statistics definition
+ERROR: duplicate expression in statistics definition
CREATE STATISTICS tst ON x, x, y, x, x, y, x, x, y FROM ext_stats_test;
ERROR: cannot have more than 8 columns in statistics
CREATE STATISTICS tst ON x, x, y, x, x, (x || 'x'), (y + 1), (x || 'x'), (x || 'x'), (y + 1) FROM ext_stats_test;
@@ -3177,6 +3177,23 @@ SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE d = 0
10 | 10
(1 row)
+SELECT expr FROM pg_stats_ext_exprs
+ WHERE statistics_name = 'virtual_gen_stats_1' AND NOT inherited;
+ expr
+---------------
+ c
+ (3 * b)
+ d
+ (d - (2 * a))
+(4 rows)
+
+SELECT pg_get_statisticsobjdef(oid) FROM pg_statistic_ext
+ WHERE stxname = 'virtual_gen_stats_1';
+ pg_get_statisticsobjdef
+---------------------------------------------------------------------------------------------------------
+ CREATE STATISTICS public.virtual_gen_stats_1 (mcv) ON c, (3 * b), d, (d - 2 * a) FROM virtual_gen_stats
+(1 row)
+
-- univariate statistics on individual virtual generated columns
DROP STATISTICS virtual_gen_stats_1;
SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0');
@@ -3692,12 +3709,12 @@ CREATE STATISTICS stats_ext_range (mcv)
ON irange, (irange + '[4,10)'::int4range)
FROM stats_ext_tbl_range;
ANALYZE stats_ext_tbl_range;
-SELECT attnames, most_common_vals
+SELECT exprs, most_common_vals
FROM pg_stats_ext
WHERE statistics_name = 'stats_ext_range';
- attnames | most_common_vals
-----------+------------------------------------------------------------
- {irange} | {{"[1,7)","[1,10)"},{"[2,9)","[2,10)"},{"[3,9)","[3,10)"}}
+ exprs | most_common_vals
+-------------------------------------------+------------------------------------------------------------
+ {irange,"(irange + '[4,10)'::int4range)"} | {{"[1,7)","[1,10)"},{"[2,9)","[2,10)"},{"[3,9)","[3,10)"}}
(1 row)
SELECT range_length_histogram, range_empty_frac, range_bounds_histogram
@@ -3709,3 +3726,15 @@ SELECT range_length_histogram, range_empty_frac, range_bounds_histogram
(1 row)
DROP TABLE stats_ext_tbl_range;
+-- Verify user-written ordering of columns and expressions is preserved.
+-- Before PG20, columns were sorted by attnum with expressions at the end.
+-- Leave for pg_upgrade testing.
+CREATE STATISTICS tenk1_mcv (mcv) ON ten, (ten + four), four FROM tenk1;
+ANALYZE tenk1;
+SELECT pg_get_statisticsobjdef(oid) FROM pg_statistic_ext
+ WHERE stxname = 'tenk1_mcv';
+ pg_get_statisticsobjdef
+--------------------------------------------------------------------------------
+ CREATE STATISTICS public.tenk1_mcv (mcv) ON ten, (ten + four), four FROM tenk1
+(1 row)
+
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index dabf9ba1cd8..900b6bbf995 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -78,7 +78,7 @@ SELECT COUNT(*) FROM pg_attribute
attnum > 0;
count
-------
- 17
+ 16
(1 row)
-- Create a view that is used purely for the type based on pg_stats_ext.
@@ -3450,6 +3450,72 @@ FROM stats_import.pg_stats_ext_exprs_get_difference('test_mr_stat', 'test_mr_sta
\gx
(0 rows)
+-- Test import of extended statistics on virtual generated columns.
+-- Virtual gen cols are stored as Var nodes in stxexprs but treated as
+-- expressions during ANALYZE (they receive per-expression stats in stxdexpr).
+CREATE TABLE stats_import.test_vgencol(
+ a int,
+ b int,
+ c int GENERATED ALWAYS AS (a + b) VIRTUAL
+);
+INSERT INTO stats_import.test_vgencol(a, b)
+ SELECT mod(i, 10), mod(i, 7) FROM generate_series(1, 1000) s(i);
+CREATE STATISTICS stats_import.stat_vgencol ON a, c
+ FROM stats_import.test_vgencol;
+ANALYZE stats_import.test_vgencol;
+CREATE TABLE stats_import.test_vgencol_clone(
+ a int,
+ b int,
+ c int GENERATED ALWAYS AS (a + b) VIRTUAL
+);
+CREATE STATISTICS stats_import.stat_vgencol_clone ON a, c
+ FROM stats_import.test_vgencol_clone;
+-- Import stats from stat_vgencol to stat_vgencol_clone
+SELECT e.statistics_name,
+ pg_catalog.pg_restore_extended_stats(
+ 'schemaname', e.statistics_schemaname::text,
+ 'relname', 'test_vgencol_clone',
+ 'statistics_schemaname', e.statistics_schemaname::text,
+ 'statistics_name', 'stat_vgencol_clone',
+ 'inherited', e.inherited,
+ 'n_distinct', e.n_distinct,
+ 'dependencies', e.dependencies,
+ 'most_common_vals', e.most_common_vals,
+ 'most_common_freqs', e.most_common_freqs,
+ 'most_common_base_freqs', e.most_common_base_freqs,
+ 'exprs', x.exprs)
+FROM pg_stats_ext AS e
+CROSS JOIN LATERAL (
+ SELECT jsonb_agg(jsonb_strip_nulls(jsonb_build_object(
+ 'null_frac', ee.null_frac::text,
+ 'avg_width', ee.avg_width::text,
+ 'n_distinct', ee.n_distinct::text,
+ 'most_common_vals', ee.most_common_vals::text,
+ 'most_common_freqs', ee.most_common_freqs::text,
+ 'histogram_bounds', ee.histogram_bounds::text,
+ 'correlation', ee.correlation::text)))
+ FROM pg_stats_ext_exprs AS ee
+ WHERE ee.statistics_schemaname = e.statistics_schemaname AND
+ ee.statistics_name = e.statistics_name AND
+ ee.inherited = e.inherited
+ ) AS x(exprs)
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'stat_vgencol';
+ statistics_name | pg_restore_extended_stats
+-----------------+---------------------------
+ stat_vgencol | t
+(1 row)
+
+SELECT statname, (stats).*
+FROM stats_import.pg_stats_ext_get_difference('stat_vgencol', 'stat_vgencol_clone')
+\gx
+(0 rows)
+
+SELECT statname, (stats).*
+FROM stats_import.pg_stats_ext_exprs_get_difference('stat_vgencol', 'stat_vgencol_clone')
+\gx
+(0 rows)
+
-- range_length_histogram, range_empty_frac, and range_bounds_histogram
-- have been added to pg_stats_ext_exprs in PostgreSQL 19. When dumping
-- expression statistics in a cluster with an older version, these fields
@@ -3536,7 +3602,7 @@ SELECT COUNT(*) FROM stats_import.test_range_expr_null
(1 row)
DROP SCHEMA stats_import CASCADE;
-NOTICE: drop cascades to 19 other objects
+NOTICE: drop cascades to 21 other objects
DETAIL: drop cascades to view stats_import.pg_stats_stable
drop cascades to view stats_import.pg_statistic_flat_t
drop cascades to function stats_import.pg_statistic_flat(text)
@@ -3555,4 +3621,6 @@ drop cascades to sequence stats_import.testseq
drop cascades to view stats_import.testview
drop cascades to table stats_import.test_clone
drop cascades to table stats_import.test_mr_clone
+drop cascades to table stats_import.test_vgencol
+drop cascades to table stats_import.test_vgencol_clone
drop cascades to table stats_import.test_range_expr_null
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index d52a93ef131..864f54d0e6f 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -277,7 +277,7 @@ SELECT attname, attcompression FROM pg_attribute
WHERE attrelid = 'ctl_foreign_table2'::regclass and attnum > 0 ORDER BY attnum;
-- LIKE ... INCLUDING STATISTICS with dropped columns in the parent,
--- so stxkeys attnums are not contiguous.
+-- so column attnums are not contiguous.
CREATE TABLE ctl_stats3_parent (a int, b int, c int);
ALTER TABLE ctl_stats3_parent DROP COLUMN b;
CREATE STATISTICS ctl_stats3_stat ON a, c FROM ctl_stats3_parent;
@@ -287,15 +287,10 @@ ALTER TABLE ctl_stats4_parent DROP COLUMN b;
CREATE STATISTICS ctl_stats4_stat ON a, c FROM ctl_stats4_parent;
CREATE TABLE ctl_stats4_child (LIKE ctl_stats4_parent INCLUDING STATISTICS);
SELECT s.stxrelid::regclass AS relation,
- array_agg(a.attname ORDER BY u.ord) AS stats_columns
+ pg_get_statisticsobjdef_columns(s.oid) AS stats_columns
FROM pg_statistic_ext s
-CROSS JOIN LATERAL
- unnest(s.stxkeys::int2[]) WITH ORDINALITY AS u(attnum, ord)
-JOIN pg_attribute a
- ON a.attrelid = s.stxrelid AND a.attnum = u.attnum
WHERE s.stxrelid IN ('ctl_stats3_child'::regclass,
'ctl_stats4_child'::regclass)
-GROUP BY s.stxrelid
ORDER BY s.stxrelid::regclass::text;
DROP TABLE ctl_stats3_parent;
DROP TABLE ctl_stats3_child;
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index 3cc6012b822..372d4a37bd1 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -1593,6 +1593,12 @@ ANALYZE virtual_gen_stats;
SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0 AND (3*b) = 0');
SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE d = 0 AND (d-2*a) = 0');
+SELECT expr FROM pg_stats_ext_exprs
+ WHERE statistics_name = 'virtual_gen_stats_1' AND NOT inherited;
+
+SELECT pg_get_statisticsobjdef(oid) FROM pg_statistic_ext
+ WHERE stxname = 'virtual_gen_stats_1';
+
-- univariate statistics on individual virtual generated columns
DROP STATISTICS virtual_gen_stats_1;
@@ -1901,10 +1907,18 @@ CREATE STATISTICS stats_ext_range (mcv)
ON irange, (irange + '[4,10)'::int4range)
FROM stats_ext_tbl_range;
ANALYZE stats_ext_tbl_range;
-SELECT attnames, most_common_vals
+SELECT exprs, most_common_vals
FROM pg_stats_ext
WHERE statistics_name = 'stats_ext_range';
SELECT range_length_histogram, range_empty_frac, range_bounds_histogram
FROM pg_stats_ext_exprs
WHERE statistics_name = 'stats_ext_range';
DROP TABLE stats_ext_tbl_range;
+
+-- Verify user-written ordering of columns and expressions is preserved.
+-- Before PG20, columns were sorted by attnum with expressions at the end.
+-- Leave for pg_upgrade testing.
+CREATE STATISTICS tenk1_mcv (mcv) ON ten, (ten + four), four FROM tenk1;
+ANALYZE tenk1;
+SELECT pg_get_statisticsobjdef(oid) FROM pg_statistic_ext
+ WHERE stxname = 'tenk1_mcv';
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 58140315efb..070e9759f17 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -2400,6 +2400,66 @@ SELECT statname, (stats).*
FROM stats_import.pg_stats_ext_exprs_get_difference('test_mr_stat', 'test_mr_stat_clone')
\gx
+-- Test import of extended statistics on virtual generated columns.
+-- Virtual gen cols are stored as Var nodes in stxexprs but treated as
+-- expressions during ANALYZE (they receive per-expression stats in stxdexpr).
+CREATE TABLE stats_import.test_vgencol(
+ a int,
+ b int,
+ c int GENERATED ALWAYS AS (a + b) VIRTUAL
+);
+INSERT INTO stats_import.test_vgencol(a, b)
+ SELECT mod(i, 10), mod(i, 7) FROM generate_series(1, 1000) s(i);
+CREATE STATISTICS stats_import.stat_vgencol ON a, c
+ FROM stats_import.test_vgencol;
+ANALYZE stats_import.test_vgencol;
+CREATE TABLE stats_import.test_vgencol_clone(
+ a int,
+ b int,
+ c int GENERATED ALWAYS AS (a + b) VIRTUAL
+);
+CREATE STATISTICS stats_import.stat_vgencol_clone ON a, c
+ FROM stats_import.test_vgencol_clone;
+-- Import stats from stat_vgencol to stat_vgencol_clone
+SELECT e.statistics_name,
+ pg_catalog.pg_restore_extended_stats(
+ 'schemaname', e.statistics_schemaname::text,
+ 'relname', 'test_vgencol_clone',
+ 'statistics_schemaname', e.statistics_schemaname::text,
+ 'statistics_name', 'stat_vgencol_clone',
+ 'inherited', e.inherited,
+ 'n_distinct', e.n_distinct,
+ 'dependencies', e.dependencies,
+ 'most_common_vals', e.most_common_vals,
+ 'most_common_freqs', e.most_common_freqs,
+ 'most_common_base_freqs', e.most_common_base_freqs,
+ 'exprs', x.exprs)
+FROM pg_stats_ext AS e
+CROSS JOIN LATERAL (
+ SELECT jsonb_agg(jsonb_strip_nulls(jsonb_build_object(
+ 'null_frac', ee.null_frac::text,
+ 'avg_width', ee.avg_width::text,
+ 'n_distinct', ee.n_distinct::text,
+ 'most_common_vals', ee.most_common_vals::text,
+ 'most_common_freqs', ee.most_common_freqs::text,
+ 'histogram_bounds', ee.histogram_bounds::text,
+ 'correlation', ee.correlation::text)))
+ FROM pg_stats_ext_exprs AS ee
+ WHERE ee.statistics_schemaname = e.statistics_schemaname AND
+ ee.statistics_name = e.statistics_name AND
+ ee.inherited = e.inherited
+ ) AS x(exprs)
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'stat_vgencol';
+
+SELECT statname, (stats).*
+FROM stats_import.pg_stats_ext_get_difference('stat_vgencol', 'stat_vgencol_clone')
+\gx
+
+SELECT statname, (stats).*
+FROM stats_import.pg_stats_ext_exprs_get_difference('stat_vgencol', 'stat_vgencol_clone')
+\gx
+
-- range_length_histogram, range_empty_frac, and range_bounds_histogram
-- have been added to pg_stats_ext_exprs in PostgreSQL 19. When dumping
-- expression statistics in a cluster with an older version, these fields
--
2.50.1 (Apple Git-155)