Subject: Re: Statistics Import and Export

From: Ashutosh Bapat <[email-masked]>
Date: Thu, 31 Aug 2023 12:37:12 +0530

On Thu, Aug 31, 2023 at 12:17 PM Corey Huinker <[email-masked]> wrote:
>
> While the primary purpose of the import function(s) are to reduce downtime
> during an upgrade, it is not hard to see that they could also be used to
> facilitate tuning and development operations, asking questions like "how might
> this query plan change if this table has 1000x rows in it?", without actually
> putting those rows into the table.

Thanks. I think this may be used with postgres_fdw to import
statistics directly from the foreigns server, whenever possible,
rather than fetching the rows and building it locally. If it's known
that the stats on foreign and local servers match for a foreign table,
we will be one step closer to accurately estimating the cost of a
foreign plan locally rather than through EXPLAIN.

-- 
Best Wishes,
Ashutosh Bapat

---

From: Corey Huinker <[email-masked]>
Date: Thu, 31 Aug 2023 17:18:32 -0400

> foreign plan locally rather than through EXPLAIN.
Yeah, that use makes sense as well, and if so then postgres_fdw would
likely need to be aware of the appropriate query for several versions back
- they change, not by much, but they do change. So now we'd have each query
text in three places: a system view, postgres_fdw, and the bin/scripts
pre-upgrade program. So I probably should consider the best way to share
those in the codebase.

---

From: Corey Huinker <[email-masked]>
Date: Tue, 31 Oct 2023 03:25:17 -0400

> pre-upgrade program. So I probably should consider the best way to share
Attached is v2 of this patch.

New features:
* imports index statistics. This is not strictly accurate: it re-computes
index statistics the same as ANALYZE does, which is to say it derives those
stats entirely from table column stats, which are imported, so in that
sense we're getting index stats without touching the heap.
* now support extended statistics except for MCV, which is currently
serialized as an difficult-to-decompose bytea field.
* bare-bones CLI script pg_export_stats, which extracts stats on databases
back to v12 (tested) and could work back to v10.
* bare-bones CLI script pg_import_stats, which obviously only works on
current devel dbs, but can take exports from older versions.
From 9bd1618db9e40caad773334921f0a43d9d63e73a Mon Sep 17 00:00:00 2001
From: coreyhuinker <[email-masked]>
Date: Mon, 30 Oct 2023 16:21:30 -0400
Subject: [PATCH v2 1/4] Additional internal jsonb access functions.

Make JsonbContainerTypeName externally visible.

Add JsonbStringValueToCString.
---
 src/include/utils/jsonb.h          |  4 ++++
 src/backend/utils/adt/jsonb.c      |  2 +-
 src/backend/utils/adt/jsonb_util.c | 15 +++++++++++++++
 3 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h
index addc9b608e..b3c1e104f2 100644
--- a/src/include/utils/jsonb.h
+++ b/src/include/utils/jsonb.h
@@ -424,6 +424,8 @@ extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in,
 								  int estimated_len);
 extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res);
 extern const char *JsonbTypeName(JsonbValue *val);
+extern const char *JsonbContainerTypeName(JsonbContainer *jbc);
+
 
 extern Datum jsonb_set_element(Jsonb *jb, Datum *path, int path_len,
 							   JsonbValue *newval);
@@ -436,4 +438,6 @@ extern Datum jsonb_build_object_worker(int nargs, const Datum *args, const bool
 extern Datum jsonb_build_array_worker(int nargs, const Datum *args, const bool *nulls,
 									  const Oid *types, bool absent_on_null);
 
+extern char *JsonbStringValueToCString(JsonbValue *j);
+
 #endif							/* __JSONB_H__ */
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 6f445f5c2b..0ad4e81d89 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -160,7 +160,7 @@ jsonb_from_text(text *js, bool unique_keys)
 /*
  * Get the type name of a jsonb container.
  */
-static const char *
+const char *
 JsonbContainerTypeName(JsonbContainer *jbc)
 {
 	JsonbValue	scalar;
diff --git a/src/backend/utils/adt/jsonb_util.c b/src/backend/utils/adt/jsonb_util.c
index 9cc95b773d..ae311b38ba 100644
--- a/src/backend/utils/adt/jsonb_util.c
+++ b/src/backend/utils/adt/jsonb_util.c
@@ -1992,3 +1992,18 @@ uniqueifyJsonbObject(JsonbValue *object, bool unique_keys, bool skip_nulls)
 		}
 	}
 }
+
+/*
+ * Extract a JsonbValue as a cstring.
+ */
+char *JsonbStringValueToCString(JsonbValue *j)
+{
+	char *s;
+
+	Assert(j->type == jbvString);
+	/* make a string that we are sure is null-terminated */
+	s = palloc(j->val.string.len + 1);
+	memcpy(s, j->val.string.val, j->val.string.len);
+	s[j->val.string.len] = '\0';
+	return s;
+}
-- 
2.41.0

From 920d546032a330fe1ec4c8f6a35c48a47ccd08b2 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 31 Oct 2023 02:27:17 -0400
Subject: [PATCH v2 2/4] Add system view pg_statistic_export.

This view is designed to aid in the export (and re-import) of table
statistics and extended statistics, mostly for upgrade/restore
situations.
---
 src/backend/catalog/system_views.sql | 215 +++++++++++++++++++++++++++
 src/test/regress/expected/rules.out  |  71 +++++++++
 doc/src/sgml/system-views.sgml       |   5 +
 3 files changed, 291 insertions(+)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index b65f6b5249..11a1037ceb 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -253,6 +253,221 @@ CREATE VIEW pg_stats WITH (security_barrier) AS
 
 REVOKE ALL ON pg_statistic FROM public;
 
+
+
+
+CREATE VIEW pg_statistic_export WITH (security_barrier) AS
+    SELECT
+        n.nspname AS schemaname,
+        r.relname AS relname,
+        current_setting('server_version_num')::integer AS server_version_num,
+        r.reltuples::float4 AS n_tuples,
+        r.relpages::integer AS n_pages,
+        (
+            WITH per_column_stats AS
+            (
+                SELECT
+                    s.stainherit,
+                    a.attname,
+                    jsonb_build_object(
+                        'stanullfrac', s.stanullfrac::text,
+                        'stawidth', s.stawidth::text,
+                        'stadistinct', s.stadistinct::text,
+                        'stakinds',
+                        (
+                            SELECT
+                                jsonb_agg(
+                                    CASE kind.kind
+                                        WHEN 0 THEN 'TRIVIAL'
+                                        WHEN 1 THEN 'MCV'
+                                        WHEN 2 THEN 'HISTOGRAM'
+                                        WHEN 3 THEN 'CORRELATION'
+                                        WHEN 4 THEN 'MCELEM'
+                                        WHEN 5 THEN 'DECHIST'
+                                        WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'
+                                        WHEN 7 THEN 'BOUNDS_HISTOGRAM'
+                                    END::text
+                                    ORDER BY kind.ord)
+                            FROM unnest(ARRAY[s.stakind1, s.stakind2,
+                                        s.stakind3, stakind4,
+                                        s.stakind5])
+                                 WITH ORDINALITY AS kind(kind, ord)
+                        ),
+                        'stanumbers',
+                        jsonb_build_array(
+                            s.stanumbers1::text::text[],
+                            s.stanumbers2::text::text[],
+                            s.stanumbers3::text::text[],
+                            s.stanumbers4::text::text[],
+                            s.stanumbers5::text::text[]),
+                        'stavalues',
+                        jsonb_build_array(
+                            s.stavalues1::text::text[],
+                            s.stavalues2::text::text[],
+                            s.stavalues3::text::text[],
+                            s.stavalues4::text::text[],
+                            s.stavalues5::text::text[])
+                    ) AS stats
+                FROM pg_attribute AS a
+                JOIN pg_statistic AS s
+                    ON s.starelid = a.attrelid
+                    AND s.staattnum = a.attnum
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+                AND has_column_privilege(a.attrelid, a.attnum, 'SELECT')
+            ),
+            attagg AS
+            (
+                SELECT
+                    pcs.stainherit,
+                    jsonb_build_object(
+                        'columns',
+                        jsonb_object_agg(
+                            pcs.attname,
+                            pcs.stats
+                        )
+                    ) AS stats
+                FROM per_column_stats AS pcs
+                GROUP BY pcs.stainherit
+            ),
+            extended_object_stats AS
+            (
+                SELECT
+                    sd.stxdinherit,
+                    e.stxname,
+                    jsonb_build_object(
+                        'stxkinds',
+                        to_jsonb(e.stxkind),
+                        'stxdndistinct',
+                        ndist.stxdndistinct,
+                        'stxdndependencies',
+                        ndep.stxdndependencies,
+                        'stxdmcv',
+                        mcv.stxdmcv,
+                        'stxdexprs',
+                        x.stdxdexprs
+                    ) AS stats
+                FROM pg_statistic_ext AS e
+                JOIN pg_statistic_ext_data AS sd
+                    ON sd.stxoid = e.oid
+                LEFT JOIN LATERAL
+                    (
+                        -- att1 [, att2..]: ndistinct
+                        SELECT
+                            jsonb_agg(
+                                jsonb_build_object(
+                                    'attnums', string_to_array(nd.attnums, ', '),
+                                    'ndistinct', nd.ndistinct
+                                    )
+                                ORDER BY nd.ord
+                            )
+                        -- jsonb does not preserve parsed order so use json
+                        FROM json_each_text(sd.stxdndistinct::text::json)
+                             WITH ORDINALITY AS nd(attnums, ndistinct, ord)
+                    ) AS ndist(stxdndistinct) ON sd.stxdndistinct IS NOT NULL
+                LEFT JOIN LATERAL
+                    (
+                        -- att1, [, att2 ...] => attN: degree
+                        SELECT
+                            jsonb_agg(
+                                jsonb_build_object(
+                                    'attnums',
+                                    string_to_array( replace(dep.attrs, ' => ', ', '), ', '),
+                                    'degree',
+                                    dep.degree
+                                    )
+                                ORDER BY dep.ord
+                            )
+                        -- jsonb does not preserve parsed order so use json
+                        FROM json_each_text(sd.stxddependencies::text::json)
+                             WITH ORDINALITY AS dep(attrs, degree, ord)
+                    ) AS ndep(stxdndependencies) ON sd.stxddependencies IS NOT NULL
+                LEFT JOIN LATERAL
+                    (
+                        -- TODO SELECT sd.stxdmcv
+                        SELECT NULL AS stxdmcv
+                    ) AS mcv(stxdmcv) ON sd.stxdmcv IS NOT NULL
+                LEFT JOIN LATERAL
+                    (
+                        SELECT
+                            jsonb_agg(
+                                jsonb_build_object(
+                                    'stanullfrac', s.stanullfrac::text,
+                                    'stawidth', s.stawidth::text,
+                                    'stadistinct', s.stadistinct::text,
+                                    'stakinds',
+                                    (
+                                        SELECT
+                                            jsonb_agg(
+                                                CASE kind.kind
+                                                    WHEN 0 THEN 'TRIVIAL'
+                                                    WHEN 1 THEN 'MCV'
+                                                    WHEN 2 THEN 'HISTOGRAM'
+                                                    WHEN 3 THEN 'CORRELATION'
+                                                    WHEN 4 THEN 'MCELEM'
+                                                    WHEN 5 THEN 'DECHIST'
+                                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'
+                                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM'
+                                                END::text
+                                                ORDER BY kind.ord)
+                                        FROM unnest(ARRAY[s.stakind1, s.stakind2,
+                                                    s.stakind3, stakind4,
+                                                    s.stakind5]) WITH ORDINALITY AS kind(kind, ord)
+                                    ),
+                                    'stanumbers',
+                                    jsonb_build_array(
+                                        s.stanumbers1::text::text[],
+                                        s.stanumbers2::text::text[],
+                                        s.stanumbers3::text::text[],
+                                        s.stanumbers4::text::text[],
+                                        s.stanumbers5::text::text[]),
+                                    'stavalues',
+                                    jsonb_build_array(
+                                        s.stavalues1::text::text[],
+                                        s.stavalues2::text::text[],
+                                        s.stavalues3::text::text[],
+                                        s.stavalues4::text::text[],
+                                        s.stavalues5::text::text[])
+                                )
+                                ORDER BY s.ordinality
+                            )
+                        FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s
+                    ) AS x(stdxdexprs) ON sd.stxdexpr IS NOT NULL
+                WHERE e.stxrelid = r.oid
+            ),
+            extagg AS
+            (
+                SELECT
+                    eos.stxdinherit,
+                    jsonb_build_object(
+                        'extended',
+                        jsonb_object_agg(
+                            eos.stxname,
+                            eos.stats
+                        )
+                    ) AS stats
+                FROM extended_object_stats AS eos
+                GROUP BY eos.stxdinherit
+            )
+            SELECT
+                jsonb_object_agg(
+                    CASE coalesce(a.stainherit, e.stxdinherit)
+                        WHEN TRUE THEN 'inherited'
+                        ELSE 'regular'
+                    END,
+                    coalesce(a.stats, '{}'::jsonb) || coalesce(e.stats, '{}'::jsonb)
+                )
+            FROM attagg AS a
+            FULL OUTER JOIN extagg e ON a.stainherit = e.stxdinherit
+        ) AS stats
+    FROM pg_class AS r
+    JOIN pg_namespace AS n
+        ON n.oid = r.relnamespace
+    WHERE relkind IN ('r', 'm', 'f', 'p')
+    AND n.nspname NOT IN ('pg_catalog', 'information_schema');
+
+
 CREATE VIEW pg_stats_ext WITH (security_barrier) AS
     SELECT cn.nspname AS schemaname,
            c.relname AS tablename,
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 1442c43d9c..20ab4da385 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2404,6 +2404,77 @@ pg_statio_user_tables| SELECT relid,
     tidx_blks_hit
    FROM pg_statio_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_statistic_export| SELECT n.nspname AS schemaname,
+    r.relname,
+    (current_setting('server_version_num'::text))::integer AS server_version_num,
+    r.reltuples AS n_tuples,
+    r.relpages AS n_pages,
+    ( WITH per_column_stats AS (
+                 SELECT s.stainherit,
+                    a_1.attname,
+                    jsonb_build_object('stanullfrac', (s.stanullfrac)::text, 'stawidth', (s.stawidth)::text, 'stadistinct', (s.stadistinct)::text, 'stakinds', ( SELECT jsonb_agg(
+                                CASE kind.kind
+                                    WHEN 0 THEN 'TRIVIAL'::text
+                                    WHEN 1 THEN 'MCV'::text
+                                    WHEN 2 THEN 'HISTOGRAM'::text
+                                    WHEN 3 THEN 'CORRELATION'::text
+                                    WHEN 4 THEN 'MCELEM'::text
+                                    WHEN 5 THEN 'DECHIST'::text
+                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'::text
+                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM'::text
+                                    ELSE NULL::text
+                                END ORDER BY kind.ord) AS jsonb_agg
+                           FROM unnest(ARRAY[s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5]) WITH ORDINALITY kind(kind, ord)), 'stanumbers', jsonb_build_array(((s.stanumbers1)::text)::text[], ((s.stanumbers2)::text)::text[], ((s.stanumbers3)::text)::text[], ((s.stanumbers4)::text)::text[], ((s.stanumbers5)::text)::text[]), 'stavalues', jsonb_build_array(((s.stavalues1)::text)::text[], ((s.stavalues2)::text)::text[], ((s.stavalues3)::text)::text[], ((s.stavalues4)::text)::text[], ((s.stavalues5)::text)::text[])) AS stats
+                   FROM (pg_attribute a_1
+                     JOIN pg_statistic s ON (((s.starelid = a_1.attrelid) AND (s.staattnum = a_1.attnum))))
+                  WHERE ((a_1.attrelid = r.oid) AND (NOT a_1.attisdropped) AND (a_1.attnum > 0) AND has_column_privilege(a_1.attrelid, a_1.attnum, 'SELECT'::text))
+                ), attagg AS (
+                 SELECT pcs.stainherit,
+                    jsonb_build_object('columns', jsonb_object_agg(pcs.attname, pcs.stats)) AS stats
+                   FROM per_column_stats pcs
+                  GROUP BY pcs.stainherit
+                ), extended_object_stats AS (
+                 SELECT sd.stxdinherit,
+                    e_1.stxname,
+                    jsonb_build_object('stxkinds', to_jsonb(e_1.stxkind), 'stxdndistinct', ndist.stxdndistinct, 'stxdndependencies', ndep.stxdndependencies, 'stxdmcv', mcv.stxdmcv, 'stxdexprs', x.stdxdexprs) AS stats
+                   FROM (((((pg_statistic_ext e_1
+                     JOIN pg_statistic_ext_data sd ON ((sd.stxoid = e_1.oid)))
+                     LEFT JOIN LATERAL ( SELECT jsonb_agg(jsonb_build_object('attnums', string_to_array(nd.attnums, ', '::text), 'ndistinct', nd.ndistinct) ORDER BY nd.ord) AS jsonb_agg
+                           FROM json_each_text(((sd.stxdndistinct)::text)::json) WITH ORDINALITY nd(attnums, ndistinct, ord)) ndist(stxdndistinct) ON ((sd.stxdndistinct IS NOT NULL)))
+                     LEFT JOIN LATERAL ( SELECT jsonb_agg(jsonb_build_object('attnums', string_to_array(replace(dep.attrs, ' => '::text, ', '::text), ', '::text), 'degree', dep.degree) ORDER BY dep.ord) AS jsonb_agg
+                           FROM json_each_text(((sd.stxddependencies)::text)::json) WITH ORDINALITY dep(attrs, degree, ord)) ndep(stxdndependencies) ON ((sd.stxddependencies IS NOT NULL)))
+                     LEFT JOIN LATERAL ( SELECT NULL::text AS stxdmcv) mcv(stxdmcv) ON ((sd.stxdmcv IS NOT NULL)))
+                     LEFT JOIN LATERAL ( SELECT jsonb_agg(jsonb_build_object('stanullfrac', (s.stanullfrac)::text, 'stawidth', (s.stawidth)::text, 'stadistinct', (s.stadistinct)::text, 'stakinds', ( SELECT jsonb_agg(
+CASE kind.kind
+ WHEN 0 THEN 'TRIVIAL'::text
+ WHEN 1 THEN 'MCV'::text
+ WHEN 2 THEN 'HISTOGRAM'::text
+ WHEN 3 THEN 'CORRELATION'::text
+ WHEN 4 THEN 'MCELEM'::text
+ WHEN 5 THEN 'DECHIST'::text
+ WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'::text
+ WHEN 7 THEN 'BOUNDS_HISTOGRAM'::text
+ ELSE NULL::text
+END ORDER BY kind.ord) AS jsonb_agg
+                                   FROM unnest(ARRAY[s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5]) WITH ORDINALITY kind(kind, ord)), 'stanumbers', jsonb_build_array(((s.stanumbers1)::text)::text[], ((s.stanumbers2)::text)::text[], ((s.stanumbers3)::text)::text[], ((s.stanumbers4)::text)::text[], ((s.stanumbers5)::text)::text[]), 'stavalues', jsonb_build_array(((s.stavalues1)::text)::text[], ((s.stavalues2)::text)::text[], ((s.stavalues3)::text)::text[], ((s.stavalues4)::text)::text[], ((s.stavalues5)::text)::text[])) ORDER BY s.ordinality) AS jsonb_agg
+                           FROM unnest(sd.stxdexpr) WITH ORDINALITY s(starelid, staattnum, stainherit, stanullfrac, stawidth, stadistinct, stakind1, stakind2, stakind3, stakind4, stakind5, staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5, stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5, stavalues1, stavalues2, stavalues3, stavalues4, stavalues5, ordinality)) x(stdxdexprs) ON ((sd.stxdexpr IS NOT NULL)))
+                  WHERE (e_1.stxrelid = r.oid)
+                ), extagg AS (
+                 SELECT eos.stxdinherit,
+                    jsonb_build_object('extended', jsonb_object_agg(eos.stxname, eos.stats)) AS stats
+                   FROM extended_object_stats eos
+                  GROUP BY eos.stxdinherit
+                )
+         SELECT jsonb_object_agg(
+                CASE COALESCE(a.stainherit, e.stxdinherit)
+                    WHEN true THEN 'inherited'::text
+                    ELSE 'regular'::text
+                END, (COALESCE(a.stats, '{}'::jsonb) || COALESCE(e.stats, '{}'::jsonb))) AS jsonb_object_agg
+           FROM (attagg a
+             FULL JOIN extagg e ON ((a.stainherit = e.stxdinherit)))) AS stats
+   FROM (pg_class r
+     JOIN pg_namespace n ON ((n.oid = r.relnamespace)))
+  WHERE ((r.relkind = ANY (ARRAY['r'::"char", 'm'::"char", 'f'::"char", 'p'::"char"])) AND (n.nspname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])));
 pg_stats| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     a.attname,
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7078491c4c..24b44ab388 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -191,6 +191,11 @@
       <entry>extended planner statistics for expressions</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-stats"><structname>pg_stats_export</structname></link></entry>
+      <entry>planner statistics for export/upgrade purposes</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-tables"><structname>pg_tables</structname></link></entry>
       <entry>tables</entry>
-- 
2.41.0

From 05b0a3537765a85fda8b59912b658eec1e6b5a81 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 31 Oct 2023 03:12:45 -0400
Subject: [PATCH v2 3/4] Add pg_import_rel_stats().

The function pg_import_rel_stats imports rowcount, pagecount, and column
statistics for a given table, as well as column statistics for the
underlying indexes and extended statistics for any named statistics
objects for that table.

The most likely application of this function is to quickly apply stats
to a newly upgraded database faster than could be accomplished by
vacuumdb --analyze-in-stages.

The function takes a best-effort approach, skipping statistics that are
expected but omitted, skipping object that are specified but do not
exist on the target system. The goal is to get better-than-empty
statistics into the table quickly, so that business operations can
resume sooner.

It should be noted that index statistics _are_ rebuilt as normal, but
use the imported column statistics from the table, so it is not
necessary for the indexes on the target table to match the indexes on
the extracted source table.

The statistics applied are not locked in any way, and will be
overwritten by the next analyze, either explicit or via autovacuum.

Extended statistics are identified by name, so those names on the target
system must match the source. If not, they are skipped.

While the column, index column, and extended statistics are applied
transactionally, the changes to pg_class (reltuples and relpages) are
not. This decision was made to avoid bloat of pg_class and is in line
with the behavior of VACUUM.

The medium of exchange is jsonb, the format of which is specified in the
view pg_statistic_export. Obviously this view does not exist in older
versions of the database, but the view definition can be extracted and
adapted to older versions.

This function also allows for tweaking of table statistics in-place,
allowing the user to inflate rowcounts, skew histograms, etc, to see
what those changes will evoke from the query planner.
---
 src/include/catalog/pg_proc.dat               |   5 +
 src/include/commands/vacuum.h                 |   4 +
 .../statistics/extended_stats_internal.h      |   3 +
 src/include/statistics/statistics.h           |  10 +
 src/backend/commands/analyze.c                | 709 ++++++++++++++++--
 src/backend/statistics/dependencies.c         | 133 ++++
 src/backend/statistics/extended_stats.c       | 259 +++++++
 src/backend/statistics/mcv.c                  |   6 +
 src/backend/statistics/mvdistinct.c           | 120 +++
 src/test/regress/expected/vacuum.out          | 153 ++++
 src/test/regress/sql/vacuum.sql               | 146 ++++
 doc/src/sgml/func.sgml                        |  42 ++
 12 files changed, 1527 insertions(+), 63 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 568aa80d92..01aeb54fe3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5639,6 +5639,11 @@
   proname => 'pg_stat_get_db_checksum_last_failure', provolatile => 's',
   proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
   prosrc => 'pg_stat_get_db_checksum_last_failure' },
+{ oid => '3813',
+  descr => 'statistics: import to relation',
+  proname => 'pg_import_rel_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool', proargtypes => 'oid int4 float4 int4 jsonb',
+  prosrc => 'pg_import_rel_stats' },
 { oid => '3074', descr => 'statistics: last reset for a database',
   proname => 'pg_stat_get_db_stat_reset_time', provolatile => 's',
   proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 4af02940c5..72c31eb882 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -24,6 +24,7 @@
 #include "storage/buf.h"
 #include "storage/lock.h"
 #include "utils/relcache.h"
+#include "utils/jsonb.h"
 
 /*
  * Flags for amparallelvacuumoptions to control the participation of bulkdelete
@@ -386,4 +387,7 @@ extern double anl_random_fract(void);
 extern double anl_init_selection_state(int n);
 extern double anl_get_next_S(double t, int n, double *stateptr);
 
+extern Datum pg_import_rel_stats(PG_FUNCTION_ARGS);
+
+
 #endif							/* VACUUM_H */
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index 7b55eb8ffa..fdefaa28f5 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -72,15 +72,18 @@ typedef struct StatsBuildData
 extern MVNDistinct *statext_ndistinct_build(double totalrows, StatsBuildData *data);
 extern bytea *statext_ndistinct_serialize(MVNDistinct *ndistinct);
 extern MVNDistinct *statext_ndistinct_deserialize(bytea *data);
+extern MVNDistinct *import_ndistinct(JsonbContainer *cont);
 
 extern MVDependencies *statext_dependencies_build(StatsBuildData *data);
 extern bytea *statext_dependencies_serialize(MVDependencies *dependencies);
 extern MVDependencies *statext_dependencies_deserialize(bytea *data);
+extern MVDependencies * import_dependencies(JsonbContainer *cont);
 
 extern MCVList *statext_mcv_build(StatsBuildData *data,
 								  double totalrows, int stattarget);
 extern bytea *statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats);
 extern MCVList *statext_mcv_deserialize(bytea *data);
+extern MCVList *import_mcv(JsonbContainer *cont);
 
 extern MultiSortSupport multi_sort_init(int ndims);
 extern void multi_sort_add_dimension(MultiSortSupport mss, int sortdim,
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 5e538fec32..2b37e77887 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -101,6 +101,7 @@ extern MCVList *statext_mcv_load(Oid mvoid, bool inh);
 extern void BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
 									   int numrows, HeapTuple *rows,
 									   int natts, VacAttrStats **vacattrstats);
+
 extern int	ComputeExtStatisticsRows(Relation onerel,
 									 int natts, VacAttrStats **vacattrstats);
 extern bool statext_is_kind_built(HeapTuple htup, char type);
@@ -127,4 +128,13 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern void ImportVacAttrStats( VacAttrStats *stats, JsonbContainer *cont);
+extern void ImportRelationExtStatistics(Relation onerel, bool inh, int natts,
+										VacAttrStats **vacattrstats,
+										JsonbContainer *cont);
+
+extern char *key_lookup_cstring(JsonbContainer *cont, const char *key);
+extern JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key);
+extern JsonbContainer *key_lookup_array(JsonbContainer *cont, const char *key);
+
 #endif							/* STATISTICS_H */
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 206d1689ef..a9715c3eb3 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -18,6 +18,7 @@
 
 #include "access/detoast.h"
 #include "access/genam.h"
+#include "access/heapam.h"
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/sysattr.h"
@@ -33,6 +34,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_namespace.h"
+#include "catalog/pg_operator.h"
 #include "catalog/pg_statistic_ext.h"
 #include "commands/dbcommands.h"
 #include "commands/progress.h"
@@ -40,6 +42,7 @@
 #include "commands/vacuum.h"
 #include "common/pg_prng.h"
 #include "executor/executor.h"
+#include "fmgr.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
@@ -57,16 +60,20 @@
 #include "utils/attoptcache.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
+#include "utils/jsonb.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/numeric.h"
 #include "utils/pg_rusage.h"
 #include "utils/sampling.h"
 #include "utils/sortsupport.h"
 #include "utils/spccache.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
+#include "utils/typcache.h"

 /* Per-index data for ANALYZE */
@@ -109,6 +116,12 @@ static void update_attstats(Oid relid, bool inh,
 static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
 static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
 
+static AnlIndexData *build_indexdata(Relation onerel, Relation *Irel,
+									 int nindexes, bool all_columns);
+static VacAttrStats **examine_rel_attributes(Relation onerel, int *attr_cnt);
+
+static
+void import_pg_statistics(Relation onerel, bool inh, JsonbContainer *cont);
 
 /*
  *	analyze_rel() -- analyze one relation
@@ -403,29 +416,8 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 		attr_cnt = tcnt;
 	}
 	else
-	{
-		attr_cnt = onerel->rd_att->natts;
-		vacattrstats = (VacAttrStats **)
-			palloc(attr_cnt * sizeof(VacAttrStats *));
-		tcnt = 0;
-		for (i = 1; i <= attr_cnt; i++)
-		{
-			vacattrstats[tcnt] = examine_attribute(onerel, i, NULL);
-			if (vacattrstats[tcnt] != NULL)
-				tcnt++;
-		}
-		attr_cnt = tcnt;
-	}
+		vacattrstats = examine_rel_attributes(onerel, &attr_cnt);
 
-	/*
-	 * Open all indexes of the relation, and see if there are any analyzable
-	 * columns in the indexes.  We do not analyze index columns if there was
-	 * an explicit column list in the ANALYZE command, however.
-	 *
-	 * If we are doing a recursive scan, we don't want to touch the parent's
-	 * indexes at all.  If we're processing a partitioned table, we need to
-	 * know if there are any indexes, but we don't want to process them.
-	 */
 	if (onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
 	{
 		List	   *idxs = RelationGetIndexList(onerel);
@@ -446,48 +438,8 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 		nindexes = 0;
 		hasindex = false;
 	}
-	indexdata = NULL;
-	if (nindexes > 0)
-	{
-		indexdata = (AnlIndexData *) palloc0(nindexes * sizeof(AnlIndexData));
-		for (ind = 0; ind < nindexes; ind++)
-		{
-			AnlIndexData *thisdata = &indexdata[ind];
-			IndexInfo  *indexInfo;
 
-			thisdata->indexInfo = indexInfo = BuildIndexInfo(Irel[ind]);
-			thisdata->tupleFract = 1.0; /* fix later if partial */
-			if (indexInfo->ii_Expressions != NIL && va_cols == NIL)
-			{
-				ListCell   *indexpr_item = list_head(indexInfo->ii_Expressions);
-
-				thisdata->vacattrstats = (VacAttrStats **)
-					palloc(indexInfo->ii_NumIndexAttrs * sizeof(VacAttrStats *));
-				tcnt = 0;
-				for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
-				{
-					int			keycol = indexInfo->ii_IndexAttrNumbers[i];
-
-					if (keycol == 0)
-					{
-						/* Found an index expression */
-						Node	   *indexkey;
-
-						if (indexpr_item == NULL)	/* shouldn't happen */
-							elog(ERROR, "too few entries in indexprs list");
-						indexkey = (Node *) lfirst(indexpr_item);
-						indexpr_item = lnext(indexInfo->ii_Expressions,
-											 indexpr_item);
-						thisdata->vacattrstats[tcnt] =
-							examine_attribute(Irel[ind], i + 1, indexkey);
-						if (thisdata->vacattrstats[tcnt] != NULL)
-							tcnt++;
-					}
-				}
-				thisdata->attr_cnt = tcnt;
-			}
-		}
-	}
+	indexdata = build_indexdata(onerel, Irel, nindexes, (va_cols == NIL));
 
 	/*
 	 * Determine how many rows we need to sample, using the worst case from
@@ -823,6 +775,92 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 	anl_context = NULL;
 }
 
+
+/*
+ * Create VacAttrStats entries for all attributes in a relation.
+ */
+static
+VacAttrStats **examine_rel_attributes(Relation onerel, int *attr_cnt)
+{
+	int natts = onerel->rd_att->natts;
+	int nfound = 0;
+	int i;
+	VacAttrStats **attrstats;
+
+	*attr_cnt = natts;
+	attrstats = (VacAttrStats **) palloc(natts * sizeof(VacAttrStats *));
+	for (i = 1; i <= natts; i++)
+	{
+		attrstats[nfound] = examine_attribute(onerel, i, NULL);
+		if (attrstats[nfound] != NULL)
+			nfound++;
+	}
+	*attr_cnt = nfound;
+	return attrstats;
+}
+
+/*
+ * Open all indexes of the relation, and see if there are any analyzable
+ * columns in the indexes.  We do not analyze index columns if there was
+ * an explicit column list in the ANALYZE command, however.
+ *
+ * If we are doing a recursive scan, we don't want to touch the parent's
+ * indexes at all.  If we're processing a partitioned table, we need to
+ * know if there are any indexes, but we don't want to process them.
+ */
+static
+AnlIndexData *build_indexdata(Relation onerel, Relation *Irel, int nindexes, bool all_columns)
+{
+	AnlIndexData   *indexdata;
+	int				ind,
+					tcnt,
+					i;
+
+	if (nindexes == 0)
+		return NULL;
+
+	indexdata = (AnlIndexData *) palloc0(nindexes * sizeof(AnlIndexData));
+
+	for (ind = 0; ind < nindexes; ind++)
+	{
+		AnlIndexData *thisdata = &indexdata[ind];
+		IndexInfo  *indexInfo;
+
+		thisdata->indexInfo = indexInfo = BuildIndexInfo(Irel[ind]);
+		thisdata->tupleFract = 1.0; /* fix later if partial */
+		if (indexInfo->ii_Expressions != NIL && all_columns)
+		{
+			ListCell   *indexpr_item = list_head(indexInfo->ii_Expressions);
+
+			thisdata->vacattrstats = (VacAttrStats **)
+				palloc(indexInfo->ii_NumIndexAttrs * sizeof(VacAttrStats *));
+			tcnt = 0;
+			for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
+			{
+				int			keycol = indexInfo->ii_IndexAttrNumbers[i];
+
+				if (keycol == 0)
+				{
+					/* Found an index expression */
+					Node	   *indexkey;
+
+					if (indexpr_item == NULL)	/* shouldn't happen */
+						elog(ERROR, "too few entries in indexprs list");
+					indexkey = (Node *) lfirst(indexpr_item);
+					indexpr_item = lnext(indexInfo->ii_Expressions,
+										 indexpr_item);
+					thisdata->vacattrstats[tcnt] =
+						examine_attribute(Irel[ind], i + 1, indexkey);
+					if (thisdata->vacattrstats[tcnt] != NULL)
+						tcnt++;
+				}
+			}
+			thisdata->attr_cnt = tcnt;
+		}
+	}
+	return indexdata;
+}
+
 /*
  * Compute statistics about indexes of a relation
  */
@@ -3073,3 +3111,548 @@ analyze_mcv_list(int *mcv_counts,
 	}
 	return num_mcv;
 }
+
+/*
+ * Get a JsonbValue from a JsonbContainer and ensure that it is a string,
+ * and return the cstring.
+ */
+char *key_lookup_cstring(JsonbContainer *cont, const char *key)
+{
+	JsonbValue	j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvString)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be a string but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	return JsonbStringValueToCString(&j);
+}
+
+/*
+ * Get a JsonbContainer from a JsonbContainer and ensure that it is a object
+ */
+JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key)
+{
+	JsonbValue	j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvBinary)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an object but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	if (!JsonContainerIsObject(j.val.binary.data))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an object but is type %s",
+				  key, JsonbContainerTypeName(j.val.binary.data))));
+
+	return j.val.binary.data;
+}
+
+/*
+ * Get a JsonbContainer from a JsonbContainer and ensure that it is an array
+ */
+JsonbContainer *key_lookup_array(JsonbContainer *cont, const char *key)
+{
+	JsonbValue	j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvBinary)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an array but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	if (!JsonContainerIsArray(j.val.binary.data))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an array but is type %s",
+				  key, JsonbContainerTypeName(j.val.binary.data))));
+
+	return j.val.binary.data;
+}
+
+/*
+ * Import statistics from JSONB export into relation
+ *
+ * Format is:
+ *
+ * {
+ *   "columns": { "colname1": ... },
+ *   "extended": { "statname1": ...}
+ * }
+ */
+Datum
+pg_import_rel_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid;
+	int32		stats_version_num;
+	Jsonb	   *jb;
+	Relation	onerel;
+	Oid			save_userid;
+	int			save_sec_context;
+	int			save_nestlevel;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("relation cannot be NULL")));
+	relid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("server_version_number cannot be NULL")));
+	stats_version_num = PG_GETARG_INT32(1);
+
+	if (stats_version_num < 80000)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics version: %d is earlier than earliest supported version",
+				  stats_version_num)));
+
+	if (PG_ARGISNULL(4))
+		jb = NULL;
+	else
+	{
+		jb = PG_GETARG_JSONB_P(4);
+		if (!JB_ROOT_IS_OBJECT(jb))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("columns must be jsonb object at root")));
+	}
+
+	onerel = vacuum_open_relation(relid, NULL, VACOPT_ANALYZE, true,
+								  ShareUpdateExclusiveLock);
+
+	if (onerel == NULL)
+		PG_RETURN_BOOL(false);
+
+	if (!vacuum_is_relation_owner(RelationGetRelid(onerel),
+								onerel->rd_rel,
+								VACOPT_ANALYZE))
+	{
+		relation_close(onerel, ShareUpdateExclusiveLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Switch to the table owner's userid, so that any index functions are run
+	 * as that user.  Also lock down security-restricted operations and
+	 * arrange to make GUC variable changes local to this command.
+	 */
+	GetUserIdAndSecContext(&save_userid, &save_sec_context);
+	SetUserIdAndSecContext(onerel->rd_rel->relowner,
+						   save_sec_context | SECURITY_RESTRICTED_OPERATION);
+	save_nestlevel = NewGUCNestLevel();
+
+	/*
+	 * Apply statistical updates, if any, to copied tuple.
+	 *
+	 * Format is:
+	 * {
+	 *   "regular": { "columns": ..., "extended": ...},
+	 *   "inherited": { "columns": ..., "extended": ...}
+	 * }
+	 *
+	 */
+	if (jb != NULL)
+	{
+		JsonbContainer	   *cont;
+
+		cont = key_lookup_object(&jb->root, "regular");
+		import_pg_statistics(onerel, false, cont);
+
+		if (onerel->rd_rel->relhassubclass)
+		{
+			cont = key_lookup_object(&jb->root, "inherited");
+			import_pg_statistics(onerel, true, cont);
+		}
+	}
+
+	/* only modify pg_class row if changes are to be made */
+	if ( ! PG_ARGISNULL(2) || ! PG_ARGISNULL(3) )
+	{
+		Relation		pg_class_rel;
+		HeapTuple		ctup;
+		Form_pg_class	pgcform;
+
+		/*
+		 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+		 * other stat-setting operation can run on it concurrently.
+		 */
+		pg_class_rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+		/* leave if relation could not be opened or locked */
+		if (!pg_class_rel)
+			PG_RETURN_BOOL(false);
+
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		if (!HeapTupleIsValid(ctup))
+			elog(ERROR, "pg_class entry for relid %u vanished during statistics import",
+				 relid);
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+		/* leave un-set values alone */
+		if (! PG_ARGISNULL(2))
+			pgcform->reltuples = PG_GETARG_FLOAT4(2);
+		if (! PG_ARGISNULL(3))
+			pgcform->relpages = PG_GETARG_INT32(3);
+
+		heap_inplace_update(pg_class_rel, ctup);
+		table_close(pg_class_rel, ShareUpdateExclusiveLock);
+	}
+
+	/* relation_close(onerel, ShareUpdateExclusiveLock); */
+	relation_close(onerel, NoLock);
+
+	/* Roll back any GUC changes executed by index functions */
+	AtEOXact_GUC(false, save_nestlevel);
+
+	/* Restore userid and security context */
+	SetUserIdAndSecContext(save_userid, save_sec_context);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Convert the STATISTICS_KIND strings defined in pg_statistic_export
+ * back to their defined enum values.
+ */
+static int16
+decode_stakind_string(char *s)
+{
+	if (strcmp(s,"MCV") == 0)
+		return STATISTIC_KIND_MCV;
+	if (strcmp(s,"HISTOGRAM") == 0)
+		return STATISTIC_KIND_HISTOGRAM;
+	if (strcmp(s,"CORRELATION") == 0)
+		return STATISTIC_KIND_CORRELATION;
+	if (strcmp(s,"MCELEM") == 0)
+		return STATISTIC_KIND_MCELEM;
+	if (strcmp(s,"DECHIST") == 0)
+		return STATISTIC_KIND_DECHIST;
+	if (strcmp(s,"RANGE_LENGTH_HISTOGRAM") == 0)
+		return STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM;
+	if (strcmp(s,"BOUNDS_HISTOGRAM") == 0)
+		return STATISTIC_KIND_BOUNDS_HISTOGRAM;
+	if (strcmp(s,"TRIVIAL") != 0)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("unknown statistics kind: %s", s)));
+
+	return 0;
+}
+
+/*
+ *
+ * Format is:
+ *
+ * {
+ *   "regular":
+ *     {
+ *       "columns": { "colname1": ... }},
+ *       "extended": { "statname1": ...}
+ *     },
+ *   "inherited":
+ *     {
+ *       "columns": { "colname1": ... }},
+ *       "extended": { "statname1": ...}
+ *     },
+ * }
+ */
+static
+void import_pg_statistics(Relation onerel, bool inh, JsonbContainer *cont)
+{
+	VacAttrStats	  **vacattrstats;
+	JsonbContainer	   *colscont;
+	JsonbContainer	   *extscont;
+	int					natts;
+	AnlIndexData	   *indexdata = NULL;
+	int					nindexes = 0;
+	int					i,
+						ind;
+	Relation		   *Irel = NULL;
+
+	/* skip if no statistics of this inheritance type available */
+	if (cont == NULL)
+		return;
+
+	vacattrstats = examine_rel_attributes(onerel, &natts);
+
+	if ((!inh) &&
+		(onerel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE))
+	{
+		vac_open_indexes(onerel, AccessShareLock, &nindexes, &Irel);
+		if (nindexes > 0)
+			indexdata = build_indexdata(onerel, Irel, nindexes, true);
+	}
+
+	colscont = key_lookup_object(cont, "columns");
+	if (colscont != NULL)
+	{
+		for (i = 0; i < natts; i++)
+		{
+			VacAttrStats   *stats;
+			JsonbContainer *attrcont;
+			const char	   *name;
+
+			stats = vacattrstats[i];
+			name = NameStr(*attnumAttName(onerel, stats->tupattnum));
+
+			attrcont = key_lookup_object(colscont, name);
+			ImportVacAttrStats(stats, attrcont);
+		}
+		update_attstats(RelationGetRelid(onerel), inh, natts, vacattrstats);
+
+		/* now compute the index stats based on imported table stats */
+		for (ind = 0; ind < nindexes; ind++)
+		{
+			AnlIndexData *thisdata = &indexdata[ind];
+
+			update_attstats(RelationGetRelid(Irel[ind]), false,
+							thisdata->attr_cnt, thisdata->vacattrstats);
+		}
+	}
+
+	extscont = key_lookup_object(cont, "extended");
+
+	if (extscont != NULL)
+	{
+		/* Build extended statistics (if there are any). */
+
+		ImportRelationExtStatistics(onerel, false, natts, vacattrstats, extscont);
+
+		if (onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+			ImportRelationExtStatistics(onerel, true, natts, vacattrstats, extscont);
+	}
+
+	if ((!inh) &&
+		(onerel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE))
+		vac_close_indexes(nindexes, Irel, NoLock);
+}
+
+/*
+ * Apply all found statistics within a JSON structure to a pre-examined
+ * VacAttrStats.
+ */
+void
+ImportVacAttrStats( VacAttrStats *stats, JsonbContainer *cont)
+{
+	JsonbContainer *arraycont;
+	char		   *s;
+	int				k;
+
+	/* nothing to import, skip */
+	if (cont == NULL)
+		return;
+
+	stats->stats_valid = true;
+
+	s = key_lookup_cstring(cont, "stanullfrac");
+	if (s != NULL)
+	{
+		stats->stanullfrac = float4in_internal(s, NULL, "real", s, NULL);
+		pfree(s);
+	}
+	s = key_lookup_cstring(cont, "stawidth");
+	if (s != NULL)
+	{
+		stats->stawidth = pg_strtoint32(s);
+		pfree(s);
+	}
+
+	s = key_lookup_cstring(cont, "stadistinct");
+	if (s != NULL)
+	{
+		stats->stadistinct = float4in_internal(s, NULL, "real", s, NULL);
+		pfree(s);
+	}
+
+	arraycont = key_lookup_array(cont, "stakinds");
+	if (arraycont != NULL)
+	{
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			StdAnalyzeData *mystats;
+			JsonbValue	   *e;
+			char		   *stakindstr;
+
+			mystats = (StdAnalyzeData *) stats->extra_data;
+			e = getIthJsonbValueFromContainer(arraycont, k);
+
+			if (e == NULL || (e->type != jbvString))
+				ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("invalid format: stakind elements must be strings")));
+
+			stakindstr = JsonbStringValueToCString(e);
+			stats->stakind[k] = decode_stakind_string(stakindstr);
+			pfree(stakindstr);
+			pfree(e);
+
+			switch(stats->stakind[k])
+			{
+				case STATISTIC_KIND_MCV:
+				case STATISTIC_KIND_DECHIST:
+					stats->staop[k] = mystats->eqopr;
+					stats->stacoll[k] = stats->attrcollid;
+					break;
+
+				case STATISTIC_KIND_HISTOGRAM:
+				case STATISTIC_KIND_CORRELATION:
+					stats->staop[k] = mystats->ltopr;
+					stats->stacoll[k] = stats->attrcollid;
+					break;
+
+				case STATISTIC_KIND_MCELEM:
+					stats->staop[k] = TextEqualOperator;
+					stats->stacoll[k] = DEFAULT_COLLATION_OID;
+					break;
+
+				case STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM:
+					stats->staop[k] = Float8LessOperator;
+					stats->stacoll[k] = InvalidOid;
+					break;
+
+				case STATISTIC_KIND_BOUNDS_HISTOGRAM:
+				default:
+					stats->staop[k] = InvalidOid;
+					stats->stacoll[k] = InvalidOid;
+					break;
+			}
+		}
+	}
+
+	/* stanumbers is an array of arrays of floats */
+	arraycont = key_lookup_array(cont, "stanumbers");
+	if (arraycont != NULL)
+	{
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			JsonbValue	   *j;
+			JsonbContainer *numarr;
+			int				nelems;
+			int				e;
+
+			j = getIthJsonbValueFromContainer(arraycont, k);
+
+			/* skip out-of-bounds and explicit nulls */
+			if (j == NULL)
+				continue;
+
+			if (j->type == jbvNull)
+			{
+				pfree(j);
+				continue;
+			}
+
+			if ((j->type != jbvBinary) ||
+				(!JsonContainerIsArray(j->val.binary.data)))
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stanumbers must be a binary array or null")));
+
+			numarr = j->val.binary.data;
+			pfree(j);
+
+			nelems = JsonContainerSize(numarr);
+			stats->numnumbers[k] = nelems;
+			stats->stanumbers[k] = (float4 *) palloc(nelems * sizeof(float4));
+
+			for (e = 0; e < nelems; e++)
+			{
+				JsonbValue *f = getIthJsonbValueFromContainer(numarr, e);
+				float4		floatval;
+				char	   *fstr;
+
+				/* skip out-of-bounds and explicit nulls */
+				if (f == NULL || f->type != jbvString)
+					ereport(ERROR,
+					  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					   errmsg("invalid statistics format, elements of stanumbers must a string")));
+
+				fstr = JsonbStringValueToCString(f);
+				floatval = float4in_internal(fstr, NULL, "realx3", fstr, NULL);
+				stats->stanumbers[k][e] = floatval;
+				pfree(f);
+				pfree(fstr);
+			}
+		}
+	}
+
+	/* stavalues is an array of arrays of the column attr type */
+	arraycont = key_lookup_array(cont, "stavalues");
+	if (arraycont != NULL)
+	{
+		Oid			in_func;
+		Oid			typioparam;
+		FmgrInfo	finfo;
+
+		getTypeInputInfo(stats->attrtypid, &in_func, &typioparam);
+		fmgr_info(in_func, &finfo);
+
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			JsonbValue	   *j;
+			JsonbContainer *attrarr;
+			int				nelems;
+			int				e;
+
+			j = getIthJsonbValueFromContainer(arraycont, k);
+
+			/* skip out-of-bounds and explicit nulls */
+			if (j == NULL)
+				continue;
+
+			if (j->type == jbvNull)
+			{
+				pfree(j);
+				continue;
+			}
+
+			if ((j->type != jbvBinary) ||
+				(!JsonContainerIsArray(j->val.binary.data)))
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stavalues must be a binary array or null")));
+
+			attrarr = j->val.binary.data;
+			pfree(j);
+			nelems = JsonContainerSize(attrarr);
+			stats->numvalues[k] = nelems;
+			stats->stavalues[k] = (Datum *) palloc(nelems * sizeof(Datum));
+
+			for (e = 0; e < nelems; e++)
+			{
+				JsonbValue *f;
+				char	   *fstr;
+				Datum		datum;
+
+				f = getIthJsonbValueFromContainer(attrarr, e);
+
+				/* skip out-of-bounds and explicit nulls */
+				if (f == NULL || f->type != jbvString)
+					ereport(ERROR,
+					  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					   errmsg("invalid statistics format, elements of stavalues must be a string")));
+
+				fstr = JsonbStringValueToCString(f);
+				datum = InputFunctionCall(&finfo, fstr, typioparam, stats->attrtypmod);
+				stats->stavalues[k][e] = datum;
+				pfree(fstr);
+				pfree(f);
+			}
+		}
+	}
+}
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index edb2e5347d..f410510a28 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -27,7 +27,9 @@
 #include "parser/parsetree.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -1829,3 +1831,134 @@ dependencies_clauselist_selectivity(PlannerInfo *root,
 
 	return s1;
 }
+
+
+/*
+ * imports functional dependencies between groups of columns
+ *
+ * Generates all possible subsets of columns (variations) and computes
+ * the degree of validity for each one. For example when creating statistics
+ * on three columns (a,b,c) there are 9 possible dependencies
+ *
+ *	   two columns			  three columns
+ *	   -----------			  -------------
+ *	   (a) -> b				  (a,b) -> c
+ *	   (a) -> c				  (a,c) -> b
+ *	   (b) -> a				  (b,c) -> a
+ *	   (b) -> c
+ *	   (c) -> a
+ *	   (c) -> b
+ */
+MVDependencies *
+import_dependencies(JsonbContainer *cont)
+{
+	MVDependencies *dependencies = NULL;
+
+	int			numcombs;
+	int			i;
+
+	/* no dependencies to import */
+	if (cont == NULL)
+		return NULL;
+
+	/*
+	 *
+	 * format example:
+	 *
+	 * "stxdndependencies": <= you are here
+	 *   [
+	 *     {
+	 *       "attnums": [ "2", "-1" ],
+	 *       "degree": "0.500"
+	 *     },
+	 *     ...
+	 *   ]
+	 *
+	 */
+	numcombs = JsonContainerSize(cont);
+
+	/* no dependencies to import */
+	if (numcombs == 0)
+		return NULL;
+
+	dependencies = palloc(offsetof(MVDependencies, deps)
+							+ numcombs * sizeof(MVDependency *));
+
+	dependencies->magic = STATS_DEPS_MAGIC;
+	dependencies->type = STATS_DEPS_TYPE_BASIC;
+	dependencies->ndeps = numcombs;
+
+	for (i = 0; i < numcombs; i++)
+	{
+		MVDependency	   *d = dependencies->deps[i];
+		JsonbValue		   *elem;
+		JsonbContainer	   *dep;
+		JsonbValue		   *attnums;
+		JsonbContainer	   *attnumscont;
+		JsonbValue		   *degree;
+		char			   *degree_str;
+		int					numattnums;
+		int					j;
+
+		elem = getIthJsonbValueFromContainer(cont, i);
+
+		if ((elem->type != jbvBinary) ||
+			(!JsonContainerIsObject(elem->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxnddependencies elements must be binary objects")));
+
+		dep = elem->val.binary.data;
+		pfree(elem);
+
+		attnums = getKeyJsonValueFromContainer(dep, "attnums", strlen("attnums"), NULL);
+		if ((attnums == NULL) || (attnums->type != jbvBinary) ||
+			(!JsonContainerIsArray(attnums->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxndependencies must contain attnums array")));
+
+		attnumscont = attnums->val.binary.data;
+		pfree(attnums);
+		numattnums = JsonContainerSize(attnumscont);
+
+		d = palloc(offsetof(MVDependency, attributes)
+							+ numattnums * sizeof(AttrNumber));
+
+		d->nattributes = numattnums;
+
+		for (j = 0; j < numattnums; j++)
+		{
+			JsonbValue	   *attnum;
+			char		   *s;
+
+			attnum = getIthJsonbValueFromContainer(attnumscont, j);
+
+			if (attnum->type != jbvString)
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stxndependencies attnums elements must be strings")));
+
+			s = JsonbStringValueToCString(attnum);
+			pfree(attnum);
+
+			d->attributes[j] = pg_strtoint16(s);
+			pfree(s);
+		}
+
+		degree = getKeyJsonValueFromContainer(dep, "degree", strlen("degree"), NULL);
+		if ((degree == NULL) || (degree->type != jbvString))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxndependencies elements must have degree element")));
+
+		degree_str = JsonbStringValueToCString(degree);
+		pfree(degree);
+		d->degree = float8in_internal(degree_str, NULL, "double", degree_str, NULL);
+		pfree(degree_str);
+		dependencies->deps[i] = d;
+	}
+
+
+	return dependencies;
+}
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 9f67a57724..4e6e444f4c 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -2636,3 +2636,262 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+/* form an array of pg_statistic rows (per update_attstats) */
+static Datum
+serialize_vacattrstats(VacAttrStats **exprstats, int nexprs)
+{
+	int			exprno;
+	Oid			typOid;
+	Relation	sd;
+
+	ArrayBuildState *astate = NULL;
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+
+	/* lookup OID of composite type for pg_statistic */
+	typOid = get_rel_type_id(StatisticRelationId);
+	if (!OidIsValid(typOid))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("relation \"%s\" does not have a composite type",
+						"pg_statistic")));
+
+	for (exprno = 0; exprno < nexprs; exprno++)
+	{
+		int			i,
+					k;
+		VacAttrStats *stats = exprstats[exprno];
+
+		Datum		values[Natts_pg_statistic];
+		bool		nulls[Natts_pg_statistic];
+		HeapTuple	stup;
+
+		if (!stats->stats_valid)
+		{
+			astate = accumArrayResult(astate,
+									  (Datum) 0,
+									  true,
+									  typOid,
+									  CurrentMemoryContext);
+			continue;
+		}
+
+		/*
+		 * Construct a new pg_statistic tuple
+		 */
+		for (i = 0; i < Natts_pg_statistic; ++i)
+		{
+			nulls[i] = false;
+		}
+
+		values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(InvalidAttrNumber);
+		values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(false);
+		values[Anum_pg_statistic_stanullfrac - 1] = Float4GetDatum(stats->stanullfrac);
+		values[Anum_pg_statistic_stawidth - 1] = Int32GetDatum(stats->stawidth);
+		values[Anum_pg_statistic_stadistinct - 1] = Float4GetDatum(stats->stadistinct);
+		i = Anum_pg_statistic_stakind1 - 1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			values[i++] = Int16GetDatum(stats->stakind[k]); /* stakindN */
+		}
+		i = Anum_pg_statistic_staop1 - 1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			values[i++] = ObjectIdGetDatum(stats->staop[k]);	/* staopN */
+		}
+		i = Anum_pg_statistic_stacoll1 - 1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			values[i++] = ObjectIdGetDatum(stats->stacoll[k]);	/* stacollN */
+		}
+		i = Anum_pg_statistic_stanumbers1 - 1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			int			nnum = stats->numnumbers[k];
+
+			if (nnum > 0)
+			{
+				int			n;
+				Datum	   *numdatums = (Datum *) palloc(nnum * sizeof(Datum));
+				ArrayType  *arry;
+
+				for (n = 0; n < nnum; n++)
+					numdatums[n] = Float4GetDatum(stats->stanumbers[k][n]);
+				arry = construct_array_builtin(numdatums, nnum, FLOAT4OID);
+				values[i++] = PointerGetDatum(arry);	/* stanumbersN */
+			}
+			else
+			{
+				nulls[i] = true;
+				values[i++] = (Datum) 0;
+			}
+		}
+		i = Anum_pg_statistic_stavalues1 - 1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			if (stats->numvalues[k] > 0)
+			{
+				ArrayType  *arry;
+
+				arry = construct_array(stats->stavalues[k],
+									   stats->numvalues[k],
+									   stats->statypid[k],
+									   stats->statyplen[k],
+									   stats->statypbyval[k],
+									   stats->statypalign[k]);
+				values[i++] = PointerGetDatum(arry);	/* stavaluesN */
+			}
+			else
+			{
+				nulls[i] = true;
+				values[i++] = (Datum) 0;
+			}
+		}
+
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+
+		astate = accumArrayResult(astate,
+								  heap_copy_tuple_as_datum(stup, RelationGetDescr(sd)),
+								  false,
+								  typOid,
+								  CurrentMemoryContext);
+	}
+
+	table_close(sd, RowExclusiveLock);
+
+	return makeArrayResult(astate, CurrentMemoryContext);
+}
+
+/*
+ * Import requested extended stats, using the pre-computed (single-column)
+ * stats.
+ *
+ * This fetches a list of stats types from pg_statistic_ext, extracts the
+ * requested stats from the jsonb, and serializes them back into the catalog.
+ */
+void
+ImportRelationExtStatistics(Relation onerel, bool inh,
+						   int natts,
+						   VacAttrStats **vacattrstats,
+						   JsonbContainer *cont)
+{
+	Relation	pg_stext;
+	ListCell   *lc;
+	List	   *statslist;
+
+	/* Do nothing if there are no columns to analyze. */
+	if (!natts)
+		return;
+
+	/* Do nothing if there are no stats to import */
+	if (cont == NULL)
+		return;
+
+	/* the list of stats has to be allocated outside the memory context */
+	pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock);
+	statslist = fetch_statentries_for_relation(pg_stext, RelationGetRelid(onerel));
+
+	/*
+	 * format:
+	 *
+	 * { <= you are here
+	 *   <stat-name>:
+	 *     {
+	 *        "stxkinds": array of single characters (up to 3?),
+	 *        "stxdndistinct": [ {ndistinct}, ... ],
+	 *        "stxdndependencies": [ {dependency}, ... ]
+	 *        "stxdmcv": [ {mcv}, ... ]
+	 *        "stxdexprs" : [ {pg_statistic}, ... ]
+	 *     },
+	 *     ...
+	 * }
+	 */
+
+	foreach(lc, statslist)
+	{
+		StatExtEntry *stat = (StatExtEntry *) lfirst(lc);
+		MVNDistinct *ndistinct = NULL;
+		MVDependencies *dependencies = NULL;
+		MCVList    *mcv = NULL;
+		Datum		exprstats = (Datum) 0;
+		VacAttrStats **stats;
+		JsonbValue	   *exprval;
+		JsonbContainer *attrcont,
+					   *ndistcont,
+					   *depcont,
+					   *exprcont;
+
+		/*
+		 * Check if we can build these stats based on the column analyzed. If
+		 * not, report this fact (except in autovacuum) and move on.
+		 */
+		stats = lookup_var_attr_stats(onerel, stat->columns, stat->exprs,
+									  natts, vacattrstats);
+		if (!stats)
+			continue;
+
+		attrcont = key_lookup_object(cont, stat->name);
+
+		/* staname not found, skip */
+		if (attrcont == NULL)
+			continue;
+
+		ndistcont = key_lookup_array(attrcont, "stxdndistinct");
+
+		if (ndistcont != NULL)
+			ndistinct = import_ndistinct(ndistcont);
+
+		depcont = key_lookup_array(attrcont, "stxdndependencies");
+		if (depcont != NULL)
+			dependencies = import_dependencies(depcont);
+
+		/* TODO mcvcont for "stxdmcv" and import_mcv() */
+
+		exprval = getKeyJsonValueFromContainer(attrcont, "stxdexprs", strlen("stxdexprs"), NULL);
+		if (exprval != NULL)
+		{
+			if (exprval->type != jbvNull)
+			{
+				int	nexprs;
+				int	k;
+
+				if (!JsonContainerIsArray(exprval->val.binary.data))
+			   		errmsg("invalid statistics format, stxndeprs must be array or null");
+
+				exprcont = exprval->val.binary.data;
+
+				nexprs = JsonContainerSize(exprcont);
+
+				for (k = 0; k < nexprs; k++)
+				{
+					JsonbValue *statelem;
+
+					statelem = getIthJsonbValueFromContainer(exprcont, k);
+					if ((statelem->type != jbvBinary) ||
+						(!JsonContainerIsObject(statelem->val.binary.data)))
+						ereport(ERROR,
+						  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						   errmsg("invalid statistics format, stxdexprs elements must be binary objects")));
+
+					ImportVacAttrStats(stats[k], statelem->val.binary.data);
+					pfree(statelem);
+				}
+
+				exprstats = serialize_vacattrstats(stats, nexprs);
+
+			}
+			pfree(exprval);
+		}
+
+
+		/* store the statistics in the catalog */
+		statext_store(stat->statOid, inh,
+					  ndistinct, dependencies, mcv, exprstats, stats);
+	}
+
+	list_free(statslist);
+
+	table_close(pg_stext, RowExclusiveLock);
+}
diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c
index 03b9f04bb5..8fbb388028 100644
--- a/src/backend/statistics/mcv.c
+++ b/src/backend/statistics/mcv.c
@@ -2177,3 +2177,9 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat,
 
 	return s;
 }
+
+MCVList *import_mcv(JsonbContainer *cont)
+{
+	return NULL;
+}
+
diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 6d25c14644..8489a758a6 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -31,6 +31,8 @@
 #include "lib/stringinfo.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
@@ -698,3 +700,121 @@ generate_combinations(CombinationGenerator *state)
 
 	pfree(current);
 }
+
+
+/*
+ * Import ndistinct values from JsonB.
+ *
+ * To handle expressions easily, we treat them as system attributes with
+ * negative attnums, and offset everything by number of expressions to
+ * allow using Bitmapsets.
+ */
+MVNDistinct *
+import_ndistinct(JsonbContainer *cont)
+{
+	MVNDistinct *result;
+	int			numcombs;
+	int			i;
+
+	/* no ndistinct to import */
+	if (cont == NULL)
+		return NULL;
+
+	/* each row of the JSON is a combination */
+	numcombs = JsonContainerSize(cont);
+
+	/* no ndistinct to import */
+	if (numcombs == 0)
+		return NULL;
+
+	result = palloc(offsetof(MVNDistinct, items) +
+					numcombs * sizeof(MVNDistinctItem));
+	result->magic = STATS_NDISTINCT_MAGIC;
+	result->type = STATS_NDISTINCT_TYPE_BASIC;
+	result->nitems = numcombs;
+
+	/*
+	 * format example:
+	 *
+	 * "stxdndistinct": [
+	 *     {
+	 *       "attnums": [ "2", "1" ],
+	 *       "ndistinct": "4"
+	 *     },
+	 *     ...
+	 *   ]
+	 *
+	 */
+	for (i = 0; i < numcombs; i++)
+	{
+		MVNDistinctItem	   *item;
+		JsonbValue		   *elem;
+		JsonbContainer	   *combo;
+		JsonbValue		   *attnums;
+		JsonbContainer	   *attnumscont;
+		JsonbValue		   *ndistinct;
+		char			   *ndist_str;
+		int					numattnums;
+		int					j;
+
+		item = &result->items[i];
+
+		elem = getIthJsonbValueFromContainer(cont, i);
+
+		if ((elem->type != jbvBinary) ||
+			(!JsonContainerIsObject(elem->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxndistinct elements must be a binary objects")));
+
+		combo = elem->val.binary.data;
+
+		attnums = getKeyJsonValueFromContainer(combo, "attnums", strlen("attnums"), NULL);
+		if ((attnums == NULL) || (attnums->type != jbvBinary) ||
+			(!JsonContainerIsArray(attnums->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxndistinct must contain attnums array")));
+
+		attnumscont = attnums->val.binary.data;
+		numattnums = JsonContainerSize(attnumscont);
+
+		item->attributes = palloc(sizeof(AttrNumber) * numattnums);
+		item->nattributes = numattnums;
+
+		for (j = 0; j < numattnums; j++)
+		{
+			JsonbValue	   *attnum;
+			char		   *s;
+			attnum = getIthJsonbValueFromContainer(attnumscont, j);
+
+			if (attnum->type != jbvString)
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stxndistinct attnums elements must be strings, but one is %s", 
+						  JsonbTypeName(attnum))));
+
+			s = JsonbStringValueToCString(attnum);
+
+			item->attributes[j] = pg_strtoint16(s);
+			pfree(s);
+			pfree(attnum);
+		}
+
+		ndistinct = getKeyJsonValueFromContainer(combo, "ndistinct", strlen("ndistinct"), NULL);
+		if ((ndistinct == NULL) || (ndistinct->type != jbvString))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxndistinct elements must have ndistinct element")));
+
+		ndist_str = JsonbStringValueToCString(ndistinct);
+		item->ndistinct = float8in_internal(ndist_str, NULL, "double", ndist_str, NULL);
+
+		pfree(ndist_str);
+		pfree(ndistinct);
+		pfree(attnums);
+		pfree(elem);
+	}
+
+	return result;
+}
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index 4def90b805..4e609ca3b6 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -508,3 +508,156 @@ RESET ROLE;
 DROP TABLE vacowned;
 DROP TABLE vacowned_parted;
 DROP ROLE regress_vacuum;
+CREATE TYPE stats_import_complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import_test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import_complex_type
+);
+INSERT INTO stats_import_test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import_complex_type
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import_complex_type
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type
+UNION ALL
+SELECT 4, 'four', NULL;
+CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
+CREATE STATISTICS oddness ON name, ((comp).a % 2 = 1) FROM stats_import_test;
+ANALYZE stats_import_test;
+CREATE TABLE stats_export AS
+SELECT e.*
+FROM pg_catalog.pg_statistic_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname = 'stats_import_test';
+SELECT c.reltuples AS before_tuples, c.relpages AS before_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ before_tuples | before_pages 
+---------------+--------------
+             4 |            1
+(1 row)
+
+-- test settting tuples and pages but no columns
+SELECT pg_import_rel_stats(c.oid, current_setting('server_version_num')::integer,
+                           1000.0, 200, NULL::jsonb)
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+SELECT c.reltuples AS after_tuples, c.relpages AS after_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ after_tuples | after_pages 
+--------------+-------------
+         1000 |         200
+(1 row)
+
+CREATE TEMPORARY TABLE orig_stats
+AS
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_test'::regclass;
+CREATE TEMPORARY TABLE orig_export
+AS
+SELECT e.*
+FROM stats_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname = 'stats_import_test';
+-- create a table just like stats_import_test
+CREATE TABLE stats_import_clone ( LIKE stats_import_test );
+-- create an index that does not exist on stats_import_test
+CREATE INDEX is_even ON stats_import_clone(((comp).a % 2 = 0));
+-- rename statistics object to allow for a same-named one on the clone
+ALTER STATISTICS oddness RENAME TO oddness_original;
+CREATE STATISTICS oddness ON name, ((comp).a % 2 = 1) FROM stats_import_clone;
+-- copy table stats to clone table
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN orig_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'stats_import_test'
+WHERE c.oid = 'stats_import_clone'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+-- stats should match
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_test'::regclass
+EXCEPT
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_clone'::regclass;
+ staattnum | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 
+-----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----
+(0 rows)
+
+-- compare all except stxdmcv and oid-related stxdexprs
+SELECT d.stxdinherit, d.stxdndistinct, d.stxddependencies, xpr.xpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+LEFT JOIN LATERAL (
+    SELECT
+        array_agg(ROW(x.staattnum, x.stainherit, x.stanullfrac, x.stawidth,
+                      x.stadistinct, x.stakind1, x.stakind2, x.stakind3,
+                      x.stakind4, x.stakind5, x.stanumbers1, x.stanumbers2,
+                      x.stanumbers3, x.stanumbers4, x.stanumbers5, x.stavalues1,
+                      x.stavalues2, x.stavalues3, x.stavalues4, x.stavalues5
+                      )::text ORDER BY x.ordinality)
+    FROM unnest(d.stxdexpr) WITH ORDINALITY AS x
+) AS xpr(xpr) ON d.stxdexpr IS NOT NULL
+WHERE e.stxrelid = 'stats_import_test'::regclass
+AND e.stxname = 'oddness_original'
+EXCEPT
+SELECT d.stxdinherit, d.stxdndistinct, d.stxddependencies, xpr.xpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+LEFT JOIN LATERAL (
+    SELECT
+        array_agg(ROW(x.staattnum, x.stainherit, x.stanullfrac, x.stawidth,
+                      x.stadistinct, x.stakind1, x.stakind2, x.stakind3,
+                      x.stakind4, x.stakind5, x.stanumbers1, x.stanumbers2,
+                      x.stanumbers3, x.stanumbers4, x.stanumbers5, x.stavalues1,
+                      x.stavalues2, x.stavalues3, x.stavalues4, x.stavalues5
+                      )::text ORDER BY x.ordinality)
+    FROM unnest(d.stxdexpr) WITH ORDINALITY AS x
+) AS xpr(xpr) ON d.stxdexpr IS NOT NULL
+WHERE e.stxrelid = 'stats_import_clone'::regclass
+AND e.stxname = 'oddness';
+ stxdinherit | stxdndistinct | stxddependencies | xpr 
+-------------+---------------+------------------+-----
+(0 rows)
+
+DROP TABLE stats_export;
+DROP TABLE stats_import_clone;
+DROP TABLE stats_import_test;
+DROP TYPE stats_import_complex_type;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 51d7b1fecc..b42f35a8e3 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -377,3 +377,149 @@ RESET ROLE;
 DROP TABLE vacowned;
 DROP TABLE vacowned_parted;
 DROP ROLE regress_vacuum;
+
+
+CREATE TYPE stats_import_complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import_test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import_complex_type
+);
+
+INSERT INTO stats_import_test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import_complex_type
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import_complex_type
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type
+UNION ALL
+SELECT 4, 'four', NULL;
+
+CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
+
+CREATE STATISTICS oddness ON name, ((comp).a % 2 = 1) FROM stats_import_test;
+
+ANALYZE stats_import_test;
+
+CREATE TABLE stats_export AS
+SELECT e.*
+FROM pg_catalog.pg_statistic_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname = 'stats_import_test';
+
+SELECT c.reltuples AS before_tuples, c.relpages AS before_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+-- test settting tuples and pages but no columns
+SELECT pg_import_rel_stats(c.oid, current_setting('server_version_num')::integer,
+                           1000.0, 200, NULL::jsonb)
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+SELECT c.reltuples AS after_tuples, c.relpages AS after_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+CREATE TEMPORARY TABLE orig_stats
+AS
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_test'::regclass;
+
+CREATE TEMPORARY TABLE orig_export
+AS
+SELECT e.*
+FROM stats_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname = 'stats_import_test';
+
+-- create a table just like stats_import_test
+CREATE TABLE stats_import_clone ( LIKE stats_import_test );
+
+-- create an index that does not exist on stats_import_test
+CREATE INDEX is_even ON stats_import_clone(((comp).a % 2 = 0));
+
+-- rename statistics object to allow for a same-named one on the clone
+ALTER STATISTICS oddness RENAME TO oddness_original;
+
+CREATE STATISTICS oddness ON name, ((comp).a % 2 = 1) FROM stats_import_clone;
+
+-- copy table stats to clone table
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN orig_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'stats_import_test'
+WHERE c.oid = 'stats_import_clone'::regclass;
+
+-- stats should match
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_test'::regclass
+EXCEPT
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_clone'::regclass;
+
+-- compare all except stxdmcv and oid-related stxdexprs
+SELECT d.stxdinherit, d.stxdndistinct, d.stxddependencies, xpr.xpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+LEFT JOIN LATERAL (
+    SELECT
+        array_agg(ROW(x.staattnum, x.stainherit, x.stanullfrac, x.stawidth,
+                      x.stadistinct, x.stakind1, x.stakind2, x.stakind3,
+                      x.stakind4, x.stakind5, x.stanumbers1, x.stanumbers2,
+                      x.stanumbers3, x.stanumbers4, x.stanumbers5, x.stavalues1,
+                      x.stavalues2, x.stavalues3, x.stavalues4, x.stavalues5
+                      )::text ORDER BY x.ordinality)
+    FROM unnest(d.stxdexpr) WITH ORDINALITY AS x
+) AS xpr(xpr) ON d.stxdexpr IS NOT NULL
+WHERE e.stxrelid = 'stats_import_test'::regclass
+AND e.stxname = 'oddness_original'
+EXCEPT
+SELECT d.stxdinherit, d.stxdndistinct, d.stxddependencies, xpr.xpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+LEFT JOIN LATERAL (
+    SELECT
+        array_agg(ROW(x.staattnum, x.stainherit, x.stanullfrac, x.stawidth,
+                      x.stadistinct, x.stakind1, x.stakind2, x.stakind3,
+                      x.stakind4, x.stakind5, x.stanumbers1, x.stanumbers2,
+                      x.stanumbers3, x.stanumbers4, x.stanumbers5, x.stavalues1,
+                      x.stavalues2, x.stavalues3, x.stavalues4, x.stavalues5
+                      )::text ORDER BY x.ordinality)
+    FROM unnest(d.stxdexpr) WITH ORDINALITY AS x
+) AS xpr(xpr) ON d.stxdexpr IS NOT NULL
+WHERE e.stxrelid = 'stats_import_clone'::regclass
+AND e.stxname = 'oddness';
+
+DROP TABLE stats_export;
+DROP TABLE stats_import_clone;
+DROP TABLE stats_import_test;
+DROP TYPE stats_import_complex_type;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index c76ec52c55..61e01d1b90 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28014,6 +28014,48 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_import_rel_stats</primary>
+        </indexterm>
+        <function>pg_import_rel_stats</function> ( <parameter>relation</parameter> <type>regclass</type>, <parameter>server_version_num</parameter> <type>integer</type>, <parameter>num_tuples</parameter> <type>float4</type>, <parameter>num_pages</parameter> <type>integer</type>, <parameter>column_stats</parameter> <type>jsonb</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Modifies the <structname>pg_class</structname> row with the
+        <structfield>oid</structfield> matching <parameter>relation</parameter>
+        to set the <structfield>reltuples</structfield> and
+        <structfield>relpages</structfield> fields. This is done nontransactionally.
+        The <structname>pg_statistic</structname> rows for the
+        <structfield>statrelid</structfield> matching <parameter>relation</parameter>
+        are replaced with the values found in <parameter>column_stats</parameter>,
+        and this is done transactionally. The purpose of this function is to apply
+        statistics values in an upgrade situation that are "good enough" for system
+        operation until they are replaced by the next auto-analyze. This function
+        is used by <program>pg_upgrade</program> and <program>pg_restore</program>
+        to convey the statistics from the old system version into the new one.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.41.0

From da9b9a4012757a8bb08711d4479ce5ebb38e974d Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 31 Oct 2023 03:13:24 -0400
Subject: [PATCH v2 4/4] Add pg_export_stats, pg_import_stats.

pg_export_stats is used to export stats from databases as far back as
v10. The output is currently only to stdout and should be redirected to
a file in most use cases.

pg_import_stats is used to import stats to any version that has the
function pg_import_rel_stats().
---
 src/bin/scripts/Makefile          |   6 +-
 src/bin/scripts/pg_export_stats.c | 946 ++++++++++++++++++++++++++++++
 src/bin/scripts/pg_import_stats.c | 303 ++++++++++
 3 files changed, 1254 insertions(+), 1 deletion(-)
 create mode 100644 src/bin/scripts/pg_export_stats.c
 create mode 100644 src/bin/scripts/pg_import_stats.c

diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index a7a9d0fea5..da45fcfa6a 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -16,7 +16,7 @@ subdir = src/bin/scripts
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
-PROGRAMS = createdb createuser dropdb dropuser clusterdb vacuumdb reindexdb pg_isready
+PROGRAMS = createdb createuser dropdb dropuser clusterdb vacuumdb reindexdb pg_isready pg_export_stats pg_import_stats
 
 override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
 LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
@@ -31,6 +31,8 @@ clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport su
 vacuumdb: vacuumdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+pg_export_stats: pg_export_stats.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+pg_import_stats: pg_import_stats.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 
 install: all installdirs
 	$(INSTALL_PROGRAM) createdb$(X)   '$(DESTDIR)$(bindir)'/createdb$(X)
@@ -41,6 +43,8 @@ install: all installdirs
 	$(INSTALL_PROGRAM) vacuumdb$(X)   '$(DESTDIR)$(bindir)'/vacuumdb$(X)
 	$(INSTALL_PROGRAM) reindexdb$(X)  '$(DESTDIR)$(bindir)'/reindexdb$(X)
 	$(INSTALL_PROGRAM) pg_isready$(X) '$(DESTDIR)$(bindir)'/pg_isready$(X)
+	$(INSTALL_PROGRAM) pg_export_stats$(X) '$(DESTDIR)$(bindir)'/pg_export_stats$(X)
+	$(INSTALL_PROGRAM) pg_import_stats$(X) '$(DESTDIR)$(bindir)'/pg_import_stats$(X)
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
diff --git a/src/bin/scripts/pg_export_stats.c b/src/bin/scripts/pg_export_stats.c
new file mode 100644
index 0000000000..b9fc192e8f
--- /dev/null
+++ b/src/bin/scripts/pg_export_stats.c
@@ -0,0 +1,946 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_export_stats
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/scripts/pg_export_stats.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+#include "common.h"
+#include "common/logging.h"
+#include "fe_utils/cancel.h"
+#include "fe_utils/option_utils.h"
+#include "fe_utils/query_utils.h"
+#include "fe_utils/simple_list.h"
+#include "fe_utils/string_utils.h"
+
+static void help(const char *progname);
+
+/* view definition introduced in 17 */
+const char *export_query_v17 =
+	"SELECT schemaname, relname, server_version_num, n_tuples, "
+	"n_pages, stats FROM pg_statistic_export ";
+
+/* v15-v16 have the same stats layout, but lacks view definition */
+const char *export_query_v15 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    r.reltuples::float4 AS n_tuples, "
+	"    r.relpages::integer AS n_pages, "
+	"    ( "
+	"        WITH per_column_stats AS "
+	"        ( "
+	"            SELECT "
+	"                s.stainherit, "
+	"                a.attname, "
+	"                jsonb_build_object( "
+	"                    'stanullfrac', s.stanullfrac::text, "
+	"                    'stawidth', s.stawidth::text, "
+	"                    'stadistinct', s.stadistinct::text, "
+	"                    'stakinds', "
+	"                    ( "
+	"                        SELECT "
+	"                            jsonb_agg( "
+	"                                CASE kind.kind "
+	"                                    WHEN 0 THEN 'TRIVIAL' "
+	"                                    WHEN 1 THEN 'MCV' "
+	"                                    WHEN 2 THEN 'HISTOGRAM' "
+	"                                    WHEN 3 THEN 'CORRELATION' "
+	"                                    WHEN 4 THEN 'MCELEM' "
+	"                                    WHEN 5 THEN 'DECHIST' "
+	"                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                END::text "
+	"                                ORDER BY kind.ord) "
+	"                        FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                    s.stakind3, stakind4, "
+	"                                    s.stakind5]) "
+	"                             WITH ORDINALITY AS kind(kind, ord) "
+	"                    ), "
+	"                    'stanumbers', "
+	"                    jsonb_build_array( "
+	"                        s.stanumbers1::text::text[], "
+	"                        s.stanumbers2::text::text[], "
+	"                        s.stanumbers3::text::text[], "
+	"                        s.stanumbers4::text::text[], "
+	"                        s.stanumbers5::text::text[]), "
+	"                    'stavalues', "
+	"                    jsonb_build_array( "
+	"                        s.stavalues1::text::text[], "
+	"                        s.stavalues2::text::text[], "
+	"                        s.stavalues3::text::text[], "
+	"                        s.stavalues4::text::text[], "
+	"                        s.stavalues5::text::text[]) "
+	"                ) AS stats "
+	"            FROM pg_attribute AS a "
+	"            JOIN pg_statistic AS s "
+	"                ON s.starelid = a.attrelid "
+	"                AND s.staattnum = a.attnum "
+	"            WHERE a.attrelid = r.oid "
+	"            AND NOT a.attisdropped "
+	"            AND a.attnum > 0 "
+	"            AND has_column_privilege(a.attrelid, a.attnum, 'SELECT') "
+	"        ), "
+	"        attagg AS "
+	"        ( "
+	"            SELECT "
+	"                pcs.stainherit, "
+	"                jsonb_build_object( "
+	"                    'columns', "
+	"                    jsonb_object_agg( "
+	"                        pcs.attname, "
+	"                        pcs.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM per_column_stats AS pcs "
+	"            GROUP BY pcs.stainherit "
+	"        ), "
+	"        extended_object_stats AS "
+	"        ( "
+	"            SELECT "
+	"                sd.stxdinherit, "
+	"                e.stxname, "
+	"                jsonb_build_object( "
+	"                    'stxkinds', "
+	"                    to_jsonb(e.stxkind), "
+	"                    'stxdndistinct', "
+	"                    ndist.stxdndistinct, "
+	"                    'stxdndependencies', "
+	"                    ndep.stxdndependencies, "
+	"                    'stxdmcv', "
+	"                    mcv.stxdmcv, "
+	"                    'stxdexprs', "
+	"                    x.stdxdexprs "
+	"                ) AS stats "
+	"            FROM pg_statistic_ext AS e "
+	"            JOIN pg_statistic_ext_data AS sd "
+	"                ON sd.stxoid = e.oid "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', string_to_array(nd.attnums, ', '), "
+	"                                'ndistinct', nd.ndistinct "
+	"                                ) "
+	"                            ORDER BY nd.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                         WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                ) AS ndist(stxdndistinct) ON sd.stxdndistinct IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', "
+	"                                string_to_array( replace(dep.attrs, ' => ', ', '), ', '), "
+	"                                'degree', "
+	"                                dep.degree "
+	"                                ) "
+	"                            ORDER BY dep.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxddependencies::text::json) "
+	"                         WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                ) AS ndep(stxdndependencies) ON sd.stxddependencies IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT NULL AS stxdmcv "
+	"                ) AS mcv(stxdmcv) ON sd.stxdmcv IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'stanullfrac', s.stanullfrac::text, "
+	"                                'stawidth', s.stawidth::text, "
+	"                                'stadistinct', s.stadistinct::text, "
+	"                                'stakinds', "
+	"                                ( "
+	"                                    SELECT "
+	"                                        jsonb_agg( "
+	"                                            CASE kind.kind "
+	"                                                WHEN 0 THEN 'TRIVIAL' "
+	"                                                WHEN 1 THEN 'MCV' "
+	"                                                WHEN 2 THEN 'HISTOGRAM' "
+	"                                                WHEN 3 THEN 'CORRELATION' "
+	"                                                WHEN 4 THEN 'MCELEM' "
+	"                                                WHEN 5 THEN 'DECHIST' "
+	"                                                WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                                WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                            END::text "
+	"                                            ORDER BY kind.ord) "
+	"                                    FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                                s.stakind3, stakind4, "
+	"                                                s.stakind5]) WITH ORDINALITY AS kind(kind, ord) "
+	"                                ), "
+	"                                'stanumbers', "
+	"                                jsonb_build_array( "
+	"                                    s.stanumbers1::text::text[], "
+	"                                    s.stanumbers2::text::text[], "
+	"                                    s.stanumbers3::text::text[], "
+	"                                    s.stanumbers4::text::text[], "
+	"                                    s.stanumbers5::text::text[]), "
+	"                                'stavalues', "
+	"                                jsonb_build_array( "
+	"                                    s.stavalues1::text::text[], "
+	"                                    s.stavalues2::text::text[], "
+	"                                    s.stavalues3::text::text[], "
+	"                                    s.stavalues4::text::text[], "
+	"                                    s.stavalues5::text::text[]) "
+	"                            ) "
+	"                            ORDER BY s.ordinality "
+	"                        ) "
+	"                    FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s "
+	"                ) AS x(stdxdexprs) ON sd.stxdexpr IS NOT NULL "
+	"            WHERE e.stxrelid = r.oid "
+	"        ), "
+	"        extagg AS "
+	"        ( "
+	"            SELECT "
+	"                eos.stxdinherit, "
+	"                jsonb_build_object( "
+	"                    'extended', "
+	"                    jsonb_object_agg( "
+	"                        eos.stxname, "
+	"                        eos.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM extended_object_stats AS eos "
+	"            GROUP BY eos.stxdinherit "
+	"        ) "
+	"        SELECT "
+	"            jsonb_object_agg( "
+	"                CASE coalesce(a.stainherit, e.stxdinherit) "
+	"                    WHEN TRUE THEN 'inherited' "
+	"                    ELSE 'regular' "
+	"                END, "
+	"                coalesce(a.stats, '{}'::jsonb) || coalesce(e.stats, '{}'::jsonb)  "
+	"            ) "
+	"        FROM attagg AS a "
+	"        FULL OUTER JOIN extagg e ON a.stainherit = e.stxdinherit "
+	"    ) AS stats "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n "
+	"    ON n.oid = r.relnamespace "
+	"WHERE relkind IN ('r', 'm', 'f', 'p') "
+	"AND n.nspname NOT IN ('pg_catalog', 'information_schema')";
+
+/* v14 is like v15, but lacks stxdinherit on ext_data */
+const char *export_query_v14 = 
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    r.reltuples::float4 AS n_tuples, "
+	"    r.relpages::integer AS n_pages, "
+	"    ( "
+	"        WITH per_column_stats AS "
+	"        ( "
+	"            SELECT "
+	"                s.stainherit, "
+	"                a.attname, "
+	"                jsonb_build_object( "
+	"                    'stanullfrac', s.stanullfrac::text, "
+	"                    'stawidth', s.stawidth::text, "
+	"                    'stadistinct', s.stadistinct::text, "
+	"                    'stakinds', "
+	"                    ( "
+	"                        SELECT "
+	"                            jsonb_agg( "
+	"                                CASE kind.kind "
+	"                                    WHEN 0 THEN 'TRIVIAL' "
+	"                                    WHEN 1 THEN 'MCV' "
+	"                                    WHEN 2 THEN 'HISTOGRAM' "
+	"                                    WHEN 3 THEN 'CORRELATION' "
+	"                                    WHEN 4 THEN 'MCELEM' "
+	"                                    WHEN 5 THEN 'DECHIST' "
+	"                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                END::text "
+	"                                ORDER BY kind.ord) "
+	"                        FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                    s.stakind3, stakind4, "
+	"                                    s.stakind5]) "
+	"                             WITH ORDINALITY AS kind(kind, ord) "
+	"                    ), "
+	"                    'stanumbers', "
+	"                    jsonb_build_array( "
+	"                        s.stanumbers1::text::text[], "
+	"                        s.stanumbers2::text::text[], "
+	"                        s.stanumbers3::text::text[], "
+	"                        s.stanumbers4::text::text[], "
+	"                        s.stanumbers5::text::text[]), "
+	"                    'stavalues', "
+	"                    jsonb_build_array( "
+	"                        s.stavalues1::text::text[], "
+	"                        s.stavalues2::text::text[], "
+	"                        s.stavalues3::text::text[], "
+	"                        s.stavalues4::text::text[], "
+	"                        s.stavalues5::text::text[]) "
+	"                ) AS stats "
+	"            FROM pg_attribute AS a "
+	"            JOIN pg_statistic AS s "
+	"                ON s.starelid = a.attrelid "
+	"                AND s.staattnum = a.attnum "
+	"            WHERE a.attrelid = r.oid "
+	"            AND NOT a.attisdropped "
+	"            AND a.attnum > 0 "
+	"            AND has_column_privilege(a.attrelid, a.attnum, 'SELECT') "
+	"        ), "
+	"        attagg AS "
+	"        ( "
+	"            SELECT "
+	"                pcs.stainherit, "
+	"                jsonb_build_object( "
+	"                    'columns', "
+	"                    jsonb_object_agg( "
+	"                        pcs.attname, "
+	"                        pcs.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM per_column_stats AS pcs "
+	"            GROUP BY pcs.stainherit "
+	"        ), "
+	"        extended_object_stats AS "
+	"        ( "
+	"            SELECT "
+	"                false AS stxdinherit, "
+	"                e.stxname, "
+	"                jsonb_build_object( "
+	"                    'stxkinds', "
+	"                    to_jsonb(e.stxkind), "
+	"                    'stxdndistinct', "
+	"                    ndist.stxdndistinct, "
+	"                    'stxdndependencies', "
+	"                    ndep.stxdndependencies, "
+	"                    'stxdmcv', "
+	"                    mcv.stxdmcv, "
+	"                    'stxdexprs', "
+	"                    x.stdxdexprs "
+	"                ) AS stats "
+	"            FROM pg_statistic_ext AS e "
+	"            JOIN pg_statistic_ext_data AS sd "
+	"                ON sd.stxoid = e.oid "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', string_to_array(nd.attnums, ', '), "
+	"                                'ndistinct', nd.ndistinct "
+	"                                ) "
+	"                            ORDER BY nd.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                         WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                ) AS ndist(stxdndistinct) ON sd.stxdndistinct IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', "
+	"                                string_to_array( replace(dep.attrs, ' => ', ', '), ', '), "
+	"                                'degree', "
+	"                                dep.degree "
+	"                                ) "
+	"                            ORDER BY dep.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxddependencies::text::json) "
+	"                         WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                ) AS ndep(stxdndependencies) ON sd.stxddependencies IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT NULL AS stxdmcv "
+	"                ) AS mcv(stxdmcv) ON sd.stxdmcv IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'stanullfrac', s.stanullfrac::text, "
+	"                                'stawidth', s.stawidth::text, "
+	"                                'stadistinct', s.stadistinct::text, "
+	"                                'stakinds', "
+	"                                ( "
+	"                                    SELECT "
+	"                                        jsonb_agg( "
+	"                                            CASE kind.kind "
+	"                                                WHEN 0 THEN 'TRIVIAL' "
+	"                                                WHEN 1 THEN 'MCV' "
+	"                                                WHEN 2 THEN 'HISTOGRAM' "
+	"                                                WHEN 3 THEN 'CORRELATION' "
+	"                                                WHEN 4 THEN 'MCELEM' "
+	"                                                WHEN 5 THEN 'DECHIST' "
+	"                                                WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                                WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                            END::text "
+	"                                            ORDER BY kind.ord) "
+	"                                    FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                                s.stakind3, stakind4, "
+	"                                                s.stakind5]) WITH ORDINALITY AS kind(kind, ord) "
+	"                                ), "
+	"                                'stanumbers', "
+	"                                jsonb_build_array( "
+	"                                    s.stanumbers1::text::text[], "
+	"                                    s.stanumbers2::text::text[], "
+	"                                    s.stanumbers3::text::text[], "
+	"                                    s.stanumbers4::text::text[], "
+	"                                    s.stanumbers5::text::text[]), "
+	"                                'stavalues', "
+	"                                jsonb_build_array( "
+	"                                    s.stavalues1::text::text[], "
+	"                                    s.stavalues2::text::text[], "
+	"                                    s.stavalues3::text::text[], "
+	"                                    s.stavalues4::text::text[], "
+	"                                    s.stavalues5::text::text[]) "
+	"                            ) "
+	"                            ORDER BY s.ordinality "
+	"                        ) "
+	"                    FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s "
+	"                ) AS x(stdxdexprs) ON sd.stxdexpr IS NOT NULL "
+	"            WHERE e.stxrelid = r.oid "
+	"        ), "
+	"        extagg AS "
+	"        ( "
+	"            SELECT "
+	"                eos.stxdinherit, "
+	"                jsonb_build_object( "
+	"                    'extended', "
+	"                    jsonb_object_agg( "
+	"                        eos.stxname, "
+	"                        eos.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM extended_object_stats AS eos "
+	"            GROUP BY eos.stxdinherit "
+	"        ) "
+	"        SELECT "
+	"            jsonb_object_agg( "
+	"                CASE coalesce(a.stainherit, e.stxdinherit) "
+	"                    WHEN TRUE THEN 'inherited' "
+	"                    ELSE 'regular' "
+	"                END, "
+	"                coalesce(a.stats, '{}'::jsonb) || coalesce(e.stats, '{}'::jsonb)  "
+	"            ) "
+	"        FROM attagg AS a "
+	"        FULL OUTER JOIN extagg e ON a.stainherit = e.stxdinherit "
+	"    ) AS stats "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n "
+	"    ON n.oid = r.relnamespace "
+	"WHERE relkind IN ('r', 'm', 'f', 'p') "
+	"AND n.nspname NOT IN ('pg_catalog', 'information_schema')";
+
+/* v12-v13 are like v14, but lacks stxdexpr on ext_data */
+const char *export_query_v12 = 
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    r.reltuples::float4 AS n_tuples, "
+	"    r.relpages::integer AS n_pages, "
+	"    ( "
+	"        WITH per_column_stats AS "
+	"        ( "
+	"            SELECT "
+	"                s.stainherit, "
+	"                a.attname, "
+	"                jsonb_build_object( "
+	"                    'stanullfrac', s.stanullfrac::text, "
+	"                    'stawidth', s.stawidth::text, "
+	"                    'stadistinct', s.stadistinct::text, "
+	"                    'stakinds', "
+	"                    ( "
+	"                        SELECT "
+	"                            jsonb_agg( "
+	"                                CASE kind.kind "
+	"                                    WHEN 0 THEN 'TRIVIAL' "
+	"                                    WHEN 1 THEN 'MCV' "
+	"                                    WHEN 2 THEN 'HISTOGRAM' "
+	"                                    WHEN 3 THEN 'CORRELATION' "
+	"                                    WHEN 4 THEN 'MCELEM' "
+	"                                    WHEN 5 THEN 'DECHIST' "
+	"                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                END::text "
+	"                                ORDER BY kind.ord) "
+	"                        FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                    s.stakind3, stakind4, "
+	"                                    s.stakind5]) "
+	"                             WITH ORDINALITY AS kind(kind, ord) "
+	"                    ), "
+	"                    'stanumbers', "
+	"                    jsonb_build_array( "
+	"                        s.stanumbers1::text::text[], "
+	"                        s.stanumbers2::text::text[], "
+	"                        s.stanumbers3::text::text[], "
+	"                        s.stanumbers4::text::text[], "
+	"                        s.stanumbers5::text::text[]), "
+	"                    'stavalues', "
+	"                    jsonb_build_array( "
+	"                        s.stavalues1::text::text[], "
+	"                        s.stavalues2::text::text[], "
+	"                        s.stavalues3::text::text[], "
+	"                        s.stavalues4::text::text[], "
+	"                        s.stavalues5::text::text[]) "
+	"                ) AS stats "
+	"            FROM pg_attribute AS a "
+	"            JOIN pg_statistic AS s "
+	"                ON s.starelid = a.attrelid "
+	"                AND s.staattnum = a.attnum "
+	"            WHERE a.attrelid = r.oid "
+	"            AND NOT a.attisdropped "
+	"            AND a.attnum > 0 "
+	"            AND has_column_privilege(a.attrelid, a.attnum, 'SELECT') "
+	"        ), "
+	"        attagg AS "
+	"        ( "
+	"            SELECT "
+	"                pcs.stainherit, "
+	"                jsonb_build_object( "
+	"                    'columns', "
+	"                    jsonb_object_agg( "
+	"                        pcs.attname, "
+	"                        pcs.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM per_column_stats AS pcs "
+	"            GROUP BY pcs.stainherit "
+	"        ), "
+	"        extended_object_stats AS "
+	"        ( "
+	"            SELECT "
+	"                false AS stxdinherit, "
+	"                e.stxname, "
+	"                jsonb_build_object( "
+	"                    'stxkinds', "
+	"                    to_jsonb(e.stxkind), "
+	"                    'stxdndistinct', "
+	"                    ndist.stxdndistinct, "
+	"                    'stxdndependencies', "
+	"                    ndep.stxdndependencies, "
+	"                    'stxdmcv', "
+	"                    mcv.stxdmcv, "
+	"                ) AS stats "
+	"            FROM pg_statistic_ext AS e "
+	"            JOIN pg_statistic_ext_data AS sd "
+	"                ON sd.stxoid = e.oid "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', string_to_array(nd.attnums, ', '), "
+	"                                'ndistinct', nd.ndistinct "
+	"                                ) "
+	"                            ORDER BY nd.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                         WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                ) AS ndist(stxdndistinct) ON sd.stxdndistinct IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', "
+	"                                string_to_array( replace(dep.attrs, ' => ', ', '), ', '), "
+	"                                'degree', "
+	"                                dep.degree "
+	"                                ) "
+	"                            ORDER BY dep.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxddependencies::text::json) "
+	"                         WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                ) AS ndep(stxdndependencies) ON sd.stxddependencies IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT NULL AS stxdmcv "
+	"                ) AS mcv(stxdmcv) ON sd.stxdmcv IS NOT NULL "
+	"            WHERE e.stxrelid = r.oid "
+	"        ), "
+	"        extagg AS "
+	"        ( "
+	"            SELECT "
+	"                eos.stxdinherit, "
+	"                jsonb_build_object( "
+	"                    'extended', "
+	"                    jsonb_object_agg( "
+	"                        eos.stxname, "
+	"                        eos.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM extended_object_stats AS eos "
+	"            GROUP BY eos.stxdinherit "
+	"        ) "
+	"        SELECT "
+	"            jsonb_object_agg( "
+	"                CASE coalesce(a.stainherit, e.stxdinherit) "
+	"                    WHEN TRUE THEN 'inherited' "
+	"                    ELSE 'regular' "
+	"                END, "
+	"                coalesce(a.stats, '{}'::jsonb) || coalesce(e.stats, '{}'::jsonb)  "
+	"            ) "
+	"        FROM attagg AS a "
+	"        FULL OUTER JOIN extagg e ON a.stainherit = e.stxdinherit "
+	"    ) AS stats "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n "
+	"    ON n.oid = r.relnamespace "
+	"WHERE relkind IN ('r', 'm', 'f', 'p') "
+	"AND n.nspname NOT IN ('pg_catalog', 'information_schema')";
+
+/* v10-v11 are like v12, but ext_data is gone and ndistinct and dependencies are on ext */
+const char *export_query_v10 = 
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    r.reltuples::float4 AS n_tuples, "
+	"    r.relpages::integer AS n_pages, "
+	"    ( "
+	"        WITH per_column_stats AS "
+	"        ( "
+	"            SELECT "
+	"                s.stainherit, "
+	"                a.attname, "
+	"                jsonb_build_object( "
+	"                    'stanullfrac', s.stanullfrac::text, "
+	"                    'stawidth', s.stawidth::text, "
+	"                    'stadistinct', s.stadistinct::text, "
+	"                    'stakinds', "
+	"                    ( "
+	"                        SELECT "
+	"                            jsonb_agg( "
+	"                                CASE kind.kind "
+	"                                    WHEN 0 THEN 'TRIVIAL' "
+	"                                    WHEN 1 THEN 'MCV' "
+	"                                    WHEN 2 THEN 'HISTOGRAM' "
+	"                                    WHEN 3 THEN 'CORRELATION' "
+	"                                    WHEN 4 THEN 'MCELEM' "
+	"                                    WHEN 5 THEN 'DECHIST' "
+	"                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                END::text "
+	"                                ORDER BY kind.ord) "
+	"                        FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                    s.stakind3, stakind4, "
+	"                                    s.stakind5]) "
+	"                             WITH ORDINALITY AS kind(kind, ord) "
+	"                    ), "
+	"                    'stanumbers', "
+	"                    jsonb_build_array( "
+	"                        s.stanumbers1::text::text[], "
+	"                        s.stanumbers2::text::text[], "
+	"                        s.stanumbers3::text::text[], "
+	"                        s.stanumbers4::text::text[], "
+	"                        s.stanumbers5::text::text[]), "
+	"                    'stavalues', "
+	"                    jsonb_build_array( "
+	"                        s.stavalues1::text::text[], "
+	"                        s.stavalues2::text::text[], "
+	"                        s.stavalues3::text::text[], "
+	"                        s.stavalues4::text::text[], "
+	"                        s.stavalues5::text::text[]) "
+	"                ) AS stats "
+	"            FROM pg_attribute AS a "
+	"            JOIN pg_statistic AS s "
+	"                ON s.starelid = a.attrelid "
+	"                AND s.staattnum = a.attnum "
+	"            WHERE a.attrelid = r.oid "
+	"            AND NOT a.attisdropped "
+	"            AND a.attnum > 0 "
+	"            AND has_column_privilege(a.attrelid, a.attnum, 'SELECT') "
+	"        ), "
+	"        attagg AS "
+	"        ( "
+	"            SELECT "
+	"                pcs.stainherit, "
+	"                jsonb_build_object( "
+	"                    'columns', "
+	"                    jsonb_object_agg( "
+	"                        pcs.attname, "
+	"                        pcs.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM per_column_stats AS pcs "
+	"            GROUP BY pcs.stainherit "
+	"        ), "
+	"        extended_object_stats AS "
+	"        ( "
+	"            SELECT "
+	"                false AS stxdinherit, "
+	"                e.stxname, "
+	"                jsonb_build_object( "
+	"                    'stxkinds', "
+	"                    to_jsonb(e.stxkind), "
+	"                    'stxdndistinct', "
+	"                    ndist.stxdndistinct, "
+	"                    'stxdndependencies', "
+	"                    ndep.stxdndependencies, "
+	"                ) AS stats "
+	"            FROM pg_statistic_ext AS e "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', string_to_array(nd.attnums, ', '), "
+	"                                'ndistinct', nd.ndistinct "
+	"                                ) "
+	"                            ORDER BY nd.ord "
+	"                        ) "
+	"                    FROM json_each_text(e.stxndistinct::text::json) "
+	"                         WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                ) AS ndist(stxdndistinct) ON e.stxndistinct IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', "
+	"                                string_to_array( replace(dep.attrs, ' => ', ', '), ', '), "
+	"                                'degree', "
+	"                                dep.degree "
+	"                                ) "
+	"                            ORDER BY dep.ord "
+	"                        ) "
+	"                    FROM json_each_text(.stxdependencies::text::json) "
+	"                         WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                ) AS ndep(stxdndependencies) ON e.stxdependencies IS NOT NULL "
+	"            WHERE e.stxrelid = r.oid "
+	"        ), "
+	"        extagg AS "
+	"        ( "
+	"            SELECT "
+	"                eos.stxdinherit, "
+	"                jsonb_build_object( "
+	"                    'extended', "
+	"                    jsonb_object_agg( "
+	"                        eos.stxname, "
+	"                        eos.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM extended_object_stats AS eos "
+	"            GROUP BY eos.stxdinherit "
+	"        ) "
+	"        SELECT "
+	"            jsonb_object_agg( "
+	"                CASE coalesce(a.stainherit, e.stxdinherit) "
+	"                    WHEN TRUE THEN 'inherited' "
+	"                    ELSE 'regular' "
+	"                END, "
+	"                coalesce(a.stats, '{}'::jsonb) || coalesce(e.stats, '{}'::jsonb)  "
+	"            ) "
+	"        FROM attagg AS a "
+	"        FULL OUTER JOIN extagg e ON a.stainherit = e.stxdinherit "
+	"    ) AS stats "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n "
+	"    ON n.oid = r.relnamespace "
+	"WHERE relkind IN ('r', 'm', 'f', 'p') "
+	"AND n.nspname NOT IN ('pg_catalog', 'information_schema')";
+
+
+int
+main(int argc, char *argv[])
+{
+	static struct option long_options[] = {
+		{"host", required_argument, NULL, 'h'},
+		{"port", required_argument, NULL, 'p'},
+		{"username", required_argument, NULL, 'U'},
+		{"no-password", no_argument, NULL, 'w'},
+		{"password", no_argument, NULL, 'W'},
+		{"echo", no_argument, NULL, 'e'},
+		{"dbname", required_argument, NULL, 'd'},
+		{NULL, 0, NULL, 0}
+	};
+
+	const char *progname;
+	int			optindex;
+	int			c;
+
+	const char *dbname = NULL;
+	char	   *host = NULL;
+	char	   *port = NULL;
+	char	   *username = NULL;
+	enum trivalue prompt_password = TRI_DEFAULT;
+	ConnParams	cparams;
+	bool		echo = false;
+
+	PQExpBufferData sql;
+	
+	PGconn	   *conn;
+
+	FILE	   *copystream = stdout;
+
+	PGresult   *result;
+
+	ExecStatusType result_status;
+
+	char	   *buf;
+	int			ret;
+
+	pg_logging_init(argv[0]);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pgscripts"));
+
+	handle_help_version_opts(argc, argv, "clusterdb", help);
+
+	while ((c = getopt_long(argc, argv, "d:eh:p:U:wW", long_options, &optindex)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				dbname = pg_strdup(optarg);
+				break;
+			case 'e':
+				echo = true;
+				break;
+			case 'h':
+				host = pg_strdup(optarg);
+				break;
+			case 'p':
+				port = pg_strdup(optarg);
+				break;
+			case 'U':
+				username = pg_strdup(optarg);
+				break;
+			case 'w':
+				prompt_password = TRI_NO;
+				break;
+			case 'W':
+				prompt_password = TRI_YES;
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Non-option argument specifies database name as long as it wasn't
+	 * already specified with -d / --dbname
+	 */
+	if (optind < argc && dbname == NULL)
+	{
+		dbname = argv[optind];
+		optind++;
+	}
+
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* fill cparams except for dbname, which is set below */
+	cparams.pghost = host;
+	cparams.pgport = port;
+	cparams.pguser = username;
+	cparams.prompt_password = prompt_password;
+	cparams.override_dbname = NULL;
+
+	setup_cancel_handler(NULL);
+
+	if (dbname == NULL)
+	{
+		if (getenv("PGDATABASE"))
+			dbname = getenv("PGDATABASE");
+		else if (getenv("PGUSER"))
+			dbname = getenv("PGUSER");
+		else
+			dbname = get_user_name_or_exit(progname);
+	}
+
+	cparams.dbname = dbname;
+
+	conn = connectDatabase(&cparams, progname, echo, false, true);
+
+	initPQExpBuffer(&sql);
+
+	appendPQExpBufferStr(&sql, "COPY (");
+
+	if (PQserverVersion(conn) >= 170000)
+		appendPQExpBufferStr(&sql, export_query_v17);
+	else if (PQserverVersion(conn) >= 150000)
+		appendPQExpBufferStr(&sql, export_query_v15);
+	else if (PQserverVersion(conn) >= 140000)
+		appendPQExpBufferStr(&sql, export_query_v14);
+	else if (PQserverVersion(conn) >= 120000)
+		appendPQExpBufferStr(&sql, export_query_v12);
+	else if (PQserverVersion(conn) >= 100000)
+		appendPQExpBufferStr(&sql, export_query_v10);
+	else
+		pg_fatal("exporting statistics from databases prior to version 10 not supported");
+
+	appendPQExpBufferStr(&sql, ") TO STDOUT");
+
+	result = PQexec(conn, sql.data);
+	result_status = PQresultStatus(result);
+
+	if (result_status != PGRES_COPY_OUT)
+		pg_fatal("malformed copy command");
+
+	for (;;)
+	{
+		ret = PQgetCopyData(conn, &buf, 0);
+
+		if (ret < 0)
+			break;				/* done or server/connection error */
+
+		if (buf)
+		{
+			if (copystream && fwrite(buf, 1, ret, copystream) != ret)
+				pg_fatal("could not write COPY data: %m");
+			PQfreemem(buf);
+		}
+	}
+
+	if (copystream && fflush(copystream))
+		pg_fatal("could not write COPY data: %m");
+
+	if (ret == -2)
+		pg_fatal("COPY data transfer failed: %s", PQerrorMessage(conn));
+
+	PQfinish(conn);
+	termPQExpBuffer(&sql);
+	exit(0);
+}
+
+
+static void
+help(const char *progname)
+{
+	printf(_("%s clusters all previously clustered tables in a database.\n\n"), progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_("  -d, --dbname=DBNAME       database to cluster\n"));
+	printf(_("  -e, --echo                show the commands being sent to the server\n"));
+	printf(_("  -t, --table=TABLE         cluster specific table(s) only\n"));
+	printf(_("  -V, --version             output version information, then exit\n"));
+	printf(_("  -?, --help                show this help, then exit\n"));
+	printf(_("\nConnection options:\n"));
+	printf(_("  -h, --host=HOSTNAME       database server host or socket directory\n"));
+	printf(_("  -p, --port=PORT           database server port\n"));
+	printf(_("  -U, --username=USERNAME   user name to connect as\n"));
+	printf(_("  -w, --no-password         never prompt for password\n"));
+	printf(_("  -W, --password            force password prompt\n"));
+	printf(_("  --maintenance-db=DBNAME   alternate maintenance database\n"));
+	printf(_("\nRead the description of the SQL command CLUSTER for details.\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
diff --git a/src/bin/scripts/pg_import_stats.c b/src/bin/scripts/pg_import_stats.c
new file mode 100644
index 0000000000..cfcadff769
--- /dev/null
+++ b/src/bin/scripts/pg_import_stats.c
@@ -0,0 +1,303 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_import_stats
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/scripts/pg_import_stats.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+#include "common.h"
+#include "common/logging.h"
+#include "fe_utils/cancel.h"
+#include "fe_utils/option_utils.h"
+#include "fe_utils/query_utils.h"
+#include "fe_utils/simple_list.h"
+#include "fe_utils/string_utils.h"
+
+#define COPY_BUF_LEN 8192
+
+static void help(const char *progname);
+
+int
+main(int argc, char *argv[])
+{
+	static struct option long_options[] = {
+		{"host", required_argument, NULL, 'h'},
+		{"port", required_argument, NULL, 'p'},
+		{"username", required_argument, NULL, 'U'},
+		{"no-password", no_argument, NULL, 'w'},
+		{"password", no_argument, NULL, 'W'},
+		{"quiet", no_argument, NULL, 'q'},
+		{"dbname", required_argument, NULL, 'd'},
+		{NULL, 0, NULL, 0}
+	};
+
+	const char *progname;
+	int			optindex;
+	int			c;
+
+	const char *dbname = NULL;
+	char	   *host = NULL;
+	char	   *port = NULL;
+	char	   *username = NULL;
+	enum trivalue prompt_password = TRI_DEFAULT;
+	ConnParams	cparams;
+	bool		quiet = false;
+
+	PGconn	   *conn;
+
+	FILE	   *copysrc= stdin;
+
+	PGresult   *result;
+
+	int		i;
+	int		numtables;
+
+	pg_logging_init(argv[0]);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pgscripts"));
+
+	handle_help_version_opts(argc, argv, "clusterdb", help);
+
+	while ((c = getopt_long(argc, argv, "d:h:p:qU:wW", long_options, &optindex)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				dbname = pg_strdup(optarg);
+				break;
+			case 'h':
+				host = pg_strdup(optarg);
+				break;
+			case 'p':
+				port = pg_strdup(optarg);
+				break;
+			case 'q':
+				quiet = true;
+				break;
+			case 'U':
+				username = pg_strdup(optarg);
+				break;
+			case 'w':
+				prompt_password = TRI_NO;
+				break;
+			case 'W':
+				prompt_password = TRI_YES;
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Non-option argument specifies database name as long as it wasn't
+	 * already specified with -d / --dbname
+	 */
+	if (optind < argc && dbname == NULL)
+	{
+		dbname = argv[optind];
+		optind++;
+	}
+
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* fill cparams except for dbname, which is set below */
+	cparams.pghost = host;
+	cparams.pgport = port;
+	cparams.pguser = username;
+	cparams.prompt_password = prompt_password;
+	cparams.override_dbname = NULL;
+
+	setup_cancel_handler(NULL);
+
+	if (dbname == NULL)
+	{
+		if (getenv("PGDATABASE"))
+			dbname = getenv("PGDATABASE");
+		else if (getenv("PGUSER"))
+			dbname = getenv("PGUSER");
+		else
+			dbname = get_user_name_or_exit(progname);
+	}
+
+	cparams.dbname = dbname;
+
+	conn = connectDatabase(&cparams, progname, false, false, true);
+
+	/* open file */
+
+	/* iterate over records */
+
+
+	result = PQexec(conn, 
+		"CREATE TEMPORARY TABLE import_stats ( "
+		"id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
+		"schemaname text, relname text, server_version_num integer, "
+		"n_tuples float4, n_pages integer, stats jsonb )");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("could not create temporary file: %s", PQerrorMessage(conn));
+
+	PQclear(result);
+
+	result = PQexec(conn, 
+		"COPY import_stats(schemaname, relname, server_version_num, n_tuples, "
+		"n_pages, stats) FROM STDIN");
+
+	if (PQresultStatus(result) != PGRES_COPY_IN)
+		pg_fatal("error copying data to import_stats: %s", PQerrorMessage(conn));
+
+	for (;;)
+	{
+		char copybuf[COPY_BUF_LEN];
+
+		int numread = fread(copybuf, 1, COPY_BUF_LEN, copysrc);
+
+		if (ferror(copysrc))
+			pg_fatal("error reading from source");
+
+		if (numread == 0)
+			break;
+
+		if (PQputCopyData(conn, copybuf, numread) == -1)
+			pg_fatal("eror during copy: %s", PQerrorMessage(conn));
+	}
+
+	if (PQputCopyEnd(conn, NULL) == -1)
+		pg_fatal("eror during copy: %s", PQerrorMessage(conn));
+	fclose(copysrc);
+
+	result = PQgetResult(conn);
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("error copying data to import_stats: %s", PQerrorMessage(conn));
+
+	numtables = atol(PQcmdTuples(result));
+
+	PQclear(result);
+
+	result = PQprepare(conn, "import", 
+		"SELECT pg_import_rel_stats(c.oid, s.server_version_num, "
+		"             s.n_tuples, s.n_pages, s.stats) as import_result "
+		"FROM import_stats AS s "
+		"JOIN pg_namespace AS n ON n.nspname = s.schemaname "
+		"JOIN pg_class AS c ON c.relnamespace = n.oid " 
+		"                   AND c.relname = s.relname "
+		"WHERE s.id = $1::bigint ",
+		1, NULL);
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
+
+	PQclear(result);
+
+	if (!quiet)
+	{
+		result = PQprepare(conn, "echo", 
+			"SELECT s.schemaname, s.relname "
+			"FROM import_stats AS s "
+			"WHERE s.id = $1::bigint ",
+			1, NULL);
+
+		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+			pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
+
+		PQclear(result);
+	}
+
+	for (i = 1; i <= numtables; i++)
+	{
+		char	istr[32];
+		char   *schema = NULL;
+		char   *table = NULL;
+
+		const char *const values[] = {istr};
+
+		snprintf(istr, 32, "%d", i);
+
+		if (!quiet)
+		{
+			result = PQexecPrepared(conn, "echo", 1, values, NULL, NULL, 0);
+			schema = pg_strdup(PQgetvalue(result, 0, 0));
+			table = pg_strdup(PQgetvalue(result, 0, 1));
+		}
+
+		PQclear(result);
+
+		result = PQexecPrepared(conn, "import", 1, values, NULL, NULL, 0);
+
+		if (quiet)
+		{
+			PQclear(result);
+			continue;
+		}
+
+		if (PQresultStatus(result) == PGRES_TUPLES_OK)
+		{
+			int 	rows = PQntuples(result);
+
+			if (rows == 1)
+			{
+				char   *retval = PQgetvalue(result, 0, 0);
+				if (*retval == 't')
+					printf("%s.%s: imported\n", schema, table);
+				else
+					printf("%s.%s: failed\n", schema, table);
+			}
+			else if (rows == 0)
+				printf("%s.%s: not found\n", schema, table);
+			else
+				pg_fatal("import function must return 0 or 1 rows");
+		}
+		else
+			printf("%s.%s: error: %s\n", schema, table, PQerrorMessage(conn));
+
+		if (schema != NULL)
+			pfree(schema);
+
+		if (table != NULL)
+			pfree(table);
+
+		PQclear(result);
+	}
+
+	exit(0);
+}
+
+
+static void
+help(const char *progname)
+{
+	printf(_("%s clusters all previously clustered tables in a database.\n\n"), progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_("  -d, --dbname=DBNAME       database to cluster\n"));
+	printf(_("  -q, --quiet               don't write any messages\n"));
+	printf(_("  -t, --table=TABLE         cluster specific table(s) only\n"));
+	printf(_("  -V, --version             output version information, then exit\n"));
+	printf(_("  -?, --help                show this help, then exit\n"));
+	printf(_("\nConnection options:\n"));
+	printf(_("  -h, --host=HOSTNAME       database server host or socket directory\n"));
+	printf(_("  -p, --port=PORT           database server port\n"));
+	printf(_("  -U, --username=USERNAME   user name to connect as\n"));
+	printf(_("  -w, --no-password         never prompt for password\n"));
+	printf(_("  -W, --password            force password prompt\n"));
+	printf(_("  --maintenance-db=DBNAME   alternate maintenance database\n"));
+	printf(_("\nRead the description of the SQL command CLUSTER for details.\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
-- 
2.41.0

---

From: Tomas Vondra <[email-masked]>
Date: Wed, 1 Nov 2023 21:07:27 +0100

On 10/31/23 08:25, Corey Huinker wrote:
> * imports index statistics. This is not strictly accurate: it 
> re-computes index statistics the same as ANALYZE does, which is to
> say it derives those stats entirely from table column stats, which
> are imported, so in that sense we're getting index stats without
> touching the heap.

Maybe I just don't understand, but I'm pretty sure ANALYZE does not
derive index stats from column stats. It actually builds them from the
row sample.

> serialized as an difficult-to-decompose bytea field.

Doesn't pg_mcv_list_items() already do all the heavy work?

regards

-- 
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

---

From: Corey Huinker <[email-masked]>
Date: Thu, 2 Nov 2023 01:01:49 -0400

> derive index stats from column stats. It actually builds them from the

That is correct, my error.

> Doesn't pg_mcv_list_items() already do all the heavy work?

Thanks! I'll look into that.

The comment below in mcv.c made me think there was no easy way to get
output.

/*
 * pg_mcv_list_out      - output routine for type pg_mcv_list.
 *
 * MCV lists are serialized into a bytea value, so we simply call byteaout()
 * to serialize the value into text. But it'd be nice to serialize that into
 * a meaningful representation (e.g. for inspection by people).
 *
 * XXX This should probably return something meaningful, similar to what
 * pg_dependencies_out does. Not sure how to deal with the deduplicated
 * values, though - do we want to expand that or not?
 */

---

From: Tomas Vondra <[email-masked]>
Date: Thu, 2 Nov 2023 14:52:20 +0100

>  * values, though - do we want to expand that or not?

Yeah, that was the simplest output function possible, it didn't seem
worth it to implement something more advanced. pg_mcv_list_items() is
more convenient for most needs, but it's quite far from the on-disk
representation.

That's actually a good question - how closely should the exported data
be to the on-disk format? I'd say we should keep it abstract, not tied
to the details of the on-disk format (which might easily change between
versions).

I'm a bit confused about the JSON schema used in pg_statistic_export
view, though. It simply serializes stakinds, stavalues, stanumbers into
arrays ... which works, but why not to use the JSON nesting? I mean,
there could be a nested document for histogram, MCV, ... with just the
correct fields.

  {
    ...
    histogram : { stavalues: [...] },
    mcv : { stavalues: [...], stanumbers: [...] },
    ...
  }

and so on. Also, what does TRIVIAL stand for?

regards

---

From: Shubham Khanna <[email-masked]>
Date: Mon, 6 Nov 2023 16:19:41 +0530

On Mon, Nov 6, 2023 at 4:16 PM Corey Huinker <[email-masked]> wrote:
>>
>>
>> Yeah, that use makes sense as well, and if so then postgres_fdw would likely need to be aware of the appropriate query for several versions back - they change, not by much, but they do change. So now we'd have each query text in three places: a system view, postgres_fdw, and the bin/scripts pre-upgrade program. So I probably should consider the best way to share those in the codebase.
>>
>

While applying Patch, I noticed few Indentation issues:
1) D:\Project\Postgres>git am v2-0003-Add-pg_import_rel_stats.patch
.git/rebase-apply/patch:1265: space before tab in indent.
                                        errmsg("invalid statistics
format, stxndeprs must be array or null");
.git/rebase-apply/patch:1424: trailing whitespace.
                                   errmsg("invalid statistics format,
stxndistinct attnums elements must be strings, but one is %s",
.git/rebase-apply/patch:1315: new blank line at EOF.
+
warning: 3 lines add whitespace errors.
Applying: Add pg_import_rel_stats().

2) D:\Project\Postgres>git am v2-0004-Add-pg_export_stats-pg_import_stats.patch
.git/rebase-apply/patch:282: trailing whitespace.
const char *export_query_v14 =
.git/rebase-apply/patch:489: trailing whitespace.
const char *export_query_v12 =
.git/rebase-apply/patch:648: trailing whitespace.
const char *export_query_v10 =
.git/rebase-apply/patch:826: trailing whitespace.

.git/rebase-apply/patch:1142: trailing whitespace.
        result = PQexec(conn,
warning: squelched 4 whitespace errors
warning: 9 lines add whitespace errors.
Applying: Add pg_export_stats, pg_import_stats.

Thanks and Regards,
Shubham Khanna.

---

From: Ashutosh Bapat <[email-masked]>
Date: Tue, 7 Nov 2023 14:53:54 +0530

On Tue, Oct 31, 2023 at 12:55 PM Corey Huinker <[email-masked]> wrote:
> * imports index statistics. This is not strictly accurate: it re-computes index statistics the same as ANALYZE does, which is to say it derives those stats entirely from table column stats, which are imported, so in that sense we're getting index stats without touching the heap.
> * now support extended statistics except for MCV, which is currently serialized as an difficult-to-decompose bytea field.
> * bare-bones CLI script pg_export_stats, which extracts stats on databases back to v12 (tested) and could work back to v10.
> * bare-bones CLI script pg_import_stats, which obviously only works on current devel dbs, but can take exports from older versions.
>

I did a small experiment with your patches. In a separate database
"fdw_dst" I created a table t1 and populated it with 100K rows
#create table t1 (a int, b int);
#insert into t1 select i, i + 1 from generate_series(1, 100000) i;
#analyse t1;

In database "postgres" on the same server, I created a foreign table
pointing to t1
#create server fdw_dst_server foreign data wrapper postgres_fdw
OPTIONS ( dbname 'fdw_dst', port '5432');
#create user mapping for public server fdw_dst_server ;
#create foreign table t1 (a int, b int) server fdw_dst_server;

The estimates are off
#explain select * from t1 where a = 100;
                        QUERY PLAN
-----------------------------------------------------------
 Foreign Scan on t1  (cost=100.00..142.26 rows=13 width=8)
(1 row)

Export and import stats for table t1
$ pg_export_stats -d fdw_dst | pg_import_stats -d postgres

gives accurate estimates
#explain select * from t1 where a = 100;
                        QUERY PLAN
-----------------------------------------------------------
 Foreign Scan on t1  (cost=100.00..1793.02 rows=1 width=8)
(1 row)

In this simple case it's working like a charm.

Then I wanted to replace all ANALYZE commands in postgres_fdw.sql with
import and export of statistics. But I can not do that since it
requires table names to match. Foreign table metadata stores the
mapping between local and remote table as well as column names. Import
can use that mapping to install the statistics appropriately. We may
want to support a command or function in postgres_fdw to import
statistics of all the tables that point to a given foreign server.
That may be some future work based on your current patches.

I have not looked at the code though.

--
Best Wishes,
Ashutosh Bapat

---

From: Corey Huinker <[email-masked]>
Date: Wed, 13 Dec 2023 05:26:04 -0500

> more convenient for most needs, but it's quite far from the on-disk

I was able to make it work.

> to the details of the on-disk format (which might easily change between

For the most part, I chose the exported data json types and formats in a
way that was the most accommodating to cstring input functions. So, while
so many of the statistic values are obviously only ever integers/floats,
those get stored as a numeric data type which lacks direct
numeric->int/float4/float8 functions (though we could certainly create
them, and I'm not against that), casting them to text lets us leverage
pg_strtoint16, etc.

>     mcv : { stavalues: [...], stanumbers: [...] },

That's a very good question. I went with this format because it was fairly
straightforward to code in SQL using existing JSON/JSONB functions, and
that's what we will need if we want to export statistics on any server
currently in existence. I'm certainly not locked in with the current
format, and if it can be shown how to transform the data into a superior
format, I'd happily do so.

It's currently serving double-duty for "there are no stats in this slot"
and the situations where the stats computation could draw no conclusions
about the data.

Attached is v3 of this patch. Key features are:

* Handles regular pg_statistic stats for any relation type.
* Handles extended statistics.
* Export views pg_statistic_export and pg_statistic_ext_export to allow
inspection of existing stats and saving those values for later use.
* Import functions pg_import_rel_stats() and pg_import_ext_stats() which
take Oids as input. This is intentional to allow stats from one object to
be imported into another object.
* User scripts pg_export_stats and pg_import stats, which offer a primitive
way to serialize all the statistics of one database and import them into
another.
* Has regression test coverage for both with a variety of data types.
* Passes my own manual test of extracting all of the stats from a v15
version of the popular "dvdrental" example database, as well as some
additional extended statistics objects, and importing them into a
development database.
* Import operations never touch the heap of any relation outside of
pg_catalog. As such, this should be significantly faster than even the most
cursory analyze operation, and therefore should be useful in upgrade
situations, allowing the database to work with "good enough" stats more
quickly, while still allowing for regular autovacuum to recalculate the
stats "for real" at some later point.

The relation statistics code was adapted from similar features in
analyze.c, but is now done in a query context. As before, the
rowcount/pagecount values are updated on pg_class in a non-transactional
fashion to avoid table bloat, while the updates to pg_statistic are
pg_statistic_ext_data are done transactionally.

The existing statistics _store() functions were leveraged wherever
practical, so much so that the extended statistics import is mostly just
adapting the existing _build() functions into _import() functions which
pull their values from JSON rather than computing the statistics.

Current concerns are:

1. I had to code a special-case exception for MCELEM stats on array data
types, so that the array_in() call uses the element type rather than the
array type. I had assumed that the existing exmaine_attribute() functions
would have properly derived the typoid for that column, but it appears to
not be the case, and I'm clearly missing how the existing code gets it
right.
2. This hasn't been tested with external custom datatypes, but if they have
a custom typanalyze function things should be ok.
3. While I think I have cataloged all of the schema-structural changes to
pg_statistic[_ext[_data]] since version 10, I may have missed a case where
the schema stayed the same, but the values are interpreted differently.
4. I don't yet have a complete vision for how these tools will be used by
pg_upgrade and pg_dump/restore, the places where these will provide the
biggest win for users.
From 4ae4ac484bcaf4b74fef1284d63e6e6d4d6a236f Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 12 Dec 2023 20:48:42 -0500
Subject: [PATCH v3 2/9] Add system view pg_statistic_export.

This view is designed to aid in the export (and re-import) of table
statistics, mostly for upgrade/restore situations.
---
 src/backend/catalog/system_views.sql | 84 ++++++++++++++++++++++++++++
 src/test/regress/expected/rules.out  | 31 ++++++++++
 doc/src/sgml/system-views.sgml       |  5 ++
 3 files changed, 120 insertions(+)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 11d18ed9dd..7655bf7458 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -274,6 +274,90 @@ CREATE VIEW pg_stats WITH (security_barrier) AS

+
+
+CREATE VIEW pg_statistic_export WITH (security_barrier) AS
+    SELECT
+        n.nspname AS schemaname,
+        r.relname AS relname,
+        current_setting('server_version_num')::integer AS server_version_num,
+        r.reltuples::float4 AS n_tuples,
+        r.relpages::integer AS n_pages,
+        (
+            SELECT
+                jsonb_object_agg(
+                    CASE
+                        WHEN a.stainherit THEN 'inherited'
+                        ELSE 'regular'
+                    END,
+                    a.stats
+                )
+            FROM
+            (
+                SELECT
+                    s.stainherit,
+                    jsonb_object_agg(
+                        a.attname,
+                        jsonb_build_object(
+                            'stanullfrac', s.stanullfrac::text,
+                            'stawidth', s.stawidth::text,
+                            'stadistinct', s.stadistinct::text,
+                            'stakinds',
+                            (
+                                SELECT
+                                    jsonb_agg(
+                                        CASE kind.kind
+                                            WHEN 0 THEN 'TRIVIAL'
+                                            WHEN 1 THEN 'MCV'
+                                            WHEN 2 THEN 'HISTOGRAM'
+                                            WHEN 3 THEN 'CORRELATION'
+                                            WHEN 4 THEN 'MCELEM'
+                                            WHEN 5 THEN 'DECHIST'
+                                            WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'
+                                            WHEN 7 THEN 'BOUNDS_HISTOGRAM'
+                                        END::text
+                                        ORDER BY kind.ord)
+                                FROM unnest(ARRAY[s.stakind1, s.stakind2,
+                                            s.stakind3, stakind4,
+                                            s.stakind5])
+                                     WITH ORDINALITY AS kind(kind, ord)
+                            ),
+                            'stanumbers',
+                            jsonb_build_array(
+                                s.stanumbers1::text,
+                                s.stanumbers2::text,
+                                s.stanumbers3::text,
+                                s.stanumbers4::text,
+                                s.stanumbers5::text),
+                            'stavalues',
+                            jsonb_build_array(
+                                -- casting to text makes it easier to import using array_in()
+                                s.stavalues1::text,
+                                s.stavalues2::text,
+                                s.stavalues3::text,
+                                s.stavalues4::text,
+                                s.stavalues5::text)
+                        )
+                    ) AS stats
+                FROM pg_attribute AS a
+                JOIN pg_statistic AS s
+                    ON s.starelid = a.attrelid
+                    AND s.staattnum = a.attnum
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+                AND has_column_privilege(a.attrelid, a.attnum, 'SELECT')
+                GROUP BY s.stainherit
+            ) AS a
+        ) AS stats
+    FROM pg_class AS r
+    JOIN pg_namespace AS n
+        ON n.oid = r.relnamespace
+    WHERE relkind IN ('r', 'm', 'f', 'p', 'i')
+    AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema');
+
+
+
 CREATE VIEW pg_stats_ext WITH (security_barrier) AS
     SELECT cn.nspname AS schemaname,
            c.relname AS tablename,
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 05070393b9..f2b059af5e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2404,6 +2404,37 @@ pg_statio_user_tables| SELECT relid,
     tidx_blks_hit
    FROM pg_statio_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_statistic_export| SELECT n.nspname AS schemaname,
+    r.relname,
+    (current_setting('server_version_num'::text))::integer AS server_version_num,
+    r.reltuples AS n_tuples,
+    r.relpages AS n_pages,
+    ( SELECT jsonb_object_agg(
+                CASE
+                    WHEN a.stainherit THEN 'inherited'::text
+                    ELSE 'regular'::text
+                END, a.stats) AS jsonb_object_agg
+           FROM ( SELECT s.stainherit,
+                    jsonb_object_agg(a_1.attname, jsonb_build_object('stanullfrac', (s.stanullfrac)::text, 'stawidth', (s.stawidth)::text, 'stadistinct', (s.stadistinct)::text, 'stakinds', ( SELECT jsonb_agg(
+                                CASE kind.kind
+                                    WHEN 0 THEN 'TRIVIAL'::text
+                                    WHEN 1 THEN 'MCV'::text
+                                    WHEN 2 THEN 'HISTOGRAM'::text
+                                    WHEN 3 THEN 'CORRELATION'::text
+                                    WHEN 4 THEN 'MCELEM'::text
+                                    WHEN 5 THEN 'DECHIST'::text
+                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'::text
+                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM'::text
+                                    ELSE NULL::text
+                                END ORDER BY kind.ord) AS jsonb_agg
+                           FROM unnest(ARRAY[s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5]) WITH ORDINALITY kind(kind, ord)), 'stanumbers', jsonb_build_array((s.stanumbers1)::text, (s.stanumbers2)::text, (s.stanumbers3)::text, (s.stanumbers4)::text, (s.stanumbers5)::text), 'stavalues', jsonb_build_array((s.stavalues1)::text, (s.stavalues2)::text, (s.stavalues3)::text, (s.stavalues4)::text, (s.stavalues5)::text))) AS stats
+                   FROM (pg_attribute a_1
+                     JOIN pg_statistic s ON (((s.starelid = a_1.attrelid) AND (s.staattnum = a_1.attnum))))
+                  WHERE ((a_1.attrelid = r.oid) AND (NOT a_1.attisdropped) AND (a_1.attnum > 0) AND has_column_privilege(a_1.attrelid, a_1.attnum, 'SELECT'::text))
+                  GROUP BY s.stainherit) a) AS stats
+   FROM (pg_class r
+     JOIN pg_namespace n ON ((n.oid = r.relnamespace)))
+  WHERE ((r.relkind = ANY (ARRAY['r'::"char", 'm'::"char", 'f'::"char", 'p'::"char", 'i'::"char"])) AND (n.nspname <> ALL (ARRAY['pg_catalog'::name, 'pg_toast'::name, 'information_schema'::name])));
 pg_stats| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     a.attname,
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 0ef1745631..91b3ab22fb 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -191,6 +191,11 @@
       <entry>extended planner statistics for expressions</entry>
      </row>
 
+     <row>
2.43.0

From 7cc09a452ce4407fad83e0fcc9723d43121d4db1 Mon Sep 17 00:00:00 2001
From: coreyhuinker <[email-masked]>
Date: Mon, 30 Oct 2023 16:21:30 -0400
Subject: [PATCH v3 1/9] Additional internal jsonb access functions.

 
 
2.43.0

From 2260a0f53572d8422857237e4065da619401a0f1 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 12 Dec 2023 22:21:42 -0500
Subject: [PATCH v3 3/9] Add pg_import_rel_stats()

The function pg_import_rel_stats imports rowcount, pagecount, and column
statistics for a given table or index.

While the statistics are applied transactionally, the changes to
pg_class (reltuples and relpages) are not. This decision was made
to avoid bloat of pg_class and is in line with the behavior of VACUUM.

This function also allows for tweaking of table statistics in-place,
allowing the user to inflate rowcounts, skew histograms, etc, to see
what those changes will evoke from the query planner.
---
 src/include/catalog/pg_proc.dat               |   5 +
 src/include/statistics/statistics.h           |  16 +
 src/backend/statistics/Makefile               |   3 +-
 src/backend/statistics/meson.build            |   1 +
 src/backend/statistics/statistics.c           | 806 ++++++++++++++++++
 .../regress/expected/stats_export_import.out  | 137 +++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/stats_export_import.sql  | 119 +++
 doc/src/sgml/func.sgml                        |  43 +
 9 files changed, 1130 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/statistics/statistics.c
 create mode 100644 src/test/regress/expected/stats_export_import.out
 create mode 100644 src/test/regress/sql/stats_export_import.sql

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 77e8b13764..4d1e9bde1f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5655,6 +5655,11 @@
   proname => 'pg_stat_get_db_stat_reset_time', provolatile => 's',
   proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
   prosrc => 'pg_stat_get_db_stat_reset_time' },
+{ oid => '3814',
+  descr => 'statistics: import to relation',
+  proname => 'pg_import_rel_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool', proargtypes => 'oid int4 float4 int4 jsonb',
+  prosrc => 'pg_import_rel_stats' },
 { oid => '3150', descr => 'statistics: number of temporary files written',
   proname => 'pg_stat_get_db_temp_files', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 5e538fec32..4251558593 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -15,6 +15,7 @@
 
 #include "commands/vacuum.h"
 #include "nodes/pathnodes.h"
+#include "utils/jsonb.h"
 
 #define STATS_MAX_DIMENSIONS	8	/* max number of attributes */
 
@@ -101,6 +102,7 @@ extern MCVList *statext_mcv_load(Oid mvoid, bool inh);
 extern void BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
 									   int numrows, HeapTuple *rows,
 									   int natts, VacAttrStats **vacattrstats);
+
 extern int	ComputeExtStatisticsRows(Relation onerel,
 									 int natts, VacAttrStats **vacattrstats);
 extern bool statext_is_kind_built(HeapTuple htup, char type);
@@ -127,4 +129,18 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern char *key_lookup_cstring(JsonbContainer *cont, const char *key);
+extern JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key);
+extern JsonbContainer *key_lookup_array(JsonbContainer *cont, const char *key);
+
+extern Datum pg_import_rel_stats(PG_FUNCTION_ARGS);
+
+extern VacAttrStats *examine_rel_attribute(Form_pg_attribute attr,
+										   Relation onerel, Node *index_expr);
+
+extern
+void import_attribute(Oid relid, const VacAttrStats *stat,
+					  JsonbContainer *cont, bool inh, Datum values[],
+					  bool nulls[], bool replaces[]);
+
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index e12737b011..1e6e100d3c 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c'
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..968ccfaaf2
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,806 @@
+/*-------------------------------------------------------------------------
+ *
+ * statistics.c
+ *
+ * IDENTIFICATION
+ *	  src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_operator.h"
+#include "fmgr.h"
+#include "nodes/nodeFuncs.h"
+#include "utils/builtins.h"
+#include "utils/datum.h" /* REMOVE */
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/jsonb.h"
+#include "utils/numeric.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+#include "statistics/statistics.h"
+
+
+static int16
+decode_stakind_string(char *s);
+
+static
+void import_pg_statistic(Relation rel, bool inh, JsonbContainer *cont);
+
+static
+void import_stakinds(const VacAttrStats *stat, JsonbContainer *cont,
+					 bool inh, int16 kindenums[], Datum kindvalues[],
+					 bool kindnulls[], bool kindreplaces[], Datum opvalues[],
+					 bool opnulls[], bool opreplaces[], Datum collvalues[],
+					 bool collnulls[], bool collreplaces[]);
+
+static
+void import_stanumbers(const VacAttrStats *stat, JsonbContainer *cont,
+					   Datum kindvalues[], bool kindnulls[],
+					   bool kindreplaces[]);
+
+static
+void import_stavalues(const VacAttrStats *stat, JsonbContainer *cont,
+					  int16 kindenums[], Datum valvalues[],
+					  bool valnulls[], bool valreplaces[]);
+
+
+/*
+ * Import staistic from:
+ *   root->"regular"
+ *   and
+ *   root->"inherited"
+ *
+ * Container format is:
+ *
+ * {
+ * 	 "colname1": { ...per column stats... },
+ * 	 "colname2": { ...per column stats... },
+ *   ...
+ * }
+ *
+ */
+static
+void import_pg_statistic(Relation rel, bool inh, JsonbContainer *cont)
+{
+	TupleDesc   		tupdesc = RelationGetDescr(rel);
+	Oid					relid = RelationGetRelid(rel);
+	int					natts = tupdesc->natts;
+	CatalogIndexState	indstate = NULL;
+	Relation			sd;
+	int					i;
+	bool				has_index_exprs = false;
+	ListCell		   *indexpr_item = NULL;
+
+	if (cont == NULL)
+		return;
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+
+	/*
+	 * If this relation is an index and that index has expressions in
+	 * it, then we will need to keep the list of remaining expressions
+	 * aligned with the attributes as we iterate over them, whether or
+	 * not those attributes have statistics to import.
+	 */
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+			|| (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		 && (rel->rd_indexprs != NIL))
+	{
+		has_index_exprs = true;
+		indexpr_item = list_head(rel->rd_indexprs);
+	}
+
+	for (i = 0; i < natts; i++)
+	{
+
+		Form_pg_attribute	att;
+		char			   *name;
+		JsonbContainer	   *attrcont;
+		VacAttrStats	   *stat;
+		Node			   *index_expr = NULL;
+
+		att = TupleDescAttr(tupdesc, i);
+
+		if (att->attisdropped)
+			continue;
+
+		if (has_index_exprs && (rel->rd_index->indkey.values[i] == 0))
+		{
+			if (indexpr_item == NULL)   /* shouldn't happen */
+				elog(ERROR, "too few entries in indexprs list");
+
+			index_expr = (Node *) lfirst(indexpr_item);
+			indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+		}
+
+		stat = examine_rel_attribute(att, rel, index_expr);
+
+		name = NameStr(att->attname);
+
+		attrcont = key_lookup_object(cont, name);
+
+		if (attrcont != NULL)
+		{
+			Datum		values[Natts_pg_statistic] = { 0 };
+			bool		nulls[Natts_pg_statistic] = { false };
+			bool		replaces[Natts_pg_statistic] = { false };
+			HeapTuple	stup,
+						oldtup;
+
+			import_attribute(relid, stat, attrcont, inh, values, nulls, replaces);
+
+			/* Is there already a pg_statistic tuple for this attribute? */
+			oldtup = SearchSysCache3(STATRELATTINH,
+									 ObjectIdGetDatum(RelationGetRelid(rel)),
+									 Int16GetDatum(att->attnum),
+									 BoolGetDatum(inh));
+
+			/* Open index information when we know we need it */
+			if (indstate == NULL)
+				indstate = CatalogOpenIndexes(sd);
+
+			if (HeapTupleIsValid(oldtup))
+			{
+				/* Yes, replace it */
+				stup = heap_modify_tuple(oldtup,
+										 RelationGetDescr(sd),
+										 values,
+										 nulls,
+										 replaces);
+				ReleaseSysCache(oldtup);
+				CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+			}
+			else
+			{
+				/* No, insert new tuple */
+				stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+				CatalogTupleInsertWithInfo(sd, stup, indstate);
+			}
+			heap_freetuple(stup);
+		}
+		/* DEBUG pfree(stat); */
+	}
+
+	if (indstate != NULL)
+		CatalogCloseIndexes(indstate);
+	table_close(sd, RowExclusiveLock);
+}
+
+/*
+ * Import statitics for one attribute
+ *
+ */
+void
+import_attribute(Oid relid, const VacAttrStats *stat,
+				 JsonbContainer *cont, bool inh,
+				 Datum values[], bool nulls[], bool replaces[])
+{
+	JsonbContainer *arraycont;
+	char		   *s;
+	int16			kindenums[STATISTIC_NUM_SLOTS] = {0};
+
+	Assert(cont != NULL);
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(stat->tupattnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inh);
+
+	s = key_lookup_cstring(cont, "stanullfrac");
+	if (s != NULL)
+	{
+		float4 f = float4in_internal(s, NULL, "real", s, NULL);
+		pfree(s);
+		values[Anum_pg_statistic_stanullfrac - 1] = Float4GetDatum(f);
+		replaces[Anum_pg_statistic_stanullfrac - 1] = true;
+	}
+
+	s = key_lookup_cstring(cont, "stawidth");
+	if (s != NULL)
+	{
+		int32 d = pg_strtoint32(s);
+		pfree(s);
+		values[Anum_pg_statistic_stawidth - 1] = Int32GetDatum(d);
+		replaces[Anum_pg_statistic_stawidth - 1] = true;
+	}
+
+	s = key_lookup_cstring(cont, "stadistinct");
+	if (s != NULL)
+	{
+		float4 f = float4in_internal(s, NULL, "real", s, NULL);
+		pfree(s);
+		values[Anum_pg_statistic_stadistinct - 1] = Float4GetDatum(f);
+		replaces[Anum_pg_statistic_stadistinct - 1] = true;
+	}
+
+	arraycont = key_lookup_array(cont, "stakinds");
+	import_stakinds(stat, arraycont, inh, kindenums,
+					&values[Anum_pg_statistic_stakind1 - 1],
+					&nulls[Anum_pg_statistic_stakind1 - 1],
+					&replaces[Anum_pg_statistic_stakind1 - 1],
+					&values[Anum_pg_statistic_staop1 - 1],
+					&nulls[Anum_pg_statistic_staop1 - 1],
+					&replaces[Anum_pg_statistic_staop1 - 1],
+					&values[Anum_pg_statistic_stacoll1 - 1],
+					&nulls[Anum_pg_statistic_stacoll1 - 1],
+					&replaces[Anum_pg_statistic_stacoll1 - 1]);
+
+	arraycont = key_lookup_array(cont, "stanumbers");
+	import_stanumbers(stat, arraycont,
+					  &values[Anum_pg_statistic_stanumbers1 - 1],
+					  &nulls[Anum_pg_statistic_stanumbers1 - 1],
+					  &replaces[Anum_pg_statistic_stanumbers1 - 1]);
+
+	arraycont = key_lookup_array(cont, "stavalues");
+	import_stavalues(stat, arraycont, kindenums,
+					 &values[Anum_pg_statistic_stavalues1 - 1],
+					 &nulls[Anum_pg_statistic_stavalues1 - 1],
+					 &replaces[Anum_pg_statistic_stavalues1 - 1]);
+}
+
+/*
+ * import stakinds values from json, the values of which determine
+ * the staop and stacoll values to use as well.
+ */
+static
+void import_stakinds(const VacAttrStats *stat, JsonbContainer *cont,
+					 bool inh, int16 kindenums[], Datum kindvalues[],
+					 bool kindnulls[], bool kindreplaces[], Datum opvalues[],
+					 bool opnulls[], bool opreplaces[], Datum collvalues[],
+					 bool collnulls[], bool collreplaces[])
+{
+	int k;
+	int numkinds = 0;
+
+	if (cont != NULL)
+	{
+		TypeCacheEntry *typentry = lookup_type_cache(stat->attrtypid,
+													TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+		Datum	lt_opr = ObjectIdGetDatum(typentry->lt_opr);
+		Datum	eq_opr = ObjectIdGetDatum(typentry->eq_opr);
+
+		numkinds = JsonContainerSize(cont);
+
+		if (numkinds > STATISTIC_NUM_SLOTS)
+			ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid format: number of stakinds %d is greater than available slots %d",
+						numkinds, STATISTIC_NUM_SLOTS)));
+
+		for (k = 0; k < numkinds; k++)
+		{
+			JsonbValue *j = getIthJsonbValueFromContainer(cont, k);
+			int16		kind;
+			char	   *s;
+
+			if (j == NULL || (j->type != jbvString))
+				ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("invalid format: stakind elements must be strings")));
+
+			s = JsonbStringValueToCString(j);
+			kind = decode_stakind_string(s);
+			pfree(s);
+			pfree(j);
+
+			kindenums[k] = kind;
+			kindvalues[k] = Int16GetDatum(kind);
+			kindreplaces[k] = true;
+
+			switch(kind)
+			{
+				case STATISTIC_KIND_MCV:
+					opvalues[k] = eq_opr;
+					opreplaces[k] = true;
+					collvalues[k] = ObjectIdGetDatum(stat->attrcollid);
+					collreplaces[k] = true;
+					break;
+
+				case STATISTIC_KIND_HISTOGRAM:
+				case STATISTIC_KIND_CORRELATION:
+					opvalues[k] = lt_opr;
+					opreplaces[k] = true;
+					collvalues[k] = ObjectIdGetDatum(stat->attrcollid);
+					collreplaces[k] = true;
+					break;
+
+				case STATISTIC_KIND_MCELEM:
+				case STATISTIC_KIND_DECHIST:
+					opvalues[k] = ObjectIdGetDatum(TextEqualOperator);
+					opreplaces[k] = true;
+					collvalues[k] = ObjectIdGetDatum(DEFAULT_COLLATION_OID);
+					collreplaces[k] = true;
+					break;
+
+				case STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM:
+					opvalues[k] = ObjectIdGetDatum(Float8LessOperator);
+					opreplaces[k] = true;
+					collvalues[k] = ObjectIdGetDatum(InvalidOid);
+					collreplaces[k] = true;
+					break;
+
+				case STATISTIC_KIND_BOUNDS_HISTOGRAM:
+				default:
+					opvalues[k] = ObjectIdGetDatum(InvalidOid);
+					opreplaces[k] = true;
+					collvalues[k] = ObjectIdGetDatum(InvalidOid);
+					collreplaces[k] = true;
+					break;
+			}
+		}
+	}
+
+	/* fill out empty slots, but do not replace */
+	for (k = numkinds; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		kindvalues[k] = Int16GetDatum(0);
+		opvalues[k] = ObjectIdGetDatum(InvalidOid);
+		collvalues[k] = ObjectIdGetDatum(InvalidOid);
+	}
+}
+
+static
+void import_stanumbers(const VacAttrStats *stat, JsonbContainer *cont,
+					   Datum numvalues[], bool numnulls[],
+					   bool numreplaces[])
+{
+	int numnumbers = 0;
+	int k;
+
+	if (cont != NULL)
+	{
+		FmgrInfo	finfo;
+
+		numnumbers = JsonContainerSize(cont);
+
+		if (numnumbers > STATISTIC_NUM_SLOTS)
+			ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid format: number of stanumbers %d is greater than available slots %d",
+						numnumbers, STATISTIC_NUM_SLOTS)));
+
+		fmgr_info(F_ARRAY_IN, &finfo);
+
+		for (k = 0; k < numnumbers; k++)
+		{
+			JsonbValue *j = getIthJsonbValueFromContainer(cont, k);
+
+			if (j == NULL)
+			{
+				numvalues[k] = (Datum) 0;
+				numnulls[k] = true;
+				continue;
+			}
+
+			if (j->type == jbvNull)
+			{
+				numvalues[k] = (Datum) 0;
+				numnulls[k] = true;
+				pfree(j);
+				continue;
+			}
+
+			if (j->type == jbvString)
+			{
+				char *s = JsonbStringValueToCString(j);
+
+				numvalues[k] = FunctionCall3(&finfo, CStringGetDatum(s),
+											 ObjectIdGetDatum(FLOAT4OID),
+											 Int32GetDatum(0));
+				numreplaces[k] = true;
+				pfree(s);
+				pfree(j);
+				continue;
+			}
+			else
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stanumbers elements "
+						  "must be a string that is castable to an array of floats")));
+
+		}
+	}
+
+	/* fill out empty slots, but do not replace */
+	for (k = numnumbers; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		numvalues[k] = (Datum) 0;
+		numnulls[k] = true;
+	}
+}
+
+static
+void import_stavalues(const VacAttrStats *stat, JsonbContainer *cont,
+					  int16 kindenums[], Datum valvalues[],
+					  bool valnulls[], bool valreplaces[])
+{
+	int numvals = 0;
+	int k;
+
+	if (cont != NULL)
+	{
+		FmgrInfo	finfo;
+
+		fmgr_info(F_ARRAY_IN, &finfo);
+		numvals = JsonContainerSize(cont);
+
+		if (numvals > STATISTIC_NUM_SLOTS)
+			ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid format: number of stavalues %d is greater than available slots %d",
+						numvals, STATISTIC_NUM_SLOTS)));
+
+		for (k = 0; k < numvals; k++)
+		{
+			JsonbValue *j = getIthJsonbValueFromContainer(cont, k);
+
+			if (j == NULL)
+			{
+				valvalues[k] = (Datum) 0;
+				valnulls[k] = true;
+				continue;
+			}
+
+			if (j->type == jbvNull)
+			{
+				valvalues[k] = (Datum) 0;
+				valnulls[k] = true;
+				pfree(j);
+				continue;
+			}
+
+			if (j->type == jbvString)
+			{
+				char   *s = JsonbStringValueToCString(j);
+				Oid		typoid = stat->statypid[k];
+				int32	typmod = 0;
+
+				/*
+				 * MCELEM stat arrays are of the same type as the
+				 * array base element type.
+				 */
+				if (kindenums[k] == STATISTIC_KIND_MCELEM)
+				{
+					TypeCacheEntry *typentry = lookup_type_cache(typoid, 0);
+					if (IsTrueArrayType(typentry))
+						typoid = typentry->typelem;
+				}
+				valvalues[k] = FunctionCall3(&finfo, CStringGetDatum(s),
+								 ObjectIdGetDatum(typoid),
+								 Int32GetDatum(typmod));
+				valreplaces[k] = true;
+				pfree(s);
+				pfree(j);
+			}
+			else
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stavalues elements must "
+						  "be a string that is castable to an array of the "
+						  "column type")));
+
+		}
+	}
+
+	/* fill out empty slots, but do not replace */
+	for (k = numvals; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		valvalues[k] = (Datum) 0;
+		valnulls[k] = true;
+	}
+}
+
+/*
+ * Get a JsonbValue from a JsonbContainer and ensure that it is a string,
+ * and return the cstring.
+ */
+char *key_lookup_cstring(JsonbContainer *cont, const char *key)
+{
+	JsonbValue	j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvString)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be a string but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	return JsonbStringValueToCString(&j);
+}
+
+/*
+ * Get a JsonbContainer from a JsonbContainer and ensure that it is a object
+ */
+JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key)
+{
+	JsonbValue		j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvBinary)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an object but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	if (!JsonContainerIsObject(j.val.binary.data))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an object but is type %s",
+				  key, JsonbContainerTypeName(j.val.binary.data))));
+
+	return j.val.binary.data;
+}
+
+/*
+ * Get a JsonbContainer from a JsonbContainer and ensure that it is an array
+ */
+JsonbContainer *key_lookup_array(JsonbContainer *cont, const char *key)
+{
+	JsonbValue	j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvBinary)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an array but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	if (!JsonContainerIsArray(j.val.binary.data))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an array but is type %s",
+				  key, JsonbContainerTypeName(j.val.binary.data))));
+
+	return j.val.binary.data;
+}
+
+/*
+ * Convert the STATISTICS_KIND strings defined in pg_statistic_export
+ * back to their defined enum values.
+ */
+static int16
+decode_stakind_string(char *s)
+{
+	if (strcmp(s,"MCV") == 0)
+		return STATISTIC_KIND_MCV;
+	if (strcmp(s,"HISTOGRAM") == 0)
+		return STATISTIC_KIND_HISTOGRAM;
+	if (strcmp(s,"CORRELATION") == 0)
+		return STATISTIC_KIND_CORRELATION;
+	if (strcmp(s,"MCELEM") == 0)
+		return STATISTIC_KIND_MCELEM;
+	if (strcmp(s,"DECHIST") == 0)
+		return STATISTIC_KIND_DECHIST;
+	if (strcmp(s,"RANGE_LENGTH_HISTOGRAM") == 0)
+		return STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM;
+	if (strcmp(s,"BOUNDS_HISTOGRAM") == 0)
+		return STATISTIC_KIND_BOUNDS_HISTOGRAM;
+	if (strcmp(s,"TRIVIAL") != 0)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("unknown statistics kind: %s", s)));
+
+	return 0;
+}
+
+/*
+ * examine_rel_attribute -- pre-analysis of a single column
+ *
+ * Determine whether the column is analyzable; if so, create and initialize
+ * a VacAttrStats struct for it.  If not, return NULL.
+ *
+ * If index_expr isn't NULL, then we're trying to import an expression index,
+ * and index_expr is the expression tree representing the column's data.
+ */
+VacAttrStats *
+examine_rel_attribute(Form_pg_attribute attr, Relation onerel, Node *index_expr)
+{
+	HeapTuple		typtuple;
+	int				i;
+	bool			ok;
+	VacAttrStats   *stats;
+
+	/* Never analyze dropped columns */
+	if (attr->attisdropped)
+		return NULL;
+
+	/* Don't analyze column if user has specified not to */
+	if (attr->attstattarget == 0)
+		return NULL;
+
+	/*
+	 * Create the VacAttrStats struct.
+	 */
+	stats = (VacAttrStats *) palloc0(sizeof(VacAttrStats));
+	stats->attstattarget = attr->attstattarget;
+
+	/*
+	 * When analyzing an expression index, believe the expression tree's type
+	 * not the column datatype --- the latter might be the opckeytype storage
+	 * type of the opclass, which is not interesting for our purposes.  (Note:
+	 * if we did anything with non-expression index columns, we'd need to
+	 * figure out where to get the correct type info from, but for now that's
+	 * not a problem.)	It's not clear whether anyone will care about the
+	 * typmod, but we store that too just in case.
+	 */
+	if (index_expr)
+	{
+		stats->attrtypid = exprType(index_expr);
+		stats->attrtypmod = exprTypmod(index_expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(onerel->rd_indcollation[attr->attnum - 1]))
+			stats->attrcollid = onerel->rd_indcollation[attr->attnum - 1];
+		else
+			stats->attrcollid = exprCollation(index_expr);
+	}
+	else
+	{
+		stats->attrtypid = attr->atttypid;
+		stats->attrtypmod = attr->atttypmod;
+		stats->attrcollid = attr->attcollation;
+	}
+
+	typtuple = SearchSysCacheCopy1(TYPEOID,
+								   ObjectIdGetDatum(stats->attrtypid));
+	if (!HeapTupleIsValid(typtuple))
+		elog(ERROR, "cache lookup failed for type %u", stats->attrtypid);
+	stats->attrtype = (Form_pg_type) GETSTRUCT(typtuple);
+	stats->anl_context = CurrentMemoryContext;
+	stats->tupattnum = attr->attnum;
+
+	/*
+	 * The fields describing the stats->stavalues[n] element types default to
+	 * the type of the data being analyzed, but the type-specific typanalyze
+	 * function can change them if it wants to store something else.
+	 */
+	for (i = 0; i < STATISTIC_NUM_SLOTS; i++)
+	{
+		stats->statypid[i] = stats->attrtypid;
+		stats->statyplen[i] = stats->attrtype->typlen;
+		stats->statypbyval[i] = stats->attrtype->typbyval;
+		stats->statypalign[i] = stats->attrtype->typalign;
+	}
+
+	/*
+	 * Call the type-specific typanalyze function.  If none is specified, use
+	 * std_typanalyze().
+	 */
+	if (OidIsValid(stats->attrtype->typanalyze))
+		ok = DatumGetBool(OidFunctionCall1(stats->attrtype->typanalyze,
+										   PointerGetDatum(stats)));
+	else
+		ok = std_typanalyze(stats);
+
+	if (!ok || stats->compute_stats == NULL || stats->minrows <= 0)
+	{
+		heap_freetuple(typtuple);
+		pfree(stats);
+		return NULL;
+	}
+
+	return stats;
+}
+
+/*
+ * Import statistics (pg_statistic) into a relation
+ */
+Datum
+pg_import_rel_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid;
+	int32		stats_version_num;
+	Jsonb	   *jb;
+	Relation	rel;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("relation cannot be NULL")));
+	relid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("server_version_number cannot be NULL")));
+	stats_version_num = PG_GETARG_INT32(1);
+
+	if (stats_version_num < 80000)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics version: %d is earlier than earliest supported version",
+				  stats_version_num)));
+
+	if (PG_ARGISNULL(4))
+		jb = NULL;
+	else
+	{
+		jb = PG_GETARG_JSONB_P(4);
+		if (!JB_ROOT_IS_OBJECT(jb))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("columns must be jsonb object at root")));
+	}
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	/*
+	 * Apply statistical updates, if any, to copied tuple.
+	 *
+	 * Format is:
+	 * {
+	 *   "regular": { "columns": ..., "extended": ...},
+	 *   "inherited": { "columns": ..., "extended": ...}
+	 * }
+	 *
+	 */
+	if (jb != NULL)
+	{
+		JsonbContainer	   *cont;
+
+		cont = key_lookup_object(&jb->root, "regular");
+		import_pg_statistic(rel, false, cont);
+
+		if (rel->rd_rel->relhassubclass)
+		{
+			cont = key_lookup_object(&jb->root, "inherited");
+			import_pg_statistic(rel, true, cont);
+		}
+	}
+
+	/* only modify pg_class row if changes are to be made */
+	if ( ! PG_ARGISNULL(2) || ! PG_ARGISNULL(3) )
+	{
+		Relation		pg_class_rel;
+		HeapTuple		ctup;
+		Form_pg_class	pgcform;
+
+		/*
+		 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+		 * other stat-setting operation can run on it concurrently.
+		 */
+		pg_class_rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+		/* leave if relation could not be opened or locked */
+		if (!pg_class_rel)
+			PG_RETURN_BOOL(false);
+
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		if (!HeapTupleIsValid(ctup))
+			elog(ERROR, "pg_class entry for relid %u vanished during statistics import",
+				 relid);
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+		/* leave un-set values alone */
+		if (! PG_ARGISNULL(2))
+			pgcform->reltuples = PG_GETARG_FLOAT4(2);
+		if (! PG_ARGISNULL(3))
+			pgcform->relpages = PG_GETARG_INT32(3);
+
+		heap_inplace_update(pg_class_rel, ctup);
+		table_close(pg_class_rel, ShareUpdateExclusiveLock);
+	}
+
+	/* relation_close(onerel, ShareUpdateExclusiveLock); */
+	relation_close(rel, NoLock);
+
+	PG_RETURN_BOOL(true);
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
new file mode 100644
index 0000000000..2490472198
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,137 @@
+CREATE TYPE stats_import_complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import_test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import_complex_type,
+    tags text[]
+);
+INSERT INTO stats_import_test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import_complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import_complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
+ANALYZE stats_import_test;
+-- capture snapshot of source stats
+CREATE TABLE stats_export AS
+SELECT e.*
+FROM pg_catalog.pg_statistic_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname IN ('stats_import_test', 'is_odd');
+SELECT c.reltuples AS before_tuples, c.relpages AS before_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ before_tuples | before_pages 
+---------------+--------------
+             4 |            1
+(1 row)
+
+-- test settting tuples and pages but no columns
+SELECT pg_import_rel_stats(c.oid,
+                           current_setting('server_version_num')::integer,
+                           1000.0, 200, NULL::jsonb)
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+SELECT c.reltuples AS after_tuples, c.relpages AS after_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ after_tuples | after_pages 
+--------------+-------------
+         1000 |         200
+(1 row)
+
+-- create a table just like stats_import_test
+CREATE TABLE stats_import_clone ( LIKE stats_import_test );
+-- create an index just like is_odd
+CREATE INDEX is_odd2 ON stats_import_clone(((comp).a % 2 = 0));
+-- copy table stats to clone table
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN stats_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'stats_import_test'
+WHERE c.oid = 'stats_import_clone'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+-- copy index stats to clone index
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN stats_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'is_odd'
+WHERE c.oid = 'is_odd2'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+-- table stats must match
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_test'::regclass
+EXCEPT
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_clone'::regclass;
+ staattnum | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 
+-----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----
+(0 rows)
+
+-- index stats must match
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'is_odd'::regclass
+EXCEPT
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'is_odd2'::regclass;
+ staattnum | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 
+-----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----
+(0 rows)
+
+DROP TABLE stats_export;
+DROP TABLE stats_import_clone;
+DROP TABLE stats_import_test;
+DROP TYPE stats_import_complex_type;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f0987ff537..09ffd43fc6 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 # Another group of parallel tests (JSON related)
 # ----------
-test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
+test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson stats_export_import
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..e97b9d1064
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,119 @@
+CREATE TYPE stats_import_complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import_test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import_complex_type,
+    tags text[]
+);
+
+INSERT INTO stats_import_test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import_complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import_complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+
+CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
+
+ANALYZE stats_import_test;
+
+-- capture snapshot of source stats
+CREATE TABLE stats_export AS
+SELECT e.*
+FROM pg_catalog.pg_statistic_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname IN ('stats_import_test', 'is_odd');
+
+SELECT c.reltuples AS before_tuples, c.relpages AS before_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+-- test settting tuples and pages but no columns
+SELECT pg_import_rel_stats(c.oid,
+                           current_setting('server_version_num')::integer,
+                           1000.0, 200, NULL::jsonb)
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+SELECT c.reltuples AS after_tuples, c.relpages AS after_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+-- create a table just like stats_import_test
+CREATE TABLE stats_import_clone ( LIKE stats_import_test );
+
+-- create an index just like is_odd
+CREATE INDEX is_odd2 ON stats_import_clone(((comp).a % 2 = 0));
+
+-- copy table stats to clone table
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN stats_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'stats_import_test'
+WHERE c.oid = 'stats_import_clone'::regclass;
+
+-- copy index stats to clone index
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN stats_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'is_odd'
+WHERE c.oid = 'is_odd2'::regclass;
+
+-- table stats must match
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_test'::regclass
+EXCEPT
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_clone'::regclass;
+
+-- index stats must match
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'is_odd'::regclass
+EXCEPT
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'is_odd2'::regclass;
+
+DROP TABLE stats_export;
+DROP TABLE stats_import_clone;
+DROP TABLE stats_import_test;
+DROP TYPE stats_import_complex_type;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 20da3ed033..ae3d1073e3 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28151,6 +28151,49 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+        could be used by <program>pg_upgrade</program> and
+        <program>pg_restore</program> to convey the statistics from the old system
+        version into the new one.
2.43.0

From bedf3ff9be5f26ed435f90e6db8405a5e7e32e67 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 12 Dec 2023 22:25:51 -0500
Subject: [PATCH v3 4/9] Add pg_export_stats, pg_import_stats.

pg_import_stats is used to import stats to any version that has the
function pg_import_rel_stats().
---
 src/bin/scripts/.gitignore        |   2 +
 src/bin/scripts/Makefile          |   6 +-
 src/bin/scripts/pg_export_stats.c | 298 +++++++++++++++++++++++++++++
 src/bin/scripts/pg_import_stats.c | 303 ++++++++++++++++++++++++++++++
 4 files changed, 608 insertions(+), 1 deletion(-)
 create mode 100644 src/bin/scripts/pg_export_stats.c
 create mode 100644 src/bin/scripts/pg_import_stats.c

diff --git a/src/bin/scripts/.gitignore b/src/bin/scripts/.gitignore
index 0f23fe0004..1b9addb339 100644
--- a/src/bin/scripts/.gitignore
+++ b/src/bin/scripts/.gitignore
@@ -6,5 +6,7 @@
 /reindexdb
 /vacuumdb
 /pg_isready
+/pg_export_stats
+/pg_import_stats
 
 /tmp_check/
index 20db40b103..a019894d84 100644

 
 install: all installdirs
 
 installdirs:
index 0000000000..abb3659e20
@@ -0,0 +1,298 @@
+ * Versions 10-16 have the same stats layout, but lack the view definition,
+ * so extracting the view definition ad using it as-is will work.
+	"                CASE "
+	"                    WHEN a.stainherit THEN 'inherited' "
+	"                a.stats "
+	"        FROM "
+	"                            s.stanumbers1::text, "
+	"                            s.stanumbers2::text, "
+	"                            s.stanumbers3::text, "
+	"                            s.stanumbers4::text, "
+	"                            s.stanumbers5::text), "
+	"                            s.stavalues1::text, "
+	"                            s.stavalues2::text, "
+	"                            s.stavalues3::text, "
+	"                            s.stavalues4::text, "
+	"                            s.stavalues5::text) "
+	"            GROUP BY s.stainherit "
+	"        ) AS a "
+	"WHERE relkind IN ('r', 'm', 'f', 'p', 'i') "
+	"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') ";
index 0000000000..122afc0971
2.43.0

From 059494d82745258400a145b6aa71ce958f23347a Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 9 Dec 2023 04:59:23 -0500
Subject: [PATCH v3 5/9] Add system view pg_statistic_ext_export.

This view is designed to aid in the export (and re-import) of extended
statistics, mostly for upgrade/restore situations.
---
 src/backend/catalog/system_views.sql | 131 +++++++++++++++++++++++++++
 src/test/regress/expected/rules.out  |  34 +++++++
 doc/src/sgml/system-views.sgml       |   5 +
 3 files changed, 170 insertions(+)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 7655bf7458..8dca87c061 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -356,6 +356,137 @@ CREATE VIEW pg_statistic_export WITH (security_barrier) AS
     WHERE relkind IN ('r', 'm', 'f', 'p', 'i')
     AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema');
 
+CREATE VIEW pg_statistic_ext_export WITH (security_barrier) AS
+    SELECT
+        n.nspname AS schemaname,
+        r.relname AS tablename,
+        e.stxname AS ext_stats_name,
+        (current_setting('server_version_num'::text))::integer AS server_version_num,
+        jsonb_object_agg(
+            CASE sd.stxdinherit
+                WHEN true THEN 'inherited'
+                ELSE 'regular'
+            END,
+            jsonb_build_object(
+                'stxkinds',
+                to_jsonb(e.stxkind),
+                'stxdndistinct',
+                (
+                    SELECT
+                        jsonb_agg(
+                            -- att1, [, att2 ...] => attN: degree
+                            jsonb_build_object(
+                                'attnums',
+                                string_to_array(nd.attnums, ', '::text),
+                                'ndistinct',
+                                nd.ndistinct
+                                )
+                            ORDER BY nd.ord
+                        )
+                    -- jsonb does not preserve parsed order so use json
+                    FROM json_each_text(sd.stxdndistinct::text::json)
+                        WITH ORDINALITY AS nd(attnums, ndistinct, ord)
+                    WHERE sd.stxdndistinct IS NOT NULL
+                ),
+                'stxdndependencies',
+                (
+                    SELECT
+                        jsonb_agg(
+                            jsonb_build_object(
+                                'attnums',
+                                string_to_array(
+                                    replace(dep.attrs, ' => ', ', '), ', '
+                                ),
+                                'degree',
+                                dep.degree
+                            )
+                            ORDER BY dep.ord
+                        )
+                    FROM json_each_text(sd.stxddependencies::text::json)
+                        WITH ORDINALITY AS dep(attrs, degree, ord)
+                    WHERE sd.stxddependencies IS NOT NULL
+                ),
+                'stxdmcv',
+                (
+                    SELECT
+                        jsonb_agg(
+                            jsonb_build_object(
+                                'index',
+                                mcvl.index::text,
+                                'frequency',
+                                mcvl.frequency::text,
+                                'base_frequency',
+                                mcvl.base_frequency::text,
+                                'values',
+                                mcvl.values,
+                                'nulls',
+                                mcvl.nulls
+                            )
+                        )
+                    FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl
+                    WHERE sd.stxdmcv IS NOT NULL
+                ),
+                'stxdexprs',
+                (
+                    SELECT
+                        jsonb_agg(
+                            jsonb_build_object(
+                                'stanullfrac',
+                                s.stanullfrac::text,
+                                'stawidth',
+                                s.stawidth::text,
+                                'stadistinct',
+                                s.stadistinct::text,
+                                'stakinds',
+                                (
+                                    SELECT
+                                        jsonb_agg(
+                                            CASE kind.kind
+                                                WHEN 0 THEN 'TRIVIAL'
+                                                WHEN 1 THEN 'MCV'
+                                                WHEN 2 THEN 'HISTOGRAM'
+                                                WHEN 3 THEN 'CORRELATION'
+                                                WHEN 4 THEN 'MCELEM'
+                                                WHEN 5 THEN 'DECHIST'
+                                                WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'
+                                                WHEN 7 THEN 'BOUNDS_HISTOGRAM'
+                                                ELSE NULL
+                                            END
+                                            ORDER BY kind.ord
+                                        )
+                                    FROM unnest(ARRAY[s.stakind1, s.stakind2,
+                                                      s.stakind3, s.stakind4,
+                                                      s.stakind5])
+                                        WITH ORDINALITY kind(kind, ord)
+                                ),
+                                'stanumbers',
+                                jsonb_build_array(
+                                    s.stanumbers1::text,
+                                    s.stanumbers2::text,
+                                    s.stanumbers3::text,
+                                    s.stanumbers4::text,
+                                    s.stanumbers5::text
+                                ),
+                                'stavalues',
+                                jsonb_build_array(
+                                    s.stavalues1::text,
+                                    s.stavalues2::text,
+                                    s.stavalues3::text,
+                                    s.stavalues4::text,
+                                    s.stavalues5::text)
+                                )
+                                ORDER BY s.ordinality
+                            )
+                    FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s
+                    WHERE sd.stxdexpr IS NOT NULL
+                )
+            )
+        ) AS stats
+    FROM pg_class r
+    JOIN pg_namespace n ON n.oid = r.relnamespace
+    JOIN pg_statistic_ext e ON e.stxrelid = r.oid
+    JOIN pg_statistic_ext_data sd ON sd.stxoid = e.oid
+    GROUP BY schemaname, tablename, ext_stats_name, server_version_num;

 CREATE VIEW pg_stats_ext WITH (security_barrier) AS
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f2b059af5e..66ebac6cfd 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2435,6 +2435,40 @@ pg_statistic_export| SELECT n.nspname AS schemaname,
    FROM (pg_class r
      JOIN pg_namespace n ON ((n.oid = r.relnamespace)))
   WHERE ((r.relkind = ANY (ARRAY['r'::"char", 'm'::"char", 'f'::"char", 'p'::"char", 'i'::"char"])) AND (n.nspname <> ALL (ARRAY['pg_catalog'::name, 'pg_toast'::name, 'information_schema'::name])));
+pg_statistic_ext_export| SELECT n.nspname AS schemaname,
+    r.relname AS tablename,
+    e.stxname AS ext_stats_name,
+    (current_setting('server_version_num'::text))::integer AS server_version_num,
+    jsonb_object_agg(
+        CASE sd.stxdinherit
+            WHEN true THEN 'inherited'::text
+            ELSE 'regular'::text
+        END, jsonb_build_object('stxkinds', to_jsonb(e.stxkind), 'stxdndistinct', ( SELECT jsonb_agg(jsonb_build_object('attnums', string_to_array(nd.attnums, ', '::text), 'ndistinct', nd.ndistinct) ORDER BY nd.ord) AS jsonb_agg
+           FROM json_each_text(((sd.stxdndistinct)::text)::json) WITH ORDINALITY nd(attnums, ndistinct, ord)
+          WHERE (sd.stxdndistinct IS NOT NULL)), 'stxdndependencies', ( SELECT jsonb_agg(jsonb_build_object('attnums', string_to_array(replace(dep.attrs, ' => '::text, ', '::text), ', '::text), 'degree', dep.degree) ORDER BY dep.ord) AS jsonb_agg
+           FROM json_each_text(((sd.stxddependencies)::text)::json) WITH ORDINALITY dep(attrs, degree, ord)
+          WHERE (sd.stxddependencies IS NOT NULL)), 'stxdmcv', ( SELECT jsonb_agg(jsonb_build_object('index', (mcvl.index)::text, 'frequency', (mcvl.frequency)::text, 'base_frequency', (mcvl.base_frequency)::text, 'values', mcvl."values", 'nulls', mcvl.nulls)) AS jsonb_agg
+           FROM pg_mcv_list_items(sd.stxdmcv) mcvl(index, "values", nulls, frequency, base_frequency)
+          WHERE (sd.stxdmcv IS NOT NULL)), 'stxdexprs', ( SELECT jsonb_agg(jsonb_build_object('stanullfrac', (s.stanullfrac)::text, 'stawidth', (s.stawidth)::text, 'stadistinct', (s.stadistinct)::text, 'stakinds', ( SELECT jsonb_agg(
+                        CASE kind.kind
+                            WHEN 0 THEN 'TRIVIAL'::text
+                            WHEN 1 THEN 'MCV'::text
+                            WHEN 2 THEN 'HISTOGRAM'::text
+                            WHEN 3 THEN 'CORRELATION'::text
+                            WHEN 4 THEN 'MCELEM'::text
+                            WHEN 5 THEN 'DECHIST'::text
+                            WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'::text
+                            WHEN 7 THEN 'BOUNDS_HISTOGRAM'::text
+                            ELSE NULL::text
+                        END ORDER BY kind.ord) AS jsonb_agg
+                   FROM unnest(ARRAY[s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5]) WITH ORDINALITY kind(kind, ord)), 'stanumbers', jsonb_build_array((s.stanumbers1)::text, (s.stanumbers2)::text, (s.stanumbers3)::text, (s.stanumbers4)::text, (s.stanumbers5)::text), 'stavalues', jsonb_build_array((s.stavalues1)::text, (s.stavalues2)::text, (s.stavalues3)::text, (s.stavalues4)::text, (s.stavalues5)::text)) ORDER BY s.ordinality) AS jsonb_agg
+           FROM unnest(sd.stxdexpr) WITH ORDINALITY s(starelid, staattnum, stainherit, stanullfrac, stawidth, stadistinct, stakind1, stakind2, stakind3, stakind4, stakind5, staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5, stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5, stavalues1, stavalues2, stavalues3, stavalues4, stavalues5, ordinality)
+          WHERE (sd.stxdexpr IS NOT NULL)))) AS stats
+   FROM (((pg_class r
+     JOIN pg_namespace n ON ((n.oid = r.relnamespace)))
+     JOIN pg_statistic_ext e ON ((e.stxrelid = r.oid)))
+     JOIN pg_statistic_ext_data sd ON ((sd.stxoid = e.oid)))
+  GROUP BY n.nspname, r.relname, e.stxname, (current_setting('server_version_num'::text))::integer;
 pg_stats| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     a.attname,
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 91b3ab22fb..af5dff6a74 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -196,6 +196,11 @@
       <entry>planner statistics for export/upgrade purposes</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-stats-ext"><structname>pg_stats_ext_export</structname></link></entry>
+      <entry>extended planner statistics for export/upgrade purposes</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-tables"><structname>pg_tables</structname></link></entry>
       <entry>tables</entry>
-- 
2.43.0

From 0797a698495c21baaef3b633e78425bcd7b16821 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Mon, 11 Dec 2023 03:23:28 -0500
Subject: [PATCH v3 6/9] Create create_stat_ext_entry() from
 fetch_statentries_for_relation().

Refactor fetch_statentries_for_relation() to use create_stat_ext_entry() in
its inner loop.

Later commits will make use of create_stat_ext_entry().

This was made its own commit for code clarity.
---
 src/backend/statistics/extended_stats.c | 146 +++++++++++++-----------
 1 file changed, 78 insertions(+), 68 deletions(-)

diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 7f014a0cbb..718826ecf1 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -418,6 +418,83 @@ statext_is_kind_built(HeapTuple htup, char type)
 	return !heap_attisnull(htup, attnum, NULL);
 }
 
+/*
+ * Create a single StatExtEntry from a fetched heap tuple
+ */
+static StatExtEntry *
+create_stat_ext_entry(HeapTuple htup)
+{
+	StatExtEntry *entry;
+	Datum		datum;
+	bool		isnull;
+	int			i;
+	ArrayType  *arr;
+	char	   *enabled;
+	Form_pg_statistic_ext staForm;
+	List	   *exprs = NIL;
+
+	entry = palloc0(sizeof(StatExtEntry));
+	staForm = (Form_pg_statistic_ext) GETSTRUCT(htup);
+	entry->statOid = staForm->oid;
+	entry->schema = get_namespace_name(staForm->stxnamespace);
+	entry->name = pstrdup(NameStr(staForm->stxname));
+	entry->stattarget = staForm->stxstattarget;
+	for (i = 0; i < staForm->stxkeys.dim1; i++)
+	{
+		entry->columns = bms_add_member(entry->columns,
+										staForm->stxkeys.values[i]);
+	}
+
+	/* decode the stxkind char array into a list of chars */
+	datum = SysCacheGetAttrNotNull(STATEXTOID, htup,
+								   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);
+	for (i = 0; i < ARR_DIMS(arr)[0]; i++)
+	{
+		Assert((enabled[i] == STATS_EXT_NDISTINCT) ||
+			   (enabled[i] == STATS_EXT_DEPENDENCIES) ||
+			   (enabled[i] == STATS_EXT_MCV) ||
+			   (enabled[i] == STATS_EXT_EXPRESSIONS));
+		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);
+
+		/*
+		 * 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);
+	}
+
+	entry->exprs = exprs;
+
+	return entry;
+}
+
 /*
  * Return a list (of StatExtEntry) of statistics objects for the given relation.
  */
@@ -443,74 +520,7 @@ fetch_statentries_for_relation(Relation pg_statext, Oid relid)
 
 	while (HeapTupleIsValid(htup = systable_getnext(scan)))
 	{
-		StatExtEntry *entry;
-		Datum		datum;
-		bool		isnull;
-		int			i;
-		ArrayType  *arr;
-		char	   *enabled;
-		Form_pg_statistic_ext staForm;
-		List	   *exprs = NIL;
-
-		entry = palloc0(sizeof(StatExtEntry));
-		staForm = (Form_pg_statistic_ext) GETSTRUCT(htup);
-		entry->statOid = staForm->oid;
-		entry->schema = get_namespace_name(staForm->stxnamespace);
-		entry->name = pstrdup(NameStr(staForm->stxname));
-		entry->stattarget = staForm->stxstattarget;
-		for (i = 0; i < staForm->stxkeys.dim1; i++)
-		{
-			entry->columns = bms_add_member(entry->columns,
-											staForm->stxkeys.values[i]);
-		}
-
-		/* decode the stxkind char array into a list of chars */
-		datum = SysCacheGetAttrNotNull(STATEXTOID, htup,
-									   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);
-		for (i = 0; i < ARR_DIMS(arr)[0]; i++)
-		{
-			Assert((enabled[i] == STATS_EXT_NDISTINCT) ||
-				   (enabled[i] == STATS_EXT_DEPENDENCIES) ||
-				   (enabled[i] == STATS_EXT_MCV) ||
-				   (enabled[i] == STATS_EXT_EXPRESSIONS));
-			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);
-
-			/*
-			 * 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);
-		}
-
-		entry->exprs = exprs;
-
+		StatExtEntry *entry = create_stat_ext_entry(htup);
 		result = lappend(result, entry);
 	}
 
-- 
2.43.0

From 0bfed151e366b2e0389356613713ae33d859382e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 13 Dec 2023 04:00:47 -0500
Subject: [PATCH v3 8/9] Allow explicit nulls in container lookups.

Allow key_lookup_object() and key_lookup_array() to treat explicit JSONB
jbvNull values the same as if the key were omitted entirely.

This allows export functions to create the keyed object via a query
without worrying about removing the key if there is no underlying data.

Also, having a key existing with an affirmative "there is no data here"
is more clear than the key missing.
---
 src/backend/statistics/statistics.c | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
index e54f1c9162..b1f0396d47 100644
--- a/src/backend/statistics/statistics.c
+++ b/src/backend/statistics/statistics.c
@@ -528,16 +528,19 @@ JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key)
 	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
 		return NULL;
 
+	if (j.type == jbvNull)
+		return NULL;
+
 	if (j.type != jbvBinary)
 		ereport(ERROR,
 		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-		   errmsg("invalid statistics format, %s must be an object but is type %s",
+		   errmsg("invalid statistics format, %s must be an object or null but is type %s",
 				  key, JsonbTypeName(&j))));
 
 	if (!JsonContainerIsObject(j.val.binary.data))
 		ereport(ERROR,
 		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-		   errmsg("invalid statistics format, %s must be an object but is type %s",
+		   errmsg("invalid statistics format, %s must be an object or null but is type %s",
 				  key, JsonbContainerTypeName(j.val.binary.data))));
 
 	return j.val.binary.data;
@@ -553,16 +556,19 @@ JsonbContainer *key_lookup_array(JsonbContainer *cont, const char *key)
 	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
 		return NULL;
 
+	if (j.type == jbvNull)
+		return NULL;
+
 	if (j.type != jbvBinary)
 		ereport(ERROR,
 		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-		   errmsg("invalid statistics format, %s must be an array but is type %s",
+		   errmsg("invalid statistics format, %s must be an array or null but is type %s",
 				  key, JsonbTypeName(&j))));
 
 	if (!JsonContainerIsArray(j.val.binary.data))
 		ereport(ERROR,
 		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-		   errmsg("invalid statistics format, %s must be an array but is type %s",
+		   errmsg("invalid statistics format, %s must be an array or null but is type %s",
 				  key, JsonbContainerTypeName(j.val.binary.data))));
 
 	return j.val.binary.data;
-- 
2.43.0

From 159763cbf70b11675321fb56a00e5ea1a8e4592b Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 13 Dec 2023 03:55:35 -0500
Subject: [PATCH v3 7/9] Add pg_import_ext_stats()

This is the extended statistics equivalent of pg_import_rel_stats().

The function takes a best-effort approach, skipping statistics that are
expected but omitted, skipping object that are specified but do not
exist on the target system. The goal is to get better-than-empty
statistics into the STATISTICS object quickly, so that business
operations can resume sooner. The statistics generated will replace
existing rows in pg_statistic_ext_data for the same statistics
object, and this is done in an all-or-nothing basis rather than
attempting to modify existing rows.

The medium of exchange is jsonb, the format of which is specified in the
view pg_statistic_ext_export. Obviously this view does not exist in older
versions of the database, but the view definition can be extracted and
adapted to older versions.

This function also allows for tweaking of table statistics in-place,
allowing the user to simulate correlations, skew histograms, etc, to see
what those changes will evoke from the query planner.
---
 src/include/catalog/pg_proc.dat               |   5 +
 .../statistics/extended_stats_internal.h      |   8 +-
 src/backend/statistics/dependencies.c         | 111 +++++++
 src/backend/statistics/extended_stats.c       | 291 ++++++++++++++++++
 src/backend/statistics/mcv.c                  | 217 ++++++++++++-
 src/backend/statistics/mvdistinct.c           | 101 ++++++
 src/backend/statistics/statistics.c           |   8 +-
 .../regress/expected/stats_export_import.out  |  20 ++
 src/test/regress/sql/stats_export_import.sql  |  18 ++
 doc/src/sgml/func.sgml                        |  21 ++
 10 files changed, 794 insertions(+), 6 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4d1e9bde1f..bd674e232e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5660,6 +5660,11 @@
   proname => 'pg_import_rel_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'bool', proargtypes => 'oid int4 float4 int4 jsonb',
   prosrc => 'pg_import_rel_stats' },
+{ oid => '9162',
+  descr => 'statistics: import to extended stats object',
+  proname => 'pg_import_ext_stats', provolatile => 'v', proisstrict => 't',
+  proparallel => 'u', prorettype => 'bool', proargtypes => 'oid int4 jsonb',
+  prosrc => 'pg_import_ext_stats' },
 { oid => '3150', descr => 'statistics: number of temporary files written',
   proname => 'pg_stat_get_db_temp_files', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index 7b55eb8ffa..5fb4523525 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -68,17 +68,21 @@ typedef struct StatsBuildData
 	bool	  **nulls;
 } StatsBuildData;
 
-
 extern MVNDistinct *statext_ndistinct_build(double totalrows, StatsBuildData *data);
+extern MVNDistinct *statext_ndistinct_import(JsonbContainer *cont);
 extern bytea *statext_ndistinct_serialize(MVNDistinct *ndistinct);
 extern MVNDistinct *statext_ndistinct_deserialize(bytea *data);
 
 extern MVDependencies *statext_dependencies_build(StatsBuildData *data);
+extern MVDependencies *statext_dependencies_import(JsonbContainer *cont);
 extern bytea *statext_dependencies_serialize(MVDependencies *dependencies);
 extern MVDependencies *statext_dependencies_deserialize(bytea *data);
+extern bytea *import_dependencies(JsonbContainer *cont);
 
 extern MCVList *statext_mcv_build(StatsBuildData *data,
 								  double totalrows, int stattarget);
+extern MCVList *statext_mcv_import(JsonbContainer *cont,
+									VacAttrStats **stats, int natts);
 extern bytea *statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats);
 extern MCVList *statext_mcv_deserialize(bytea *data);
 
@@ -127,4 +131,6 @@ extern Selectivity mcv_clause_selectivity_or(PlannerInfo *root,
 											 Selectivity *overlap_basesel,
 											 Selectivity *totalsel);
 
+extern Datum pg_import_ext_stats(PG_FUNCTION_ARGS);
+
 #endif							/* EXTENDED_STATS_INTERNAL_H */
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index edb2e5347d..ca8b20adab 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -27,7 +27,9 @@
 #include "parser/parsetree.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -1829,3 +1831,112 @@ dependencies_clauselist_selectivity(PlannerInfo *root,
 
 	return s1;
 }
+
+/*
+ * statext_dependencies_import
+ *
+ * Like statext_dependencies_build, but import the data
+ * from a JSON object.
+ *
+ * import format:
+ * [
+ *   {
+ *     "attnums": [ intstr, ... ],
+ *     "degree": floatstr
+ *   }
+ * ]
+ *
+ */
+MVDependencies *
+statext_dependencies_import(JsonbContainer *cont)
+{
+	MVDependencies *dependencies = NULL;
+	int				ndeps;
+	int				i;
+
+
+	if (cont == NULL)
+		ndeps = 0;
+	else
+		ndeps = JsonContainerSize(cont);
+
+	if (ndeps == 0)
+		dependencies = (MVDependencies *) palloc0(sizeof(MVDependencies));
+	else
+		dependencies = (MVDependencies *) palloc0(offsetof(MVDependencies, deps)
+												   + (ndeps * sizeof(MVDependency *)));
+
+	dependencies->magic = STATS_DEPS_MAGIC;
+	dependencies->type = STATS_DEPS_TYPE_BASIC;
+	dependencies->ndeps = ndeps;
+
+	/* compute length of output */
+	for (i = 0; i < ndeps; i++)
+	{
+		JsonbValue	   *j;
+		JsonbContainer *elemobj,
+					   *attnumarr;
+		MVDependency   *d;
+		char		   *s;
+		int				a;
+		int				natts;
+
+		j = getIthJsonbValueFromContainer(cont, i);
+
+		if ((j == NULL)
+				|| (j->type != jbvBinary)
+				|| (!JsonContainerIsObject(j->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdepndencies "
+					  "must be objects.")));
+
+		elemobj = j->val.binary.data;
+		attnumarr = key_lookup_array(elemobj, "attnums");
+
+		if (attnumarr == NULL)
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdependencies "
+					  "must contain an element called attnums which is an array.")));
+
+		natts = JsonContainerSize(attnumarr);
+		d = (MVDependency *) palloc0(offsetof(MVDependency, attributes)
+									 + (natts * sizeof(AttrNumber)));
+		dependencies->deps[i] = d;
+
+		d->nattributes = natts;
+
+		s = key_lookup_cstring(elemobj, "degree");
+		if (s != NULL)
+		{
+			d->degree = float8in_internal(s, NULL, "double", s, NULL);
+			pfree(s);
+		}
+		else
+			d->degree = 0;
+
+		for (a = 0; a < natts; a++)
+		{
+			JsonbValue *aj;
+			char	   *str;
+
+			aj = getIthJsonbValueFromContainer(attnumarr, a);
+
+			if ((aj == NULL) || (aj->type != jbvString))
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, elements of attnums "
+						  "must be string representations of integers.")));
+
+			str = JsonbStringValueToCString(aj);
+			d->attributes[a] = pg_strtoint16(str);
+			pfree(str);
+			pfree(aj);
+		}
+
+		pfree(j);
+	}
+
+	return dependencies;
+}
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 718826ecf1..69072485fa 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -19,6 +19,7 @@
 #include "access/detoast.h"
 #include "access/genam.h"
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "access/table.h"
 #include "catalog/indexing.h"
 #include "catalog/pg_collation.h"
@@ -495,6 +496,38 @@ create_stat_ext_entry(HeapTuple htup)
 	return entry;
 }
 
+/*
+ * Return a list (of StatExtEntry) of statistics objects for the given relation.
+ */
+/* TODO needed????
+static StatExtEntry *
+fetch_statentry(Relation pg_statext, Oid stxid)
+{
+	SysScanDesc scan;
+	ScanKeyData skey;
+	HeapTuple	htup;
+	StatExtEntry *entry = NULL;
+
+	/-*
+	 * Prepare to scan pg_statistic_ext for entries having given oid.
+	 *-/
+	ScanKeyInit(&skey,
+				Anum_pg_statistic_ext_oid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(stxid));
+
+	scan = systable_beginscan(pg_statext, StatisticExtRelidIndexId, true,
+							  NULL, 1, &skey);
+
+	if (HeapTupleIsValid(htup = systable_getnext(scan)))
+		entry = create_stat_ext_entry(htup);
+
+	systable_endscan(scan);
+
+	return entry;
+}
+*/
+
 /*
  * Return a list (of StatExtEntry) of statistics objects for the given relation.
  */
@@ -2421,6 +2454,8 @@ serialize_expr_stats(AnlExprData *exprdata, int nexprs)
 								  false,
 								  typOid,
 								  CurrentMemoryContext);
+
+		heap_freetuple(stup);
 	}
 
 	table_close(sd, RowExclusiveLock);
@@ -2646,3 +2681,259 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+/*
+ * Generate VacAttrStats for a single pg_statistic_ext
+ */
+static VacAttrStats **
+examine_ext_stat_types(StatExtEntry *stxentry, Relation rel)
+{
+	TupleDesc		tupdesc = RelationGetDescr(rel);
+	Bitmapset	   *columns = stxentry->columns;
+	List		   *exprs = stxentry->exprs;
+	int				natts = bms_num_members(columns) + list_length(exprs);
+	int				i = 0;
+	int				m = -1;
+
+	VacAttrStats  **stats;
+	ListCell   *lc;
+
+	stats = (VacAttrStats **) palloc(natts * sizeof(VacAttrStats *));
+
+	/* lookup VacAttrStats info for the requested columns (same attnum) */
+	while ((m = bms_next_member(columns, m)) >= 0)
+	{
+		Form_pg_attribute	attform = TupleDescAttr(tupdesc, m - 1);
+
+		stats[i] = examine_rel_attribute(attform, rel, NULL);
+
+		/* ext expr stats remove the tupattnum */
+		stats[i]->tupattnum = InvalidAttrNumber;
+		i++;
+	}
+
+	/* also add info for expressions */
+	foreach(lc, exprs)
+	{
+		Node	   *expr = (Node *) lfirst(lc);
+
+		stats[i] = examine_attribute(expr);
+		i++;
+	}
+
+	return stats;
+}
+
+/*
+ * Generate expressions from imported data.
+ */
+static Datum
+import_expressions(Relation rel, JsonbContainer *cont,
+					VacAttrStats **expr_stats, int nexprs)
+{
+	int			i;
+	int			nelems;
+	Oid			typOid;
+	Relation	sd;
+
+	ArrayBuildState *astate = NULL;
+
+	/* skip if no stats to import */
+	if (cont == NULL)
+		return (Datum) 0;
+
+	nelems = JsonContainerSize(cont);
+
+	if (nelems == 0)
+		return (Datum) 0;
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+
+	/* lookup OID of composite type for pg_statistic */
+	typOid = get_rel_type_id(StatisticRelationId);
+	if (!OidIsValid(typOid))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("relation \"%s\" does not have a composite type",
+						"pg_statistic")));
+
+	/*
+	 * The number of elements should not exceed the number of columns in the
+	 * extended statistics object. The elements should follow the same order
+	 * as they do on disk: regular attributes first, followed by expressions.
+	 * TODO make this a warning
+	 */
+	if (nelems > nexprs)
+	{
+		nelems = nexprs;
+	}
+	nelems = Min(nelems, nexprs);
+
+	for (i = 0; i < nelems; i++)
+	{
+		Datum		values[Natts_pg_statistic] = { 0 };
+		bool		nulls[Natts_pg_statistic] = { false };
+		bool		replaces[Natts_pg_statistic] = { false };
+		HeapTuple	stup;
+
+		JsonbValue   *j = getIthJsonbValueFromContainer(cont, i);
+		VacAttrStats *stat = expr_stats[i];
+
+		JsonbContainer *exprobj;
+
+		if ((j == NULL)
+				|| (j->type != jbvBinary)
+				|| (!JsonContainerIsObject(j->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdexprs "
+					  "must be objects.")));
+
+		exprobj = j->val.binary.data;
+
+		import_attribute(InvalidOid, stat, exprobj, false, values, nulls, replaces);
+
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+
+		astate = accumArrayResult(astate,
+								  heap_copy_tuple_as_datum(stup, RelationGetDescr(sd)),
+								  false,
+								  typOid,
+								  CurrentMemoryContext);
+		pfree(j);
+	}
+
+	table_close(sd, RowExclusiveLock);
+
+	return makeArrayResult(astate, CurrentMemoryContext);
+}
+
+/*
+ * import_pg_ext_stats
+ *
+ * Import stats for one aspect (inherited / regular) of an Extended Statistics
+ * object.
+ *
+ * The JSON container should look like this:
+ * {
+ *     "stxkinds": array of single characters (up to 3?),
+ *     "stxdndistinct": [ {ndistinct}, ... ],
+ *     "stxdndependencies": [ {dependency}, ... ]
+ *     "stxdmcv": [ {mcv}, ... ]
+ *     "stxdexprs" : [ {pg_statistic}, ... ]
+ * }
+ */
+static void
+import_pg_statistic_ext_data(StatExtEntry *stxentry, Relation rel,
+							 bool inh, JsonbContainer *cont,
+							 VacAttrStats **stats)
+{
+	int				ncols;
+	int				nexprs;
+	int				natts;
+	VacAttrStats  **expr_stats;
+
+	JsonbContainer *arraycont;
+	MCVList		   *mcvlist;
+	MVDependencies *dependencies;
+	MVNDistinct	   *ndistinct;
+	Datum			exprs;
+
+	/* skip if no stats to import */
+	if (cont == NULL)
+		return;
+
+	ncols = bms_num_members(stxentry->columns);
+	nexprs = list_length(stxentry->exprs);
+	natts = ncols + nexprs;
+	expr_stats = &stats[ncols];
+
+	arraycont = key_lookup_array(cont, "stxdndistinct");
+	ndistinct = statext_ndistinct_import(arraycont);
+
+	arraycont = key_lookup_array(cont, "stxdndependencies");
+	dependencies = statext_dependencies_import(arraycont);
+
+	arraycont = key_lookup_array(cont, "stxdmcv");
+	mcvlist = statext_mcv_import(arraycont, stats, natts);
+
+	arraycont = key_lookup_array(cont, "stxdexprs");
+	exprs = import_expressions(rel, arraycont, expr_stats, nexprs);
+
+	statext_store(stxentry->statOid, inh, ndistinct, dependencies, mcvlist,
+				  exprs, stats);
+}
+
+/*
+ * Import JSON-serialized stats to an Extended Statistics object.
+ */
+Datum
+pg_import_ext_stats(PG_FUNCTION_ARGS)
+{
+	Oid				stxid;
+	int32			stats_version_num;
+	Jsonb		   *jb;
+	JsonbContainer *cont;
+	Relation		rel;
+	HeapTuple		etup;
+	Relation		sd;
+	StatExtEntry   *stxentry;
+	VacAttrStats  **stats;
+
+	Form_pg_statistic_ext stxform;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("extended statistics oid cannot be NULL")));
+	stxid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("server_version_number cannot be NULL")));
+	stats_version_num = PG_GETARG_INT32(1);
+
+	if (stats_version_num < 100000)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics version: %d is earlier than earliest supported version",
+				  stats_version_num)));
+
+	if (PG_ARGISNULL(2))
+		PG_RETURN_BOOL(false);
+
+	jb = PG_GETARG_JSONB_P(2);
+	if (!JB_ROOT_IS_OBJECT(jb))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("extended_stats must be jsonb object at root")));
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	etup = SearchSysCacheCopy1(STATEXTOID, ObjectIdGetDatum(stxid));
+	if (!HeapTupleIsValid(etup))
+		elog(ERROR, "pg_statistic_ext entry for oid %u vanished during statistics import",
+			 stxid);
+
+	stxform = (Form_pg_statistic_ext) GETSTRUCT(etup);
+
+	rel = relation_open(stxform->stxrelid, ShareUpdateExclusiveLock);
+
+	stxentry = create_stat_ext_entry(etup);
+
+	stats = examine_ext_stat_types(stxentry, rel);
+
+	cont = key_lookup_object(&jb->root, "regular");
+	import_pg_statistic_ext_data(stxentry, rel, false, cont, stats);
+
+	if (rel->rd_rel->relhassubclass)
+	{
+		cont = key_lookup_object(&jb->root, "inherited");
+		import_pg_statistic_ext_data(stxentry, rel, true, cont, stats);
+	}
+
+	relation_close(rel, NoLock);
+	table_close(sd, RowExclusiveLock);
+
+	PG_RETURN_BOOL(true);
+}
diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c
index 03b9f04bb5..f1de17847a 100644
--- a/src/backend/statistics/mcv.c
+++ b/src/backend/statistics/mcv.c
@@ -29,6 +29,7 @@
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -679,7 +680,6 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats)
 			/* skip NULL values - we don't need to deduplicate those */
 			if (mcvlist->items[i].isnull[dim])
 				continue;
-
 			/* append the value at the end */
 			values[dim][counts[dim]] = mcvlist->items[i].values[dim];
 			counts[dim] += 1;
@@ -2177,3 +2177,218 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat,
 
 	return s;
 }
+
+/*
+ * statext_mcv_import
+ *      like statext_mcv_build, but import from JSON.
+ *
+ * import format:
+ * [
+ *   {
+ *     "index": intstr,
+ *     "values": '{va1ue, ...}',
+ *     "nulls": '{t,f,...}',
+ *     "frequency": floatstr,
+ *     "base_frequency": floatstr
+ *   }
+ * ]
+ *
+ */
+MCVList *
+statext_mcv_import(JsonbContainer *cont, VacAttrStats **stats, int ndims)
+{
+	int			nitems;
+	int			i;
+	MCVList	   *mcvlist;
+	Oid		    ioparams[STATS_MAX_DIMENSIONS];
+	FmgrInfo	finfos[STATS_MAX_DIMENSIONS];
+
+	if (cont != NULL)
+		nitems = JsonContainerSize(cont);
+	else
+		nitems = 0;
+
+	mcvlist = (MCVList *) palloc0(offsetof(MCVList, items) +
+								  (sizeof(MCVItem) * nitems));
+
+	mcvlist->magic = STATS_MCV_MAGIC;
+	mcvlist->type = STATS_MCV_TYPE_BASIC;
+	mcvlist->nitems = nitems;
+	mcvlist->ndimensions = ndims;
+
+	/* We will need these input functions $nitems times. */
+	for (i = 0; i < ndims; i++)
+	{
+		Oid		typid = stats[i]->attrtypid;
+		Oid		infunc;
+
+		mcvlist->types[i] = typid;
+		getTypeInputInfo(typid, &infunc, &ioparams[i]);
+		fmgr_info(infunc, &finfos[i]);
+	}
+
+	for (i = 0; i < nitems; i++)
+	{
+		JsonbValue	   *j;
+		JsonbContainer *itemobj,
+					   *valuesarr,
+					   *nullsarr;
+		int				numvalues,
+						numnulls;
+		int				k;
+		MCVItem		   *item = &mcvlist->items[i];
+		char		   *s;
+
+		item->values = (Datum *) palloc0(sizeof(Datum) * ndims);
+		item->isnull = (bool *) palloc0(sizeof(bool) * ndims);
+
+		j = getIthJsonbValueFromContainer(cont, i);
+
+		if ((j == NULL)
+				|| (j->type != jbvBinary)
+				|| (!JsonContainerIsObject(j->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdmcv "
+					  "must be objects.")));
+
+		itemobj = j->val.binary.data;
+
+		s = key_lookup_cstring(itemobj, "frequency");
+		if (s != NULL)
+		{
+			item->frequency = float8in_internal(s, NULL, "double", s, NULL);
+			pfree(s);
+		}
+		else
+			item->frequency = 0.0;
+
+		s = key_lookup_cstring(itemobj, "base_frequency");
+		if (s != NULL)
+		{
+			item->base_frequency = float8in_internal(s, NULL, "double", s, NULL);
+			pfree(s);
+		}
+		else
+			item->base_frequency = 0.0;
+
+		/*
+		 * Import the nulls array first, because that tells us which elements
+		 * of the values array we can skip.
+		 */
+		nullsarr = key_lookup_array(itemobj, "nulls");
+
+		if (nullsarr == NULL)
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdmcv "
+					  "must contain an element called nulls which is an array.")));
+
+		numnulls = JsonContainerSize(nullsarr);
+
+		/* having more nulls than dimensions is concerning. */
+		if (numnulls > ndims)
+		{
+			ereport(WARNING,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("statistics import has %d mcv dimensions, "
+					  "but the expects %d. Skipping excess dimensions.",
+						numnulls, ndims)));
+			numnulls = ndims;
+		}
+
+		for (k = 0; k < numnulls; k++)
+		{
+			JsonbValue  *nj = getIthJsonbValueFromContainer(nullsarr, k);
+
+			if ((nj == NULL) || (nj->type != jbvBool))
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, elements of nulls "
+						  "must be boolens.")));
+
+			item->isnull[k] = nj->val.boolean;
+
+			pfree(nj);
+		}
+
+		/* Any remaining slots are marked null */
+		for (k = numnulls; k < ndims; k++)
+			item->isnull[k] = true;
+
+		valuesarr = key_lookup_array(itemobj, "values");
+
+		if (valuesarr == NULL)
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdmcv "
+					  "must contain an element called values which is an array.")));
+
+		numvalues = JsonContainerSize(valuesarr);
+
+		/* having more values than dimensions is concerning. */
+		if (numvalues > ndims)
+		{
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("statistics import has %d mcv dimensions, "
+					  "but the expects %d. Skipping excess dimensions.",
+						numvalues, ndims)));
+			numvalues = ndims;
+		}
+
+		for (k = 0; k < numvalues; k++)
+		{
+			JsonbValue *vj;
+			bool		import_error = true;
+
+			/* if the element was null flagged, don't bother */
+			if (item->isnull[k])
+			{
+				item->values[k] = (Datum) 0;
+				continue;
+			}
+
+			vj = getIthJsonbValueFromContainer(valuesarr, k);
+
+			if (vj != NULL)
+			{
+				if (vj->type == jbvString)
+				{
+					char   *str = JsonbStringValueToCString(vj);
+
+					item->values[k] = InputFunctionCall(&finfos[k],
+														str,
+														ioparams[k],
+														stats[k]->attrtypmod);
+
+					import_error = false;
+					pfree(str);
+				}
+				else if (vj->type == jbvNull)
+				{
+					item->values[k] = (Datum) 0;
+					item->isnull[k] = true; /* mark just in case */
+					import_error = false;
+				}
+				pfree(vj);
+			}
+
+			if (import_error)
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, elements of values "
+						  "must be strings or null.")));
+
+		}
+
+		/* Any remaining slots are marked null */
+		for (k = numvalues; k < ndims; k++)
+		{
+			item->values[k] = (Datum) 0;
+			item->isnull[k] = true;
+		}
+	}
+
+	return mcvlist;
+}
diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 6d25c14644..e870ae02a4 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -31,6 +31,8 @@
 #include "lib/stringinfo.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
@@ -698,3 +700,102 @@ generate_combinations(CombinationGenerator *state)
 
 	pfree(current);
 }
+
+
+/*
+ * statext_ndistinct_import
+ *
+ * Like statext_ndistinct_build, but import the data
+ * from a JSON container.
+ *
+ * import format:
+ * [
+ *   {
+ *     "attnums": [ intstr, ... ],
+ *     "ndistinct": floatstr
+ *   }
+ * ]
+ *
+ */
+MVNDistinct *
+statext_ndistinct_import(JsonbContainer *cont)
+{
+	MVNDistinct	   *result;
+	int				nitems;
+	int				i;
+
+	if (cont == NULL)
+		return NULL;
+
+	nitems = JsonContainerSize(cont);
+
+	if (nitems == 0)
+		return NULL;
+
+	result = palloc(offsetof(MVNDistinct, items) +
+					(nitems * sizeof(MVNDistinctItem)));
+	result->magic = STATS_NDISTINCT_MAGIC;
+	result->type = STATS_NDISTINCT_TYPE_BASIC;
+	result->nitems = nitems;
+
+	for (i = 0; i < nitems; i++)
+	{
+		JsonbValue	   *j;
+		JsonbContainer *elemobj,
+					   *attnumarr;
+		int				a;
+		int				natts;
+		char		   *s;
+
+		MVNDistinctItem *item = &result->items[i];
+
+		j = getIthJsonbValueFromContainer(cont, i);
+
+		if ((j == NULL)
+				|| (j->type != jbvBinary)
+				|| (!JsonContainerIsObject(j->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdndistinct "
+					  "must be objects.")));
+
+		elemobj = j->val.binary.data;
+
+		s = key_lookup_cstring(elemobj, "ndistinct");
+		item->ndistinct = float8in_internal(s, NULL, "double", s, NULL);
+		pfree(s);
+
+		attnumarr = key_lookup_array(elemobj, "attnums");
+
+		if (attnumarr == NULL)
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdndistinct "
+					  "must contain an element called attnums which is an array.")));
+
+		natts = JsonContainerSize(attnumarr);
+		item->nattributes = natts;
+		item->attributes = palloc(sizeof(AttrNumber) * natts);
+
+		for (a = 0; a < natts; a++)
+		{
+			JsonbValue *aj;
+
+			aj = getIthJsonbValueFromContainer(attnumarr, a);
+
+			if ((aj == NULL) || (aj->type != jbvString))
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, elements of attnums "
+						  "must be string representations of integers.")));
+			s = JsonbStringValueToCString(aj);
+			item->attributes[a] = pg_strtoint16(s);
+			pfree(s);
+			pfree(aj);
+		}
+
+		pfree(j);
+	}
+
+	return result;
+}
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
index 968ccfaaf2..e54f1c9162 100644
--- a/src/backend/statistics/statistics.c
+++ b/src/backend/statistics/statistics.c
@@ -364,7 +364,7 @@ void import_stanumbers(const VacAttrStats *stat, JsonbContainer *cont,
 		if (numnumbers > STATISTIC_NUM_SLOTS)
 			ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("invalid format: number of stanumbers %d is greater than available slots %d",
+				errmsg("invalid format: number of stanumbers arrays (%d) is greater than available slots %d",
 						numnumbers, STATISTIC_NUM_SLOTS)));
 
 		fmgr_info(F_ARRAY_IN, &finfo);
@@ -474,8 +474,8 @@ void import_stavalues(const VacAttrStats *stat, JsonbContainer *cont,
 						typoid = typentry->typelem;
 				}
 				valvalues[k] = FunctionCall3(&finfo, CStringGetDatum(s),
-								 ObjectIdGetDatum(typoid),
-								 Int32GetDatum(typmod));
+											 ObjectIdGetDatum(typoid),
+											 Int32GetDatum(typmod));
 				valreplaces[k] = true;
 				pfree(s);
 				pfree(j);
@@ -664,7 +664,7 @@ examine_rel_attribute(Form_pg_attribute attr, Relation onerel, Node *index_expr)
 	if (!HeapTupleIsValid(typtuple))
 		elog(ERROR, "cache lookup failed for type %u", stats->attrtypid);
 	stats->attrtype = (Form_pg_type) GETSTRUCT(typtuple);
-	stats->anl_context = CurrentMemoryContext;
+	stats->anl_context = NULL; /*DEBUG CurrentMemoryContext; */
 	stats->tupattnum = attr->attnum;
 
 	/*
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
index 2490472198..7c14f292e4 100644
--- a/src/test/regress/expected/stats_export_import.out
+++ b/src/test/regress/expected/stats_export_import.out
@@ -19,6 +19,7 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, NULL;
 CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
+CREATE STATISTICS evens_test ON name, ((comp).a % 2 = 0) FROM stats_import_test;
 ANALYZE stats_import_test;
 -- capture snapshot of source stats
 CREATE TABLE stats_export AS
@@ -57,6 +58,8 @@ WHERE oid = 'stats_import_test'::regclass;
 CREATE TABLE stats_import_clone ( LIKE stats_import_test );
 -- create an index just like is_odd
 CREATE INDEX is_odd2 ON stats_import_clone(((comp).a % 2 = 0));
+-- create a statistics object like evens_test
+CREATE STATISTICS evens_clone ON name, ((comp).a % 2 = 0) FROM stats_import_clone;
 -- copy table stats to clone table
 SELECT pg_import_rel_stats(c.oid, e.server_version_num,
                             e.n_tuples, e.n_pages, e.stats)
@@ -131,6 +134,23 @@ WHERE s.starelid = 'is_odd2'::regclass;
 -----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----
 (0 rows)
 
+-- copy extended stats to clone table
+SELECT pg_import_ext_stats(
+        (
+            SELECT e.oid as ext_clone_oid
+            FROM pg_statistic_ext AS e
+            WHERE e.stxname = 'evens_clone'
+        ),
+        e.server_version_num, e.stats)
+FROM pg_catalog.pg_statistic_ext_export AS e
+WHERE e.schemaname = 'public'
+AND e.tablename = 'stats_import_test'
+AND e.ext_stats_name = 'evens_test';
+ pg_import_ext_stats 
+---------------------
+ t
+(1 row)
+
 DROP TABLE stats_export;
 DROP TABLE stats_import_clone;
 DROP TABLE stats_import_test;
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
index e97b9d1064..3bc8e3d3a3 100644
--- a/src/test/regress/sql/stats_export_import.sql
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -23,6 +23,8 @@ SELECT 4, 'four', NULL, NULL;
 
 CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
 
+CREATE STATISTICS evens_test ON name, ((comp).a % 2 = 0) FROM stats_import_test;
+
 ANALYZE stats_import_test;
 
 -- capture snapshot of source stats
@@ -53,6 +55,9 @@ CREATE TABLE stats_import_clone ( LIKE stats_import_test );
 -- create an index just like is_odd
 CREATE INDEX is_odd2 ON stats_import_clone(((comp).a % 2 = 0));
 
+-- create a statistics object like evens_test
+CREATE STATISTICS evens_clone ON name, ((comp).a % 2 = 0) FROM stats_import_clone;
+
 -- copy table stats to clone table
 SELECT pg_import_rel_stats(c.oid, e.server_version_num,
                             e.n_tuples, e.n_pages, e.stats)
@@ -113,6 +118,19 @@ SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
 FROM pg_statistic AS s
 WHERE s.starelid = 'is_odd2'::regclass;
 
+-- copy extended stats to clone table
+SELECT pg_import_ext_stats(
+        (
+            SELECT e.oid as ext_clone_oid
+            FROM pg_statistic_ext AS e
+            WHERE e.stxname = 'evens_clone'
+        ),
+        e.server_version_num, e.stats)
+FROM pg_catalog.pg_statistic_ext_export AS e
+WHERE e.schemaname = 'public'
+AND e.tablename = 'stats_import_test'
+AND e.ext_stats_name = 'evens_test';
+
 DROP TABLE stats_export;
 DROP TABLE stats_import_clone;
 DROP TABLE stats_import_test;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index ae3d1073e3..d9029fd29d 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28191,6 +28191,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
      </tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_import_ext_stats</primary>
+        </indexterm>
+        <function>pg_import_ext_stats</function> ( <parameter>extended stats object</parameter> <type>oid</type>, <parameter>server_version_num</parameter> <type>integer</type>, <parameter>extended_stats</parameter> <type>jsonb</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Modifies the <structname>pg_statistic_ext_data</structname> rows for the
+        <structfield>oid</structfield> matching
+        <parameter>extended statistics object</parameter> are transactionally
+        replaced with the values found in <parameter>extended_stats</parameter>.
+        The purpose of this function is to apply statistics values in an upgrade
+        situation that are "good enough" for system operation until they are
+        replaced by the next auto-analyze. This function could be used by
+        <program>pg_upgrade</program> and <program>pg_restore</program> to
+        convey the statistics from the old system version into the new one.
+       </para></entry>
+      </row>
+     </tbody>
     </tgroup>
    </table>
 
-- 
2.43.0

From aeffb52e6c744f624ea32b3ef587f5e9d0b5b85b Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 13 Dec 2023 05:21:00 -0500
Subject: [PATCH v3 9/9] Enable pg_export_stats, pg_import_stats to use
 extended statistics.

Both programs still use a single data stream. As a result, the columns
COPY-ed are a superset of both export tables, and on import the data
must first be loaded into a superset temporary table before being
separated into rel-specific and extended-specific tables.

While relation stats format is stable back to v10, the formats of
pg_statistic_ext and pg_statistic_ext_data change every version or two.
Extraction queries are provided back to v10, but currently only tested
back to v15. That's probably ok given that these programs primarily
serve as a reference and their functionality will most likely be moved
to pg_upgrade and pg_dump+pg_restore.
---
 src/bin/scripts/pg_export_stats.c | 448 +++++++++++++++++++++++++++++-
 src/bin/scripts/pg_import_stats.c | 289 +++++++++++++++----
 2 files changed, 672 insertions(+), 65 deletions(-)

diff --git a/src/bin/scripts/pg_export_stats.c b/src/bin/scripts/pg_export_stats.c
index abb3659e20..4aaafc729e 100644
--- a/src/bin/scripts/pg_export_stats.c
+++ b/src/bin/scripts/pg_export_stats.c
@@ -22,18 +22,20 @@
 static void help(const char *progname);
 
 /* view definition introduced in 17 */
-const char *export_query_v17 =
-	"SELECT schemaname, relname, server_version_num, n_tuples, "
-	"n_pages, stats FROM pg_statistic_export ";
+const char *export_rel_query_v17 =
+	"SELECT schemaname, relname, NULL::text AS ext_stats_name, "
+	"server_version_num, n_tuples, n_pages, stats "
+	"FROM pg_statistic_export ";
 
 /*
- * Versions 10-16 have the same stats layout, but lack the view definition,
- * so extracting the view definition ad using it as-is will work.
+ * Versions 10-16 have the same rel stats layout, but lack the view
+ * definition, so extracting the view definition ad using it as-is will work.
  */
-const char *export_query_v10 =
+const char *export_rel_query_v10 =
 	"SELECT "
 	"    n.nspname AS schemaname, "
 	"    r.relname AS relname, "
+	"    NULL::text AS ext_stats_name, "
 	"    current_setting('server_version_num')::integer AS server_version_num, "
 	"    r.reltuples::float4 AS n_tuples, "
 	"    r.relpages::integer AS n_pages, "
@@ -109,6 +111,412 @@ const char *export_query_v10 =
 	"WHERE relkind IN ('r', 'm', 'f', 'p', 'i') "
 	"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') ";
 
+/* view definition introduced in 17 */
+const char *export_ext_query_v17 =
+	"SELECT schemaname, relname, ext_stats_name, server_version_num, "
+	"NULL::float4 AS n_tuples, NULL::integer AS n_pages, stats "
+	"FROM pg_statistic_ext_export ";
+
+/* v15-v16 have the same extended stats layout, but lack the view definition */
+const char *export_ext_query_v15 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS tablename, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    NULL::float4 AS n_tuples, "
+	"    NULL::integer AS n_pages, "
+	"    jsonb_object_agg( "
+	"        CASE sd.stxdinherit "
+	"            WHEN true THEN 'inherited' "
+	"            ELSE 'regular' "
+	"        END, "
+	"        jsonb_build_object( "
+	"            'stxkinds', "
+	"            to_jsonb(e.stxkind), "
+	"            'stxdndistinct', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array(nd.attnums, ', '::text), "
+	"                            'ndistinct', "
+	"                            nd.ndistinct "
+	"                            ) "
+	"                        ORDER BY nd.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                    WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                WHERE sd.stxdndistinct IS NOT NULL "
+	"            ), "
+	"            'stxdndependencies', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array( "
+	"                                replace(dep.attrs, ' => ', ', '), ', ' "
+	"                            ), "
+	"                            'degree', "
+	"                            dep.degree "
+	"                        ) "
+	"                        ORDER BY dep.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxddependencies::text::json) "
+	"                    WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                WHERE sd.stxddependencies IS NOT NULL "
+	"            ), "
+	"            'stxdmcv', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'index', "
+	"                            mcvl.index::text, "
+	"                            'frequency', "
+	"                            mcvl.frequency::text, "
+	"                            'base_frequency', "
+	"                            mcvl.base_frequency::text, "
+	"                            'values', "
+	"                            mcvl.values, "
+	"                            'nulls', "
+	"                            mcvl.nulls "
+	"                        ) "
+	"                    ) "
+	"                FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl "
+	"                WHERE sd.stxdmcv IS NOT NULL "
+	"            ), "
+	"            'stxdexprs', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'stanullfrac', "
+	"                            s.stanullfrac::text, "
+	"                            'stawidth', "
+	"                            s.stawidth::text, "
+	"                            'stadistinct', "
+	"                            s.stadistinct::text, "
+	"                            'stakinds', "
+	"                            ( "
+	"                                SELECT "
+	"                                    jsonb_agg( "
+	"                                        CASE kind.kind "
+	"                                            WHEN 0 THEN 'TRIVIAL' "
+	"                                            WHEN 1 THEN 'MCV' "
+	"                                            WHEN 2 THEN 'HISTOGRAM' "
+	"                                            WHEN 3 THEN 'CORRELATION' "
+	"                                            WHEN 4 THEN 'MCELEM' "
+	"                                            WHEN 5 THEN 'DECHIST' "
+	"                                            WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                            WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                            ELSE NULL "
+	"                                        END "
+	"                                        ORDER BY kind.ord "
+	"                                    ) "
+	"                                FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                                  s.stakind3, s.stakind4, "
+	"                                                  s.stakind5]) "
+	"                                    WITH ORDINALITY kind(kind, ord) "
+	"                            ), "
+	"                            'stanumbers', "
+	"                            jsonb_build_array( "
+	"                                s.stanumbers1::text, "
+	"                                s.stanumbers2::text, "
+	"                                s.stanumbers3::text, "
+	"                                s.stanumbers4::text, "
+	"                                s.stanumbers5::text "
+	"                            ), "
+	"                            'stavalues', "
+	"                            jsonb_build_array( "
+	"                                s.stavalues1::text, "
+	"                                s.stavalues2::text, "
+	"                                s.stavalues3::text, "
+	"                                s.stavalues4::text, "
+	"                                s.stavalues5::text) "
+	"                            ) "
+	"                            ORDER BY s.ordinality "
+	"                        ) "
+	"                FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s "
+	"                WHERE sd.stxdexpr IS NOT NULL "
+	"            ) "
+	"        ) "
+	"    ) AS stats "
+	"FROM pg_class r "
+	"JOIN pg_namespace n ON n.oid = r.relnamespace "
+	"JOIN pg_statistic_ext e ON e.stxrelid = r.oid "
+	"JOIN pg_statistic_ext_data sd ON sd.stxoid = e.oid "
+	"GROUP BY schemaname, tablename, ext_stats_name, server_version_num ";
+
+/* v14 is like v15, but lacks stxdinherit on pg_statistic_ext_data */
+const char *export_ext_query_v14 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS tablename, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    NULL::float4 AS n_tuples, "
+	"    NULL::integer AS n_pages, "
+	"    jsonb_object_agg( "
+	"        'regular', "
+	"        jsonb_build_object( "
+	"            'stxkinds', "
+	"            to_jsonb(e.stxkind), "
+	"            'stxdndistinct', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array(nd.attnums, ', '::text), "
+	"                            'ndistinct', "
+	"                            nd.ndistinct "
+	"                            ) "
+	"                        ORDER BY nd.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                    WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                WHERE sd.stxdndistinct IS NOT NULL "
+	"            ), "
+	"            'stxdndependencies', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array( "
+	"                                replace(dep.attrs, ' => ', ', '), ', ' "
+	"                            ), "
+	"                            'degree', "
+	"                            dep.degree "
+	"                        ) "
+	"                        ORDER BY dep.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxddependencies::text::json) "
+	"                    WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                WHERE sd.stxddependencies IS NOT NULL "
+	"            ), "
+	"            'stxdmcv', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'index', "
+	"                            mcvl.index::text, "
+	"                            'frequency', "
+	"                            mcvl.frequency::text, "
+	"                            'base_frequency', "
+	"                            mcvl.base_frequency::text, "
+	"                            'values', "
+	"                            mcvl.values, "
+	"                            'nulls', "
+	"                            mcvl.nulls "
+	"                        ) "
+	"                    ) "
+	"                FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl "
+	"                WHERE sd.stxdmcv IS NOT NULL "
+	"            ), "
+	"            'stxdexprs', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'stanullfrac', "
+	"                            s.stanullfrac::text, "
+	"                            'stawidth', "
+	"                            s.stawidth::text, "
+	"                            'stadistinct', "
+	"                            s.stadistinct::text, "
+	"                            'stakinds', "
+	"                            ( "
+	"                                SELECT "
+	"                                    jsonb_agg( "
+	"                                        CASE kind.kind "
+	"                                            WHEN 0 THEN 'TRIVIAL' "
+	"                                            WHEN 1 THEN 'MCV' "
+	"                                            WHEN 2 THEN 'HISTOGRAM' "
+	"                                            WHEN 3 THEN 'CORRELATION' "
+	"                                            WHEN 4 THEN 'MCELEM' "
+	"                                            WHEN 5 THEN 'DECHIST' "
+	"                                            WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                            WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                            ELSE NULL "
+	"                                        END "
+	"                                        ORDER BY kind.ord "
+	"                                    ) "
+	"                                FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                                  s.stakind3, s.stakind4, "
+	"                                                  s.stakind5]) "
+	"                                    WITH ORDINALITY kind(kind, ord) "
+	"                            ), "
+	"                            'stanumbers', "
+	"                            jsonb_build_array( "
+	"                                s.stanumbers1::text, "
+	"                                s.stanumbers2::text, "
+	"                                s.stanumbers3::text, "
+	"                                s.stanumbers4::text, "
+	"                                s.stanumbers5::text "
+	"                            ), "
+	"                            'stavalues', "
+	"                            jsonb_build_array( "
+	"                                s.stavalues1::text, "
+	"                                s.stavalues2::text, "
+	"                                s.stavalues3::text, "
+	"                                s.stavalues4::text, "
+	"                                s.stavalues5::text) "
+	"                            ) "
+	"                            ORDER BY s.ordinality "
+	"                        ) "
+	"                FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s "
+	"                WHERE sd.stxdexpr IS NOT NULL "
+	"            ) "
+	"        ) "
+	"    ) AS stats "
+	"FROM pg_class r "
+	"JOIN pg_namespace n ON n.oid = r.relnamespace "
+	"JOIN pg_statistic_ext e ON e.stxrelid = r.oid "
+	"JOIN pg_statistic_ext_data sd ON sd.stxoid = e.oid "
+	"GROUP BY schemaname, tablename, ext_stats_name, server_version_num ";
+
+/* v12-v13 are like v14, but lack stxdexpr on pg_statistic_ext_data */
+const char *export_ext_query_v12 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS tablename, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    NULL::float4 AS n_tuples, "
+	"    NULL::integer AS n_pages, "
+	"    jsonb_object_agg( "
+	"        'regular', "
+	"        jsonb_build_object( "
+	"            'stxkinds', "
+	"            to_jsonb(e.stxkind), "
+	"            'stxdndistinct', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array(nd.attnums, ', '::text), "
+	"                            'ndistinct', "
+	"                            nd.ndistinct "
+	"                            ) "
+	"                        ORDER BY nd.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                    WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                WHERE sd.stxdndistinct IS NOT NULL "
+	"            ), "
+	"            'stxdndependencies', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array( "
+	"                                replace(dep.attrs, ' => ', ', '), ', ' "
+	"                            ), "
+	"                            'degree', "
+	"                            dep.degree "
+	"                        ) "
+	"                        ORDER BY dep.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxddependencies::text::json) "
+	"                    WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                WHERE sd.stxddependencies IS NOT NULL "
+	"            ), "
+	"            'stxdmcv', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'index', "
+	"                            mcvl.index::text, "
+	"                            'frequency', "
+	"                            mcvl.frequency::text, "
+	"                            'base_frequency', "
+	"                            mcvl.base_frequency::text, "
+	"                            'values', "
+	"                            mcvl.values, "
+	"                            'nulls', "
+	"                            mcvl.nulls "
+	"                        ) "
+	"                    ) "
+	"                FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl "
+	"                WHERE sd.stxdmcv IS NOT NULL "
+	"            ) "
+	"        ) "
+	"    ) AS stats "
+	"FROM pg_class r "
+	"JOIN pg_namespace n ON n.oid = r.relnamespace "
+	"JOIN pg_statistic_ext e ON e.stxrelid = r.oid "
+	"JOIN pg_statistic_ext_data sd ON sd.stxoid = e.oid "
+	"GROUP BY schemaname, tablename, ext_stats_name, server_version_num ";
+
+/*
+ * v10-v11 are like v12, but:
+ *     - MCV is gone
+ *     - remaining stats are stored on pg_statistic_ext
+ *     - pg_statistic_ext_data is gone
+ */
+
+const char *export_ext_query_v10 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS tablename, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    NULL::float4 AS n_tuples, "
+	"    NULL::integer AS n_pages, "
+	"    jsonb_object_agg( "
+	"        'regular', "
+	"        jsonb_build_object( "
+	"            'stxkinds', "
+	"            to_jsonb(e.stxkind), "
+	"            'stxdndistinct', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array(nd.attnums, ', '::text), "
+	"                            'ndistinct', "
+	"                            nd.ndistinct "
+	"                            ) "
+	"                        ORDER BY nd.ord "
+	"                    ) "
+	"                FROM json_each_text(e.stxndistinct::text::json) "
+	"                    WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                WHERE e.stxndistinct IS NOT NULL "
+	"            ), "
+	"            'stxdndependencies', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array( "
+	"                                replace(dep.attrs, ' => ', ', '), ', ' "
+	"                            ), "
+	"                            'degree', "
+	"                            dep.degree "
+	"                        ) "
+	"                        ORDER BY dep.ord "
+	"                    ) "
+	"                FROM json_each_text(e.stxdependencies::text::json) "
+	"                    WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                WHERE e.stxdependencies IS NOT NULL "
+	"            ) "
+	"        ) "
+	"    ) AS stats "
+	"FROM pg_class r "
+	"JOIN pg_namespace n ON n.oid = r.relnamespace "
+	"JOIN pg_statistic_ext e ON e.stxrelid = r.oid "
+	"GROUP BY schemaname, tablename, ext_stats_name, server_version_num ";
+
 int
 main(int argc, char *argv[])
 {
@@ -138,6 +546,7 @@ main(int argc, char *argv[])
 	PQExpBufferData sql;
 
 	PGconn	   *conn;
+	int			server_version_num;
 
 	FILE	   *copystream = stdout;
 
@@ -227,14 +636,31 @@ main(int argc, char *argv[])
 
 	conn = connectDatabase(&cparams, progname, echo, false, true);
 
+	server_version_num = PQserverVersion(conn);
+
 	initPQExpBuffer(&sql);
 
 	appendPQExpBufferStr(&sql, "COPY (");
 
-	if (PQserverVersion(conn) >= 170000)
-		appendPQExpBufferStr(&sql, export_query_v17);
-	else if (PQserverVersion(conn) >= 100000)
-		appendPQExpBufferStr(&sql, export_query_v10);
+	if (server_version_num >= 170000)
+		appendPQExpBufferStr(&sql, export_rel_query_v17);
+	else if (server_version_num >= 100000)
+		appendPQExpBufferStr(&sql, export_rel_query_v10);
+	else
+		pg_fatal("exporting statistics from databases prior to version 10 not supported");
+
+	appendPQExpBufferStr(&sql, " UNION ALL ");
+
+	if (server_version_num >= 170000)
+		appendPQExpBufferStr(&sql, export_ext_query_v17);
+	else if (server_version_num >= 150000)
+		appendPQExpBufferStr(&sql, export_ext_query_v15);
+	else if (server_version_num >= 140000)
+		appendPQExpBufferStr(&sql, export_ext_query_v14);
+	else if (server_version_num >= 120000)
+		appendPQExpBufferStr(&sql, export_ext_query_v12);
+	else if (server_version_num >= 100000)
+		appendPQExpBufferStr(&sql, export_ext_query_v10);
 	else
 		pg_fatal("exporting statistics from databases prior to version 10 not supported");
 
@@ -244,7 +670,7 @@ main(int argc, char *argv[])
 	result_status = PQresultStatus(result);
 
 	if (result_status != PGRES_COPY_OUT)
-		pg_fatal("malformed copy command");
+		pg_fatal("malformed copy command: %s", PQerrorMessage(conn));
 
 	for (;;)
 	{
diff --git a/src/bin/scripts/pg_import_stats.c b/src/bin/scripts/pg_import_stats.c
index 122afc0971..a8b6f9c701 100644
--- a/src/bin/scripts/pg_import_stats.c
+++ b/src/bin/scripts/pg_import_stats.c
@@ -57,6 +57,7 @@ main(int argc, char *argv[])
 
 	int		i;
 	int		numtables;
+	int		numextstats;
 
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
@@ -141,21 +142,69 @@ main(int argc, char *argv[])
 
 	/* iterate over records */
 
-
+	/*
+	 * Create a table that can received the COPY-ed file which is a mix
+	 * of relation statistics and extended statistics.
+	 */
 	result = PQexec(conn,
 		"CREATE TEMPORARY TABLE import_stats ( "
+		"schemaname text, "
+		"relname text, "
+		"ext_stats_name text, "
+		"server_version_num integer, "
+		"n_tuples float4, "
+		"n_pages integer, "
+		"stats jsonb )");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("could not create temporary table: %s", PQerrorMessage(conn));
+
+	PQclear(result);
+
+	/*
+	 * Create a table just for the relation statistics
+	 */
+	result = PQexec(conn,
+		"CREATE TEMPORARY TABLE import_rel_stats ( "
+		"id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
+		"schemaname text, "
+		"relname text, "
+		"server_version_num integer, "
+		"n_tuples float4, "
+		"n_pages integer, "
+		"stats jsonb )");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("could not create temporary table: %s", PQerrorMessage(conn));
+
+
+	PQclear(result);
+
+	/*
+	 * Create a table just for extended statistics
+	 */
+	result = PQexec(conn,
+		"CREATE TEMPORARY TABLE import_ext_stats ( "
 		"id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
-		"schemaname text, relname text, server_version_num integer, "
-		"n_tuples float4, n_pages integer, stats jsonb )");
+		"schemaname text, "
+		"relname text, "
+		"ext_stats_name text, "
+		"server_version_num integer, "
+		"n_tuples float4, "
+		"n_pages integer, "
+		"stats jsonb )");
 
 	if (PQresultStatus(result) != PGRES_COMMAND_OK)
-		pg_fatal("could not create temporary file: %s", PQerrorMessage(conn));
+		pg_fatal("could not create temporary table: %s", PQerrorMessage(conn));
 
 	PQclear(result);
 
+	/*
+	 * Copy input data into combined table.
+	 */
 	result = PQexec(conn,
-		"COPY import_stats(schemaname, relname, server_version_num, n_tuples, "
-		"n_pages, stats) FROM STDIN");
+		"COPY import_stats(schemaname, relname, ext_stats_name, "
+		"server_version_num, n_tuples, n_pages, stats) FROM STDIN");
 
 	if (PQresultStatus(result) != PGRES_COPY_IN)
 		pg_fatal("error copying data to import_stats: %s", PQerrorMessage(conn));
@@ -185,30 +234,52 @@ main(int argc, char *argv[])
 	if (PQresultStatus(result) != PGRES_COMMAND_OK)
 		pg_fatal("error copying data to import_stats: %s", PQerrorMessage(conn));
 
+	PQclear(result);
+
+	/*
+	 * Insert rel stats into their own table with numbering.
+	 */
+	result = PQexec(conn,
+		"INSERT INTO import_rel_stats(schemaname, relname, "
+		"server_version_num, n_tuples, n_pages, stats) "
+		"SELECT schemaname, relname, server_version_num, "
+		"n_tuples, n_pages, stats FROM import_stats "
+		"WHERE ext_stats_name IS NULL ");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("relation stats insert error: %s", PQerrorMessage(conn));
+
 	numtables = atol(PQcmdTuples(result));
 
 	PQclear(result);
 
-	result = PQprepare(conn, "import",
-		"SELECT pg_import_rel_stats(c.oid, s.server_version_num, "
-		"             s.n_tuples, s.n_pages, s.stats) as import_result "
-		"FROM import_stats AS s "
-		"JOIN pg_namespace AS n ON n.nspname = s.schemaname "
-		"JOIN pg_class AS c ON c.relnamespace = n.oid "
-		"                   AND c.relname = s.relname "
-		"WHERE s.id = $1::bigint ",
-		1, NULL);
+	/*
+	 * Insert extended stats into their own table with numbering.
+	 */
+	result = PQexec(conn,
+		"INSERT INTO import_ext_stats(schemaname, relname, "
+		"ext_stats_name, server_version_num, stats) "
+		"SELECT schemaname, relname, ext_stats_name, "
+		"server_version_num, stats FROM import_stats "
+		"WHERE ext_stats_name IS NOT NULL ");
 
 	if (PQresultStatus(result) != PGRES_COMMAND_OK)
-		pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
+		pg_fatal("relation stats insert error: %s", PQerrorMessage(conn));
+
+	numextstats = atol(PQcmdTuples(result));
 
 	PQclear(result);
 
-	if (!quiet)
+	if (numtables > 0)
 	{
-		result = PQprepare(conn, "echo",
-			"SELECT s.schemaname, s.relname "
-			"FROM import_stats AS s "
+
+		result = PQprepare(conn, "import_rel",
+			"SELECT pg_import_rel_stats(c.oid, s.server_version_num, "
+			"             s.n_tuples, s.n_pages, s.stats) as import_result "
+			"FROM import_rel_stats AS s "
+			"JOIN pg_namespace AS n ON n.nspname = s.schemaname "
+			"JOIN pg_class AS c ON c.relnamespace = n.oid "
+			"                   AND c.relname = s.relname "
 			"WHERE s.id = $1::bigint ",
 			1, NULL);
 
@@ -216,62 +287,172 @@ main(int argc, char *argv[])
 			pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
 
 		PQclear(result);
-	}
 
-	for (i = 1; i <= numtables; i++)
-	{
-		char	istr[32];
-		char   *schema = NULL;
-		char   *table = NULL;
+		if (!quiet)
+		{
+			result = PQprepare(conn, "echo_rel",
+				"SELECT s.schemaname, s.relname "
+				"FROM import_rel_stats AS s "
+				"WHERE s.id = $1::bigint ",
+				1, NULL);
 
-		const char *const values[] = {istr};
+			if (PQresultStatus(result) != PGRES_COMMAND_OK)
+				pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
 
-		snprintf(istr, 32, "%d", i);
+			PQclear(result);
+		}
 
-		if (!quiet)
+		for (i = 1; i <= numtables; i++)
 		{
-			result = PQexecPrepared(conn, "echo", 1, values, NULL, NULL, 0);
-			schema = pg_strdup(PQgetvalue(result, 0, 0));
-			table = pg_strdup(PQgetvalue(result, 0, 1));
+			char	istr[32];
+			char   *schema = NULL;
+			char   *table = NULL;
+
+			const char *const values[] = {istr};
+
+			snprintf(istr, 32, "%d", i);
+
+			if (!quiet)
+			{
+				result = PQexecPrepared(conn, "echo_rel", 1, values, NULL, NULL, 0);
+				schema = pg_strdup(PQgetvalue(result, 0, 0));
+				table = pg_strdup(PQgetvalue(result, 0, 1));
+			}
+
+			PQclear(result);
+
+			result = PQexecPrepared(conn, "import_rel", 1, values, NULL, NULL, 0);
+
+			if (quiet)
+			{
+				PQclear(result);
+				continue;
+			}
+
+			if (PQresultStatus(result) == PGRES_TUPLES_OK)
+			{
+				int 	rows = PQntuples(result);
+
+				if (rows == 1)
+				{
+					char   *retval = PQgetvalue(result, 0, 0);
+					if (*retval == 't')
+						printf("%s.%s: imported\n", schema, table);
+					else
+						printf("%s.%s: failed\n", schema, table);
+				}
+				else if (rows == 0)
+					printf("%s.%s: not found\n", schema, table);
+				else
+					pg_fatal("import function must return 0 or 1 rows");
+			}
+			else
+				printf("%s.%s: error: %s\n", schema, table, PQerrorMessage(conn));
+
+			if (schema != NULL)
+				pfree(schema);
+
+			if (table != NULL)
+				pfree(table);
+
+			PQclear(result);
 		}
+	}
 
-		PQclear(result);
+	if (numextstats > 0)
+	{
+
+	result = PQprepare(conn, "import_ext",
+		"SELECT pg_import_ext_stats(e.oid, s.server_version_num, "
+		"             s.stats) as import_result "
+		"FROM import_ext_stats AS s "
+		"JOIN pg_namespace AS n ON n.nspname = s.schemaname "
+		"JOIN pg_class AS c ON c.relnamespace = n.oid "
+		"                   AND c.relname = s.relname "
+		"JOIN pg_statistic_ext AS e ON e.stxrelid = c.oid "
+		"                   AND e.stxname = s.ext_stats_name "
+		"WHERE s.id = $1::bigint ",
+		1, NULL);
 
-		result = PQexecPrepared(conn, "import", 1, values, NULL, NULL, 0);
+		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+			pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
 
-		if (quiet)
+		PQclear(result);
+
+		if (!quiet)
 		{
+			result = PQprepare(conn, "echo_ext",
+				"SELECT s.schemaname, s.relname, s.ext_stats_name "
+				"FROM import_ext_stats AS s "
+				"WHERE s.id = $1::bigint ",
+				1, NULL);
+
+			if (PQresultStatus(result) != PGRES_COMMAND_OK)
+				pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
+
 			PQclear(result);
-			continue;
 		}
 
-		if (PQresultStatus(result) == PGRES_TUPLES_OK)
+		for (i = 1; i <= numextstats; i++)
 		{
-			int 	rows = PQntuples(result);
+			char	istr[32];
+			char   *schema = NULL;
+			char   *table = NULL;
+			char   *stat = NULL;
+
+			const char *const values[] = {istr};
+
+			snprintf(istr, 32, "%d", i);
 
-			if (rows == 1)
+			if (!quiet)
 			{
-				char   *retval = PQgetvalue(result, 0, 0);
-				if (*retval == 't')
-					printf("%s.%s: imported\n", schema, table);
+				result = PQexecPrepared(conn, "echo_ext", 1, values, NULL, NULL, 0);
+				schema = pg_strdup(PQgetvalue(result, 0, 0));
+				table = pg_strdup(PQgetvalue(result, 0, 1));
+				stat = pg_strdup(PQgetvalue(result, 0, 2));
+			}
+
+			PQclear(result);
+
+			result = PQexecPrepared(conn, "import_ext", 1, values, NULL, NULL, 0);
+
+			if (quiet)
+			{
+				PQclear(result);
+				continue;
+			}
+
+			if (PQresultStatus(result) == PGRES_TUPLES_OK)
+			{
+				int 	rows = PQntuples(result);
+
+				if (rows == 1)
+				{
+					char   *retval = PQgetvalue(result, 0, 0);
+					if (*retval == 't')
+						printf("%s on %s.%s: imported\n", stat, schema, table);
+					else
+						printf("%s on %s.%s: failed\n", stat, schema, table);
+				}
+				else if (rows == 0)
+					printf("%s on %s.%s: not found\n", stat, schema, table);
 				else
-					printf("%s.%s: failed\n", schema, table);
+					pg_fatal("import function must return 0 or 1 rows");
 			}
-			else if (rows == 0)
-				printf("%s.%s: not found\n", schema, table);
 			else
-				pg_fatal("import function must return 0 or 1 rows");
-		}
-		else
-			printf("%s.%s: error: %s\n", schema, table, PQerrorMessage(conn));
+				printf("%s on %s.%s: error: %s\n", stat, schema, table, PQerrorMessage(conn));
 
-		if (schema != NULL)
-			pfree(schema);
+			if (schema != NULL)
+				pfree(schema);
 
-		if (table != NULL)
-			pfree(table);
+			if (table != NULL)
+				pfree(table);
 
-		PQclear(result);
+			if (stat != NULL)
+				pfree(stat);
+
+			PQclear(result);
+		}
 	}
 
 	exit(0);
-- 
2.43.0

---

From: Andrei Lepikhov <[email-masked]>
Date: Fri, 15 Dec 2023 15:36:08 +0700

On 13/12/2023 17:26, Corey Huinker wrote:> 4. I don't yet have a 
complete vision for how these tools will be used
> by pg_upgrade and pg_dump/restore, the places where these will provide 
> the biggest win for users.

Some issues here with docs:

func.sgml:28465: parser error : Opening and ending tag mismatch: sect1 
line 26479 and sect2
   </sect2>
           ^

Also, as I remember, we already had some attempts to invent dump/restore 
statistics [1,2]. They were stopped with the problem of type 
verification. What if the definition of the type has changed between the 
dump and restore? As I see in the code, Importing statistics you just 
check the column name and don't see into the type.

[1] Backup and recovery of pg_statistic
https://www.postgresql.org/message-id/flat/724322880.K8vzik8zPz%40abook
[2] Re: Ideas about a better API for postgres_fdw remote estimates
https://www.postgresql.org/message-id/7a40707d-1758-85a2-7bb1-6e5775518e64%40postgrespro.ru

-- 
regards,
Andrei Lepikhov
Postgres Professional

---

From: Corey Huinker <[email-masked]>
Date: Fri, 15 Dec 2023 23:30:25 -0500

> func.sgml:28465: parser error : Opening and ending tag mismatch: sect1

Apologies, will fix.

> check the column name and don't see into the type.

We look up the imported statistics via column name, that is correct.

However, the values in stavalues and mcv and such are stored purely as
text, so they must be casted using the input functions for that particular
datatype. If that column definition changed, or the underlying input
function changed, the stats import of that particular table would fail. It
should be noted, however, that those same input functions were used to
bring the data into the table via restore, so it would have already failed
on that step. Either way, the structure of the table has effectively
changed, so failure to import those statistics would be a good thing.

> https://www.postgresql.org/message-id/flat/724322880.K8vzik8zPz%40abook

That proposal sought to serialize enough information on the old server such
that rows could be directly inserted into pg_statistic on the new server.
As was pointed out at the time, version N of a server cannot know what the
format of pg_statistic will be in version N+1.

This patch avoids that problem by inspecting the structure of the object to
be faux-analyzed, and using that to determine what parts of the JSON to
fetch, and what datatype to cast values to in cases like mcv and
stavaluesN. The exported JSON has no oids in it whatseover, all elements
subject to casting on import have already been cast to text, and the record
returned has the server version number of the producing system, and the
import function can use that to determine how it interprets the data it
finds.

> https://www.postgresql.org/message-id/7a40707d-1758-85a2-7bb1-6e5775518e64%40postgrespro.ru
This one seems to be pulling oids from the remote server, and we can't
guarantee their stability across systems, especially for objects and
operators from extensions. I tried to go the route of extracting the full
text name of an operator, but discovered that the qualified names, in
addition to being unsightly, were irrelevant because we can't insert stats
that disagree about type with the attribute/expression. So it didn't matter
what type the remote system thought it had, the local system was going to
coerce it into the expected data type or ereport() trying.

I think there is hope for having do_analyze() run a remote query fetching
the remote table's exported stats and then storing them locally, possibly
after some modification, and that would save us from having to sample a
remote table.

---

From: Tomas Vondra <[email-masked]>
Date: Tue, 26 Dec 2023 02:18:56 +0100

Hi,

I finally had time to look at the last version of the patch, so here's a
couple thoughts and questions in somewhat random order. Please take this
as a bit of a brainstorming and push back if you disagree some of my
comments.

In general, I like the goal of this patch - not having statistics is a
common issue after an upgrade, and people sometimes don't even realize
they need to run analyze. So, it's definitely worth improving.

I'm not entirely sure about the other use case - allowing people to
tweak optimizer statistics on a running cluster, to see what would be
the plan in that case. Or more precisely - I agree that would be an
interesting and useful feature, but maybe the interface should not be
the same as for the binary upgrade use case?

interfaces
----------

When I thought about the ability to dump/load statistics in the past, I
usually envisioned some sort of DDL that would do the export and import.
So for example we'd have EXPORT STATISTICS / IMPORT STATISTICS commands,
or something like that, and that'd do all the work. This would mean
stats are "first-class citizens" and it'd be fairly straightforward to
add this into pg_dump, for example. Or at least I think so ...

Alternatively we could have the usual "functional" interface, with a
functions to export/import statistics, replacing the DDL commands.

Unfortunately, none of this works for the pg_upgrade use case, because
existing cluster versions would not support this new interface, of
course. That's a significant flaw, as it'd make this useful only for
upgrades of future versions.

So I think for the pg_upgrade use case, we don't have much choice other
than using "custom" export through a view, which is what the patch does.

However, for the other use case (tweaking optimizer stats) this is not
really an issue - that always happens on the same instance, so no issue
with not having the "export" function and so on. I'd bet there are more
convenient ways to do this than using the export view. I'm sure it could
share a lot of the infrastructure, ofc.

I suggest we focus on the pg_upgrade use case for now. In particular, I
think we really need to find a good way to integrate this into
pg_upgrade. I'm not against having custom CLI commands, but it's still a
manual thing - I wonder if we could extend pg_dump to dump stats, or
make it built-in into pg_upgrade in some way (possibly disabled by
default, or something like that).

JSON format
-----------

As for the JSON format, I wonder if we need that at all? Isn't that an
unnecessary layer of indirection? Couldn't we simply dump pg_statistic
and pg_statistic_ext_data in CSV, or something like that? The amount of
new JSONB code seems to be very small, so it's OK I guess.

I'm still a bit unsure about the "right" JSON schema. I find it a bit
inconvenient that the JSON objects mimic the pg_statistic schema very
closely. In particular, it has one array for stakind values, another
array for stavalues, array for stanumbers etc. I understand generating
this JSON in SQL is fairly straightforward, and for the pg_upgrade use
case it's probably OK. But my concern is it's not very convenient for
the "manual tweaking" use case, because the "related" fields are
scattered in different parts of the JSON.

That's pretty much why I envisioned a format "grouping" the arrays for a
particular type of statistics (MCV, histogram) into the same object, as
for example in

 {
   "mcv" : {"values" : [...], "frequencies" : [...]}
   "histogram" : {"bounds" : [...]}
 }

But that's probably much harder to generate from plain SQL (at least I
think so, I haven't tried).

data missing in the export
--------------------------

I think the data needs to include more information. Maybe not for the
pg_upgrade use case, where it's mostly guaranteed not to change, but for
the "manual tweak" use case it can change. And I don't think we want two
different formats - we want one, working for everything.

Consider for example about the staopN and stacollN fields - if we clone
the stats from one table to the other, and the table uses different
collations, will that still work? Similarly, I think we should include
the type of each column, because it's absolutely not guaranteed the
import function will fail if the type changes. For example, if the type
changes from integer to text, that will work, but the ordering will
absolutely not be the same. And so on.

For the extended statistics export, I think we need to include also the
attribute names and expressions, because these can be different between
the statistics. And not only that - the statistics values reference the
attributes by positions, but if the two tables have the attributes in a
different order (when ordered by attnum), that will break stuff.

more strict checks
------------------

I think the code should be a bit more "defensive" when importing stuff,
and do at least some sanity checks. For the pg_upgrade use case this
should be mostly non-issue (except for maybe helping to detect bugs
earlier), but for the "manual tweak" use case it's much more important.

By this I mean checks like:

* making sure the frequencies in MCV lists are not obviously wrong
  (outside [0,1], sum exceeding > 1.0, etc.)

* cross-checking that stanumbers/stavalues make sense (e.g. that MCV has
  both arrays while histogram has only stavalues, that the arrays have
  the same length for MCV, etc.)

* checking there are no duplicate stakind values (e.g. two MCV lists)

This is another reason why I was thinking the current JSON format may be
a bit inconvenient, because it loads the fields separately, making the
checks harder. But I guess it could be done after loading everything, as
a separate phase.

Not sure if all the checks need to be regular elog(ERROR), perhaps some
could/should be just asserts.

minor questions
---------------

1) Should the views be called pg_statistic_export or pg_stats_export?
Perhaps pg_stats_export is better, because the format is meant to be
human-readable (rather than 100% internal).

2) It's not very clear what "non-transactional update" of pg_class
fields actually means. Does that mean we update the fields in-place,
can't be rolled back, is not subject to MVCC or what? I suspect users
won't know unless the docs say that explicitly.

3) The "statistics.c" code should really document the JSON structure. Or
maybe if we plan to use this for other purposes, it should be documented
in the SGML?

Actually, this means that the use supported cases determine if the
expected JSON structure is part of the API. For pg_upgrade we could keep
it as "internal" and maybe change it as needed, but for "manual tweak"
it'd become part of the public API.

4) Why do we need the separate "replaced" flags in import_stakinds? Can
it happen that collreplaces/opreplaces differ from kindreplaces?

5) What happens in we import statistics for a table that already has
some statistics? Will this discard the existing statistics, or will this
merge them somehow? (I think we should always discard the existing
stats, and keep only the new version.)

6) What happens if we import extended stats with mismatching definition?
For example, what if the "new" statistics object does not have "mcv"
enabled, but the imported data do include MCV? What if the statistics do
have the same number of "dimensions" but not the same number of columns
and expressions?

7) The func.sgml additions in 0007 seems a bit strange, particularly the
first sentence of the paragraph.

8) While experimenting with the patch, I noticed this:

  create table t (a int, b int, c text);
  create statistics s on a, b, c, (a+b), (a-b) from t;

  create table t2 (a text, b text, c text);
  create statistics s2 on a, c from t2;

  select pg_import_ext_stats(
    (select oid from pg_statistic_ext where stxname = 's2'),
    (select server_version_num from pg_statistic_ext_export
                              where ext_stats_name = 's'),
    (select stats from pg_statistic_ext_export
                 where ext_stats_name = 's'));

WARNING:  statistics import has 5 mcv dimensions, but the expects 2.
Skipping excess dimensions.
ERROR:  statistics import has 5 mcv dimensions, but the expects 2.
Skipping excess dimensions.

I guess we should not trigger WARNING+ERROR with the same message.

regards

---

From: Bruce Momjian <[email-masked]>
Date: Tue, 26 Dec 2023 13:15:14 -0500

> interfaces

I have some thoughts on this too.  I understand the desire to add
something that can be used for upgrades _to_ PG 17, but I am concerned
that this will give us a cumbersome API that will hamper future
development.  I think we should develop the API we want, regardless of
how useful it is for upgrades _to_ PG 17, and then figure out what
short-term hacks we can add to get it working for upgrades _to_ PG 17; 
these hacks can eventually be removed.  Even if they can't be removed,
they are export-only and we can continue developing the import SQL
command cleanly, and I think import is going to need the most long-term
maintenance.

I think we need a robust API to handle two cases:

*  changes in how we store statistics
*  changes in how how data type values are represented in the statistics

We have had such changes in the past, and I think these two issues are
what have prevented import/export of statistics up to this point.
Developing an API that doesn't cleanly handle these will cause long-term
pain.

In summary, I think we need an SQL-level command for this.  I think we
need to embed the Postgres export version number into the statistics
export file (maybe in the COPY header), and then load the file via COPY
internally (not JSON) into a temporary table that we know matches the
exported Postgres version.  We then need to use SQL to make any
adjustments to it before loading it into pg_statistic.  Doing that
internally in JSON just isn't efficient.  If people want JSON for such
cases, I suggest we add a JSON format to COPY.

I think we can then look at pg_upgrade to see if we can simulate the
export action which can use the statistics import SQL command.

-- 
  Bruce Momjian  <[email-masked]>        https://momjian.us
  EDB                                      https://enterprisedb.com

  Only you can decide what is important to you.

---

From: Corey Huinker <[email-masked]>
Date: Wed, 27 Dec 2023 21:41:31 -0500

> the same as for the binary upgrade use case?

This was the reason I settled on the interface that I did: while we can
create whatever interface we want for importing the statistics, we would
need to be able to extract stats from databases using only the facilities
available in those same databases, and then store that in a medium that
could be conveyed across databases, either by text files or by saving them
off in a side table prior to upgrade. JSONB met the criteria.

> share a lot of the infrastructure, ofc.

So, there is a third use case - foreign data wrappers. When analyzing a
foreign table, at least one in the postgresql_fdw family of foreign
servers, we should be able to send a query specific to the version and
dialect of that server, get back the JSONB, and import those results. That
use case may be more tangible to you than the tweak/tuning case.

> JSON format

I see a few problems with dumping pg_statistic[_ext_data]. The first is
that the importer now has to understand all of the past formats of those
two tables. The next is that the tables are chock full of Oids that don't
necessarily carry forward. I could see us having a text-ified version of
those two tables, but we'd need that for all previous iterations of those
table formats. Instead, I put the burden on the stats export to de-oid the
data and make it *_in() function friendly.

>    "histogram" : {"bounds" : [...]}

I agree that would be a lot more readable, and probably a lot more
debuggable. But I went into this unsure if there could be more than one
stats slot of a given kind per table. Knowing that they must be unique
helps.

> think so, I haven't tried).

I think it would be harder, but far from impossible.

> data missing in the export

I"m not against this at all, and I started out doing that, but the
qualified names of operators got _ugly_, and I quickly realized that what I
was generating wouldn't matter, either the input data would make sense for
the attribute's stats or it would fail trying.

> absolutely not be the same. And so on.

I can see including the type of the column, that's a lot cleaner than the
operator names for sure, and I can see us rejecting stats or sections of
stats in certain situations. Like in your example, if the collation
changed, then reject all "<" op stats but keep the "=" ones.

> different order (when ordered by attnum), that will break stuff.

Correct me if I'm wrong, but I thought expression parse trees change _a
lot_ from version to version?

Attribute reordering is a definite vulnerability of the current
implementation, so an attribute name export might be a way to mitigate that.

>   (outside [0,1], sum exceeding > 1.0, etc.)

+1

>   the same length for MCV, etc.)

To this end, there's an edge-case hack in the code where I have to derive
the array elemtype. I had thought that examine_attribute() or
std_typanalyze() was going to do that for me, but it didn't. Very much want
your input there.

> * checking there are no duplicate stakind values (e.g. two MCV lists)

Per previous comment, it's good to learn these restrictions.

> could/should be just asserts.

For this first pass, all errors were one-size fits all, safe for the
WARNING vs ERROR.

> minor questions

I have no opinion on what the best name would be, and will go with
consensus.

> won't know unless the docs say that explicitly.

Correct. Cannot be rolled back, not subject to MVCC.

> maybe if we plan to use this for other purposes, it should be documented

I agree, but I also didn't expect the format to survive first contact with
reviewers, so I held back.

> it happen that collreplaces/opreplaces differ from kindreplaces?

That was initially done to maximize the amount of code that could be copied
from do_analyze(). In retrospect, I like how extended statistics just
deletes all the pg_statistic_ext_data rows and replaces them and I would
like to do the same for pg_statistic before this is all done.

> stats, and keep only the new version.)

In the case of pg_statistic_ext_data, the stats are thrown out and replaced
by the imported ones.

In the case of pg_statistic, it's basically an upsert, and any values that
were missing in the JSON are not updated on the existing row. That's
appealing in a tweak situation where you want to only alter one or two bits
of a stat, but not really useful in other situations. Per previous comment,
I'd prefer a clean slate and forcing tweaking use cases to fill in all the
blanks.

> have the same number of "dimensions" but not the same number of columns

The importer is currently driven by the types of stats to be expected for
that pg_attribute/pg_statistic_ext. It only looks for things that are
possible for that stat type, and any extra JSON values are ignored.

---

From: Bruce Momjian <[email-masked]>
Date: Wed, 27 Dec 2023 22:11:23 -0500

On Wed, Dec 27, 2023 at 09:41:31PM -0500, Corey Huinker wrote:
> This was the reason I settled on the interface that I did: while we can create
> whatever interface we want for importing the statistics, we would need to be
> able to extract stats from databases using only the facilities available in
> those same databases, and then store that in a medium that could be conveyed
> across databases, either by text files or by saving them off in a side table
> prior to upgrade. JSONB met the criteria.

Uh, it wouldn't be crazy to add this capability to pg_upgrade/pg_dump in
a minor version upgrade if it wasn't enabled by default, and if we were
very careful.

---

From: Tomas Vondra <[email-masked]>
Date: Fri, 29 Dec 2023 00:55:08 +0100

On 12/13/23 11:26, Corey Huinker wrote:
> way that was the most accommodating to cstring input functions. So,
> while so many of the statistic values are obviously only ever
> integers/floats, those get stored as a numeric data type which lacks
> direct numeric->int/float4/float8 functions (though we could certainly
> create them, and I'm not against that), casting them to text lets us
> leverage pg_strtoint16, etc.
>
>
>
> That's a very good question. I went with this format because it was
> fairly straightforward to code in SQL using existing JSON/JSONB
> functions, and that's what we will need if we want to export statistics
> on any server currently in existence. I'm certainly not locked in with
> the current format, and if it can be shown how to transform the data
> into a superior format, I'd happily do so.
>
> take Oids as input. This is intentional to allow stats from one object
> to be imported into another object.
> * User scripts pg_export_stats and pg_import stats, which offer a
> primitive way to serialize all the statistics of one database and import
> them into another.
> pg_catalog. As such, this should be significantly faster than even the
> most cursory analyze operation, and therefore should be useful in
> upgrade situations, allowing the database to work with "good enough"
> stats more quickly, while still allowing for regular autovacuum to
> recalculate the stats "for real" at some later point.
>
> array type. I had assumed that the existing exmaine_attribute()
> functions would have properly derived the typoid for that column, but it
> appears to not be the case, and I'm clearly missing how the existing
> code gets it right.
Hmm, after looking at this, I'm not sure it's such an ugly hack ...

The way this works for ANALYZE is that examine_attribute() eventually
calls the typanalyze function:

  if (OidIsValid(stats->attrtype->typanalyze))
    ok = DatumGetBool(OidFunctionCall1(stats->attrtype->typanalyze,
                                       PointerGetDatum(stats)));

which for arrays is array_typanalyze, and this sets stats->extra_data to
ArrayAnalyzeExtraData with all the interesting info about the array
element type, and then also std_extra_data with info about the array
type itself.

  stats -> extra_data -> std_extra_data

compute_array_stats then "restores" std_extra_data to compute standard
stats for the whole array, and then uses the ArrayAnalyzeExtraData to
calculate stats for the elements.

It's not exactly pretty, because there are global variables and so on.

And examine_rel_attribute() does the same thing - calls typanalyze, so
if I break after it returns, I see this for int[] column:

(gdb) p * (ArrayAnalyzeExtraData *) stat->extra_data

$1 = {type_id = 23, eq_opr = 96, coll_id = 0, typbyval = true, typlen =
4, typalign = 105 'i', cmp = 0x2e57920, hash = 0x2e57950,
std_compute_stats = 0x6681b8 <compute_scalar_stats>, std_extra_data =
0x2efe670}

I think the "problem" will be how to use this in import_stavalues(). You
can't just do this for any array type, I think. I could create an array
type (with ELEMENT=X) but with a custom analyze function, in which case
the extra_data may be something entirely different.

I suppose the correct solution would be to add an "import" function into
the pg_type catalog (next to typanalyze). Or maybe it'd be enough to set
it from the typanalyze? After all, that's what sets compute_stats.

But maybe it's enough to just do what you did - if we get an MCELEM
slot, can it ever contain anything else than array of elements of the
attribute array type? I'd bet that'd cause all sorts of issues, no?

regards

---

From: Corey Huinker <[email-masked]>
Date: Fri, 29 Dec 2023 11:27:50 -0500

> attribute array type? I'd bet that'd cause all sorts of issues, no?
Thanks for the explanation of why it wasn't working for me. Knowing that
the case of MCELEM + is-array-type is the only case where we'd need to do
that puts me at ease.

---

From: Tomas Vondra <[email-masked]>
Date: Fri, 29 Dec 2023 21:14:34 +0100

On 12/29/23 17:27, Corey Huinker wrote:
> the case of MCELEM + is-array-type is the only case where we'd need to
> do that puts me at ease.
> 

But I didn't claim MCELEM is the only slot where this might be an issue.
I merely asked if a MCELEM slot can ever contain an array with element
type different from the "original" attribute.

After thinking about this a bit more, and doing a couple experiments
with a trivial custom data type, I think this is true:

1) MCELEM slots for "real" array types are OK

I don't think we allow "real" arrays created by users directly, all
arrays are created implicitly by the system. Those types always have
array_typanalyze, which guarantees MCELEM has the correct element type.

I haven't found a way to either inject my custom array type or alter the
typanalyze to some custom function. So I think this is OK.

2) I'm not sure we can extend this regular data types / other slots

For example, I think I can implement a data type with custom typanalyze
function (and custom compute_stats function) that fills slots with some
other / strange stuff. For example I might build MCV with hashes of the
original data, a CountMin sketch, or something like that.

Yes, I don't think people do that often, but as long as the type also
implements custom selectivity functions for the operators, I think this
would work.

regards

---

From: Peter Smith <[email-masked]>
Date: Mon, 22 Jan 2024 17:09:24 +1100

2024-01 Commitfest.

Hi, This patch has a CF status of "Needs Review" [1], but it seems
there were CFbot test failures last time it was run [2]. Please have a
look and post an updated version if necessary.

======
[1] https://commitfest.postgresql.org/46/4538/
[2] https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/46/4538

Kind Regards,
Peter Smith.

---

From: Corey Huinker <[email-masked]>
Date: Fri, 2 Feb 2024 03:33:08 -0500

On Mon, Jan 22, 2024 at 1:09 AM Peter Smith <[email-masked]> wrote:

> [2]
> https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/46/4538
>
> Kind Regards,
> Peter Smith.
>

Attached is v4 of the statistics export/import patch.

This version has been refactored to match the design feedback received
previously.

The system views are gone. These were mostly there to serve as a baseline
for what an export query would look like. That role is temporarily
reassigned to pg_export_stats.c, but hopefully they will be integrated into
pg_dump in the next version. The regression test also contains the version
of each query suitable for the current server version.

The export format is far closer to the raw format of pg_statistic and
pg_statistic_ext_data, respectively. This format involves exporting oid
values for types, collations, operators, and attributes - values which are
specific to the server they were created on. To make sense of those values,
a subset of the columns of pg_type, pg_attribute, pg_collation, and
pg_operator are exported as well, which allows pg_import_rel_stats() and
pg_import_ext_stats() to reconstitute the data structure as it existed on
the old server, and adapt it to the modern structure and local schema
objects.

pg_import_rel_stats matches up local columns with the exported stats by
column name, not attnum. This allows for stats to be imported when columns
have been dropped, added, or reordered.

pg_import_ext_stats can also handle column reordering, though it currently
would get confused by changes in expressions that maintain the same result
data type. I'm not yet brave enough to handle importing nodetrees, nor do I
think it's wise to try. I think we'd be better off validating that the
destination extended stats object is identical in structure, and to fail
the import of that one object if it isn't perfect.

Export formats go back to v10.