From 9adcc9735069edc14af05b28088725594e912c84 Mon Sep 17 00:00:00 2001 From: Corey Huinker Date: Thu, 15 Feb 2024 03:11:40 -0500 Subject: [PATCH v5 1/2] Create pg_import_rel_stats. The function pg_import_rel_stats imports pg_class rowcount, pagecount, and pg_statistic data for a given relation. 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 jsonb parameter which contains the generated statistics for one relaton, the format of which varies by the version of the server that exported it. The function takes that version int account when processing the input json into pg_statistic rows. The statistics applied are not locked in any way, and will be overwritten by the next analyze, either explicit or via autovacuum. 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. Currently the function supports two boolean flags for checking the validity of the imported data. The flag validate initiates a battery of validation tests to ensure that all sub-objects (types, operators, collatons, attributes, statistics) have no duplicate values. The flag require_match_oids verifies the oids resolved in the new statistics rows match the oids specified in the json. Setting this flag makes sense during a binary upgrade, but not a restore. 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 | 6 +- src/include/statistics/statistics.h | 2 + src/include/statistics/statistics_internal.h | 28 + src/backend/statistics/Makefile | 3 +- src/backend/statistics/meson.build | 1 + src/backend/statistics/statistics.c | 1331 +++++++++++++++++ .../regress/expected/stats_export_import.out | 530 +++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/stats_export_import.sql | 499 ++++++ doc/src/sgml/func.sgml | 65 + 10 files changed, 2464 insertions(+), 3 deletions(-) create mode 100644 src/include/statistics/statistics_internal.h 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 9c120fc2b7..0e48c08566 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8825,7 +8825,11 @@ { oid => '3813', descr => 'generate XML text node', proname => 'xmltext', proisstrict => 't', prorettype => 'xml', proargtypes => 'text', prosrc => 'xmltext' }, - +{ oid => '3814', + descr => 'statistics: import to relation', + proname => 'pg_import_rel_stats', provolatile => 'v', proisstrict => 'f', + proparallel => 'u', prorettype => 'bool', proargtypes => 'oid jsonb bool bool', + prosrc => 'pg_import_rel_stats' }, { oid => '2923', descr => 'map table contents to XML', proname => 'table_to_xml', procost => '100', provolatile => 's', proparallel => 'r', prorettype => 'xml', diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h index 7f2bf18716..0c3867f918 100644 --- a/src/include/statistics/statistics.h +++ b/src/include/statistics/statistics.h @@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind, int nclauses); extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx); +extern Datum pg_import_rel_stats(PG_FUNCTION_ARGS); + #endif /* STATISTICS_H */ diff --git a/src/include/statistics/statistics_internal.h b/src/include/statistics/statistics_internal.h new file mode 100644 index 0000000000..e61a64d8b7 --- /dev/null +++ b/src/include/statistics/statistics_internal.h @@ -0,0 +1,28 @@ +/*------------------------------------------------------------------------- + * + * statistics_internal.h + * Extended statistics and selectivity estimation functions. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/statistics/statistics_internal.h + * + *------------------------------------------------------------------------- + */ +#ifndef STATISTICS_INTERNAL_H +#define STATISTICS_INTERNAL_H + +#include "nodes/pathnodes.h" + +extern void validate_no_duplicates(Datum document, bool document_null, + const char *sql, const char *docname, + const char *colname); + +extern void validate_exported_types(Datum types, bool types_null); +extern void validate_exported_collations(Datum collations, bool collations_null); +extern void validate_exported_operators(Datum operators, bool operators_null); +extern void validate_exported_attributes(Datum attributes, bool attributes_null); +extern void validate_exported_statistics(Datum statistics, bool statistics_null); + +#endif /* STATISTICS_INTERNAL_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 73b29a3d50..331e82c776 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..25ae54d4e8 --- /dev/null +++ b/src/backend/statistics/statistics.c @@ -0,0 +1,1331 @@ +/*------------------------------------------------------------------------- + * + * statistics.c + * + * IDENTIFICATION + * src/backend/statistics/statistics.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/indexing.h" +#include "catalog/pg_type.h" +#include "executor/spi.h" +#include "fmgr.h" +#include "nodes/nodeFuncs.h" +#include "parser/parse_oper.h" +#include "statistics/statistics.h" +#include "statistics/statistics_internal.h" +#include "utils/builtins.h" +#include "utils/rel.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * Struct to capture only the infomration we need from + * get_attrinfo + */ +typedef struct { + Oid typid; + int32 typmod; + Oid collid; + Oid eqopr; + Oid ltopr; + Oid basetypid; + Oid baseeqopr; + Oid baseltopr; +} AttrInfo; + + +/* + * Generate AttrInfo entries for each attribute in the relation. + * This data is a small subset of what VacAttrStats collects, + * and we leverage VacAttrStats to stay compatible with what + * do_analyze() does. + */ +static AttrInfo * +get_attrinfo(Relation rel) +{ + TupleDesc tupdesc = RelationGetDescr(rel); + int natts = tupdesc->natts; + bool has_index_exprs = false; + ListCell *indexpr_item = NULL; + AttrInfo *res = palloc0(natts * sizeof(AttrInfo)); + int i; + + /* + * 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 attr = TupleDescAttr(rel->rd_att, i); + + /* + * If this this attribute is an expression, pop an expression off + * of the list. We need to do this even if the attribute is + * dropped to pop a potential expression off the list. + */ + if (has_index_exprs && (rel->rd_index->indkey.values[i] == 0)) + { + Node *index_expr = NULL; + + 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); + res[i].typid = exprType(index_expr); + res[i].typmod = 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(attr->attcollation)) + res[i].collid = attr->attcollation; + else + res[i].collid = exprCollation(index_expr); + } + else + { + res[i].typid = attr->atttypid; + res[i].typmod = attr->atttypmod; + res[i].collid = attr->attcollation; + } + + if (attr->attisdropped) + continue; + + get_sort_group_operators(res[i].typid, + false, false, false, + &res[i].ltopr, &res[i].eqopr, NULL, + NULL); + + res[i].basetypid = get_base_element_type(res[i].typid); + if (res[i].basetypid == InvalidOid) + { + /* type is its own base type */ + res[i].basetypid = res[i].typid; + res[i].baseltopr = res[i].ltopr; + res[i].baseeqopr = res[i].eqopr; + } + else + get_sort_group_operators(res[i].basetypid, + false, false, false, + &res[i].baseltopr, &res[i].baseeqopr, + NULL, NULL); + } + return res; +} + +/* + * Delete all pg_statistic entries for a relation + inheritance type + */ +static void +remove_pg_statistics(Relation rel, Relation sd, bool inh) +{ + TupleDesc tupdesc = RelationGetDescr(rel); + int natts = tupdesc->natts; + int attnum; + + for (attnum = 1; attnum <= natts; attnum++) + { + HeapTuple tup = SearchSysCache3(STATRELATTINH, + ObjectIdGetDatum(RelationGetRelid(rel)), + Int16GetDatum(attnum), + BoolGetDatum(inh)); + + if (HeapTupleIsValid(tup)) + { + CatalogTupleDelete(sd, &tup->t_self); + + ReleaseSysCache(tup); + } + } +} + +#define NULLARG(x) ((x) ? 'n' : ' ') + +/* + * Find any duplicate values in a jsonb object casted via SPI SQL + * into a single-key table. + * + * This function assumes a valid SPI connection. + */ +void +validate_no_duplicates(Datum document, bool document_null, + const char *sql, const char *docname, + const char *colname) +{ + Oid argtypes[1] = { JSONBOID }; + Datum args[1] = { document }; + char argnulls[1] = { NULLARG(document_null) }; + + SPITupleTable *tuptable; + int ret; + + ret = SPI_execute_with_args(sql, 1, argtypes, args, argnulls, true, 0); + + if (ret != SPI_OK_SELECT) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("statistic export JSON is not in proper format"))); + + tuptable = SPI_tuptable; + if (tuptable->numvals > 0) + { + char *s = SPI_getvalue(tuptable->vals[0], tuptable->tupdesc, 1); + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("statistic export JSON document \"%s\" has duplicate rows with %s = %s", + docname, colname, (s) ? s : "NULL"))); + } +} + +/* + * Ensure that the "types" document is valid. + * + * Presently the only check we make is to ensure that no duplicate oid values + * exist in the expanded table. + * + * This function assumes a valid SPI connection. + */ +void +validate_exported_types(Datum types, bool types_null) +{ + const char *sql = + "SELECT et.oid " + "FROM jsonb_to_recordset($1) " + " AS et(oid oid, typname text, nspname text) " + "GROUP BY et.oid " + "HAVING COUNT(*) > 1 "; + + validate_no_duplicates(types, types_null, sql, "types", "oid"); +} + +/* + * Ensure that the "collations" document is valid. + * + * Presently the only check we make is to ensure that no duplicate oid values + * exist in the expanded table. + * + * This function assumes a valid SPI connection. + */ +void +validate_exported_collations(Datum collations, bool collations_null) +{ + const char* sql = + "SELECT ec.oid " + "FROM jsonb_to_recordset($1) " + " AS ec(oid oid, collname text, nspname text) " + "GROUP BY ec.oid " + "HAVING COUNT(*) > 1 "; + + validate_no_duplicates(collations, collations_null, sql, "collations", "oid"); +} + +/* + * Ensure that the "operators" document is valid. + * + * Presently the only check we make is to ensure that no duplicate oid values + * exist in the expanded table. + * + * This function assumes a valid SPI connection. + */ +void +validate_exported_operators(Datum operators, bool operators_null) +{ + const char* sql = + "SELECT eo.oid " + "FROM jsonb_to_recordset($1) " + " AS eo(oid oid, oprname text, nspname text) " + "GROUP BY eo.oid " + "HAVING COUNT(*) > 1 "; + + validate_no_duplicates(operators, operators_null, sql, "operators", "oid"); +} + +/* + * Ensure that the "attributes" document is valid. + * + * Presently the only check we make is to ensure that no duplicate attnum + * values exist in the expanded table. + * + * This function assumes a valid SPI connection. + */ +void +validate_exported_attributes(Datum attributes, bool attributes_null) +{ + const char* sql = + "SELECT ea.attnum " + "FROM jsonb_to_recordset($1) " + " AS ea(attnum int2, attname text, atttypid oid, " + " attcollation oid) " + "GROUP BY ea.attnum " + "HAVING COUNT(*) > 1 "; + + validate_no_duplicates(attributes, attributes_null, sql, "attributes", "attnum"); +} + +/* + * Ensure that the "statistics" document is valid. + * + * Presently the only check we make is to ensure that no duplicate combinations + * of (staattnum, stainherit) exist within the expanded table. + * + * This function assumes a valid SPI connection. + */ +void +validate_exported_statistics(Datum statistics, bool statistics_null) +{ + Oid argtypes[1] = { JSONBOID }; + Datum args[1] = { statistics }; + char argnulls[1] = { NULLARG(statistics_null) }; + + const char *sql = + "SELECT s.staattnum, s.stainherit " + "FROM jsonb_to_recordset($1) " + " AS s(staattnum integer, " + " stainherit boolean, " + " stanullfrac float4, " + " stawidth integer, " + " stadistinct float4, " + " stakind1 int2, " + " stakind2 int2, " + " stakind3 int2, " + " stakind4 int2, " + " stakind5 int2, " + " staop1 oid, " + " staop2 oid, " + " staop3 oid, " + " staop4 oid, " + " staop5 oid, " + " stacoll1 oid, " + " stacoll2 oid, " + " stacoll3 oid, " + " stacoll4 oid, " + " stacoll5 oid, " + " stanumbers1 float4[], " + " stanumbers2 float4[], " + " stanumbers3 float4[], " + " stanumbers4 float4[], " + " stanumbers5 float4[], " + " stavalues1 text, " + " stavalues2 text, " + " stavalues3 text, " + " stavalues4 text, " + " stavalues5 text) " + "GROUP BY s.staattnum, s.stainherit " + "HAVING COUNT(*) > 1 "; + + SPITupleTable *tuptable; + int ret; + + ret = SPI_execute_with_args(sql, 1, argtypes, args, argnulls, true, 0); + + if (ret != SPI_OK_SELECT) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("statistic export JSON is not in proper format"))); + + tuptable = SPI_tuptable; + + if (tuptable->numvals > 0) + { + char *s1 = SPI_getvalue(tuptable->vals[0], tuptable->tupdesc, 1); + char *s2 = SPI_getvalue(tuptable->vals[0], tuptable->tupdesc, 2); + + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("statistic export JSON document \"%s\" has duplicate rows with %s = %s, %s = %s", + "statistics", "staattnum", (s1) ? s1 : "NULL", + "stainherit", (s2) ? s2 : "NULL"))); + } +} + + +/* + * Transactionally import statistics for a given relation + * into pg_statistic. + * + * The jsonb datums are in the same order: + * types, collations, operators, attributes, statistics + * + * The statistics import query does not vary by server version. + * However, the stacollN columns will always be NULL for versions prior + * to v12. + * + * The query as currently written is clearly overboard, and for now serves + * to show what is possible in terms of comparing the exported statistics + * to the existing local schema. Once we have determined what types of + * checks are worthwhile, we can trim out unnecessary joins and columns. + * + * Analytic columns columns like dup_count serve to check the consistency + * and correctness of the exported data. + * + * The return value is an array of HeapTuples. + * The parameter ntuples is set to the number of HeapTuples returned. + */ + +static HeapTuple * +import_pg_statistics(Relation rel, Relation sd, int server_version_num, + Datum types_datum, bool types_null, + Datum collations_datum, bool collations_null, + Datum operators_datum, bool operators_null, + Datum attributes_datum, bool attributes_null, + Datum statistics_datum, bool statistics_null, + bool require_match_oids, int *ntuples) +{ + +#define PGS_NARGS 6 + + Oid argtypes[PGS_NARGS] = { + JSONBOID, JSONBOID, JSONBOID, JSONBOID, JSONBOID, OIDOID }; + Datum args[PGS_NARGS] = { + types_datum, collations_datum, operators_datum, + attributes_datum, statistics_datum, + ObjectIdGetDatum(RelationGetRelid(rel)) }; + char argnulls[PGS_NARGS] = { + NULLARG(types_null), NULLARG(collations_null), + NULLARG(operators_null), NULLARG(attributes_null), + NULLARG(statistics_null), NULLARG(false) }; + + /* + * This query is currently in kitchen-sink mode, and it can be trimmed down + * to eliminate any columns not needed for output or validation once + * all requirements are settled. + */ + const char *sql = + "WITH exported_types AS ( " + " SELECT et.* " + " FROM jsonb_to_recordset($1) " + " AS et(oid oid, typname text, nspname text) " + "), " + "exported_collations AS ( " + " SELECT ec.* " + " FROM jsonb_to_recordset($2) " + " AS ec(oid oid, collname text, nspname text) " + "), " + "exported_operators AS ( " + " SELECT eo.* " + " FROM jsonb_to_recordset($3) " + " AS eo(oid oid, oprname text, nspname text) " + "), " + "exported_attributes AS ( " + " SELECT ea.* " + " FROM jsonb_to_recordset($4) " + " AS ea(attnum int2, attname text, atttypid oid, " + " attcollation oid) " + "), " + "exported_statistics AS ( " + " SELECT s.* " + " FROM jsonb_to_recordset($5) " + " AS s(staattnum integer, " + " stainherit boolean, " + " stanullfrac float4, " + " stawidth integer, " + " stadistinct float4, " + " stakind1 int2, " + " stakind2 int2, " + " stakind3 int2, " + " stakind4 int2, " + " stakind5 int2, " + " staop1 oid, " + " staop2 oid, " + " staop3 oid, " + " staop4 oid, " + " staop5 oid, " + " stacoll1 oid, " + " stacoll2 oid, " + " stacoll3 oid, " + " stacoll4 oid, " + " stacoll5 oid, " + " stanumbers1 float4[], " + " stanumbers2 float4[], " + " stanumbers3 float4[], " + " stanumbers4 float4[], " + " stanumbers5 float4[], " + " stavalues1 text, " + " stavalues2 text, " + " stavalues3 text, " + " stavalues4 text, " + " stavalues5 text) " + ") " + "SELECT pga.attnum, pga.attname, pga.atttypid, pga.atttypmod, " + " pga.attcollation, pgat.typname, pgac.collname, " + " ea.attnum AS exp_attnum, ea.atttypid AS exp_atttypid, " + " ea.attcollation AS exp_attcollation, " + " et.typname AS exp_typname, et.nspname AS exp_typschema, " + " ec.collname AS exp_collname, ec.nspname AS exp_collschema, " + " es.stainherit, es.stanullfrac, es.stawidth, es.stadistinct, " + " es.stakind1, es.stakind2, es.stakind3, es.stakind4, " + " es.stakind5, " + " es.staop1 AS exp_staop1, es.staop2 AS exp_staop2, " + " es.staop3 AS exp_staop3, es.staop4 AS exp_staop4, " + " es.staop5 AS exp_staop5, " + " es.stacoll1 AS exp_staop1, es.stacoll2 AS exp_staop2, " + " es.stacoll3 AS exp_staop3, es.stacoll4 AS exp_staop4, " + " es.stacoll5 AS exp_staop5, " + " es.stanumbers1, es.stanumbers2, es.stanumbers3, " + " es.stanumbers4, es.stanumbers5, " + " es.stavalues1, es.stavalues2, es.stavalues3, es.stavalues4, " + " es.stavalues5, " + " eo1.nspname AS exp_oprschema1, " + " eo2.nspname AS exp_oprschema2, " + " eo3.nspname AS exp_oprschema3, " + " eo4.nspname AS exp_oprschema4, " + " eo5.nspname AS exp_oprschema5, " + " eo1.oprname AS exp_oprname1, " + " eo2.oprname AS exp_oprname2, " + " eo3.oprname AS exp_oprname3, " + " eo4.oprname AS exp_oprname4, " + " eo5.oprname AS exp_oprname5, " + " coalesce(io1.oid, 0) AS staop1, " + " coalesce(io2.oid, 0) AS staop2, " + " coalesce(io3.oid, 0) AS staop3, " + " coalesce(io4.oid, 0) AS staop4, " + " coalesce(io5.oid, 0) AS staop5, " + " ec1.nspname AS exp_collschema1, " + " ec2.nspname AS exp_collschema2, " + " ec3.nspname AS exp_collschema3, " + " ec4.nspname AS exp_collschema4, " + " ec5.nspname AS exp_collschema5, " + " ec1.collname AS exp_collname1, " + " ec2.collname AS exp_collname2, " + " ec3.collname AS exp_collname3, " + " ec4.collname AS exp_collname4, " + " ec5.collname AS exp_collname5, " + " coalesce(ic1.oid, 0) AS stacoll1, " + " coalesce(ic2.oid, 0) AS stacoll2, " + " coalesce(ic3.oid, 0) AS stacoll3, " + " coalesce(ic4.oid, 0) AS stacoll4, " + " coalesce(ic5.oid, 0) AS stacoll5, " + " (pga.attname IS DISTINCT FROM ea.attname) AS attname_miss, " + " (ea.attnum IS DISTINCT FROM es.staattnum) AS staattnum_miss, " + " COUNT(*) OVER (PARTITION BY pga.attnum, " + " es.stainherit) AS dup_count " + "FROM pg_attribute AS pga " + "JOIN pg_type AS pgat ON pgat.oid = pga.atttypid " + "LEFT JOIN pg_collation AS pgac ON pgac.oid = pga.attcollation " + "LEFT JOIN exported_attributes AS ea ON ea.attname = pga.attname " + "LEFT JOIN exported_statistics AS es ON es.staattnum = ea.attnum " + "LEFT JOIN exported_types AS et ON et.oid = ea.atttypid " + "LEFT JOIN exported_collations AS ec ON ec.oid = ea.attcollation " + "LEFT JOIN exported_operators AS eo1 ON eo1.oid = es.staop1 " + "LEFT JOIN exported_operators AS eo2 ON eo2.oid = es.staop2 " + "LEFT JOIN exported_operators AS eo3 ON eo3.oid = es.staop3 " + "LEFT JOIN exported_operators AS eo4 ON eo4.oid = es.staop4 " + "LEFT JOIN exported_operators AS eo5 ON eo5.oid = es.staop5 " + "LEFT JOIN exported_collations AS ec1 ON ec1.oid = es.stacoll1 " + "LEFT JOIN exported_collations AS ec2 ON ec2.oid = es.stacoll2 " + "LEFT JOIN exported_collations AS ec3 ON ec3.oid = es.stacoll3 " + "LEFT JOIN exported_collations AS ec4 ON ec4.oid = es.stacoll4 " + "LEFT JOIN exported_collations AS ec5 ON ec5.oid = es.stacoll5 " + "LEFT JOIN pg_namespace AS ion1 ON ion1.nspname = eo1.nspname " + "LEFT JOIN pg_namespace AS ion2 ON ion2.nspname = eo2.nspname " + "LEFT JOIN pg_namespace AS ion3 ON ion3.nspname = eo3.nspname " + "LEFT JOIN pg_namespace AS ion4 ON ion4.nspname = eo4.nspname " + "LEFT JOIN pg_namespace AS ion5 ON ion5.nspname = eo5.nspname " + "LEFT JOIN pg_namespace AS icn1 ON icn1.nspname = ec1.nspname " + "LEFT JOIN pg_namespace AS icn2 ON icn2.nspname = ec2.nspname " + "LEFT JOIN pg_namespace AS icn3 ON icn3.nspname = ec3.nspname " + "LEFT JOIN pg_namespace AS icn4 ON icn4.nspname = ec4.nspname " + "LEFT JOIN pg_namespace AS icn5 ON icn5.nspname = ec5.nspname " + "LEFT JOIN pg_operator AS io1 ON io1.oprnamespace = ion1.oid " + " AND io1.oprname = eo1.oprname " + " AND io1.oprleft = pga.atttypid " + " AND io1.oprright = pga.atttypid " + "LEFT JOIN pg_operator AS io2 ON io2.oprnamespace = ion2.oid " + " AND io2.oprname = eo2.oprname " + " AND io2.oprleft = pga.atttypid " + " AND io2.oprright = pga.atttypid " + "LEFT JOIN pg_operator AS io3 ON io3.oprnamespace = ion3.oid " + " AND io3.oprname = eo3.oprname " + " AND io3.oprleft = pga.atttypid " + " AND io3.oprright = pga.atttypid " + "LEFT JOIN pg_operator AS io4 ON io4.oprnamespace = ion4.oid " + " AND io4.oprname = eo4.oprname " + " AND io4.oprleft = pga.atttypid " + " AND io4.oprright = pga.atttypid " + "LEFT JOIN pg_operator AS io5 ON io5.oprnamespace = ion5.oid " + " AND io5.oprname = eo5.oprname " + " AND io5.oprleft = pga.atttypid " + " AND io5.oprright = pga.atttypid " + "LEFT JOIN pg_collation as ic1 " + " ON ic1.collnamespace = icn1.oid AND ic1.collname = ec1.collname " + "LEFT JOIN pg_collation as ic2 " + " ON ic2.collnamespace = icn2.oid AND ic2.collname = ec2.collname " + "LEFT JOIN pg_collation as ic3 " + " ON ic3.collnamespace = icn3.oid AND ic3.collname = ec3.collname " + "LEFT JOIN pg_collation as ic4 " + " ON ic4.collnamespace = icn4.oid AND ic4.collname = ec4.collname " + "LEFT JOIN pg_collation as ic5 " + " ON ic5.collnamespace = icn5.oid AND ic5.collname = ec5.collname " + "WHERE pga.attrelid = $6 " + "AND pga.attnum > 0 " + "ORDER BY pga.attnum, coalesce(es.stainherit, false)"; + + /* + * Columns with names containing _EXP_ are values that come from exported + * json data and therefore should not be directly imported into + * pg_statistic. Those values were joined to current catalog values to + * derive the proper value to import, and the column is exposed mostly + * for validation purposes. + */ + enum + { + PGS_ATTNUM = 0, + PGS_ATTNAME, + PGS_ATTTYPID, + PGS_ATTTYPMOD, + PGS_ATTCOLLATION, + PGS_TYPNAME, + PGS_COLLNAME, + PGS_EXP_ATTNUM, + PGS_EXP_ATTTYPID, + PGS_EXP_ATTCOLLATION, + PGS_EXP_TYPNAME, + PGS_EXP_TYPSCHEMA, + PGS_EXP_COLLNAME, + PGS_EXP_COLLSCHEMA, + PGS_STAINHERIT, + PGS_STANULLFRAC, + PGS_STAWIDTH, + PGS_STADISTINCT, + PGS_STAKIND1, + PGS_STAKIND2, + PGS_STAKIND3, + PGS_STAKIND4, + PGS_STAKIND5, + PGS_EXP_STAOP1, + PGS_EXP_STAOP2, + PGS_EXP_STAOP3, + PGS_EXP_STAOP4, + PGS_EXP_STAOP5, + PGS_EXP_STACOLL1, + PGS_EXP_STACOLL2, + PGS_EXP_STACOLL3, + PGS_EXP_STACOLL4, + PGS_EXP_STACOLL5, + PGS_STANUMBERS1, + PGS_STANUMBERS2, + PGS_STANUMBERS3, + PGS_STANUMBERS4, + PGS_STANUMBERS5, + PGS_STAVALUES1, + PGS_STAVALUES2, + PGS_STAVALUES3, + PGS_STAVALUES4, + PGS_STAVALUES5, + PGS_EXP_OPRSCHEMA1, + PGS_EXP_OPRSCHEMA2, + PGS_EXP_OPRSCHEMA3, + PGS_EXP_OPRSCHEMA4, + PGS_EXP_OPRSCHEMA5, + PGS_EXP_OPRNAME1, + PGS_EXP_OPRNAME2, + PGS_EXP_OPRNAME3, + PGS_EXP_OPRNAME4, + PGS_EXP_OPRNAME5, + PGS_STAOP1, + PGS_STAOP2, + PGS_STAOP3, + PGS_STAOP4, + PGS_STAOP5, + PGS_EXP_COLLSCHEMA1, + PGS_EXP_COLLSCHEMA2, + PGS_EXP_COLLSCHEMA3, + PGS_EXP_COLLSCHEMA4, + PGS_EXP_COLLSCHEMA5, + PGS_EXP_COLLNAME1, + PGS_EXP_COLLNAME2, + PGS_EXP_COLLNAME3, + PGS_EXP_COLLNAME4, + PGS_EXP_COLLNAME5, + PGS_STACOLL1, + PGS_STACOLL2, + PGS_STACOLL3, + PGS_STACOLL4, + PGS_STACOLL5, + PGS_ATTNAME_MISS, + PGS_STAATTNUM_MISS, + PGS_DUP_COUNT, + NUM_PGS_COLS + }; + + AttrInfo *relattrinfo = get_attrinfo(rel); + AttrInfo *attrinfo; + + int ret; + int i; + int tupctr = 0; + + SPITupleTable *tuptable; + HeapTuple *rettuples; + + ret = SPI_execute_with_args(sql, PGS_NARGS, argtypes, args, argnulls, true, 0); + + if (ret != SPI_OK_SELECT) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("statistic export JSON is not in proper format"))); + + tuptable = SPI_tuptable; + + rettuples = palloc0(sizeof(HeapTuple) * tuptable->numvals); + + for (i = 0; i < tuptable->numvals; i++) + { + Datum pgs_datums[NUM_PGS_COLS]; + bool pgs_nulls[NUM_PGS_COLS]; + bool skip = false; + + Datum values[Natts_pg_statistic] = { 0 }; + bool nulls[Natts_pg_statistic] = { false }; + + int dup_count; + AttrNumber attnum; + char *attname; + bool stainherit; + char *inhstr; + AttrNumber exported_attnum; + FmgrInfo finfo; + int k; + + heap_deform_tuple(tuptable->vals[i], tuptable->tupdesc, pgs_datums, + pgs_nulls); + + /* + * Check all the columns that cannot plausibly be null regardless of + * json data quality + */ + Assert(!pgs_nulls[PGS_ATTNUM]); + Assert(!pgs_nulls[PGS_ATTNAME]); + Assert(!pgs_nulls[PGS_ATTTYPID]); + Assert(!pgs_nulls[PGS_ATTTYPMOD]); + Assert(!pgs_nulls[PGS_ATTCOLLATION]); + Assert(!pgs_nulls[PGS_TYPNAME]); + Assert(!pgs_nulls[PGS_DUP_COUNT]); + Assert(!pgs_nulls[PGS_ATTNAME_MISS]); + Assert(!pgs_nulls[PGS_STAATTNUM_MISS]); + + attnum = DatumGetInt16(pgs_datums[PGS_ATTNUM]); + attname = NameStr(*(DatumGetName(pgs_datums[PGS_ATTNAME]))); + attrinfo = &relattrinfo[attnum - 1]; + + fmgr_info(F_ARRAY_IN, &finfo); + + if (pgs_nulls[PGS_STAINHERIT]) + { + stainherit = false; + inhstr = "NULL"; + } + else if (DatumGetBool(pgs_datums[PGS_STAINHERIT])) + { + stainherit = true; + inhstr = "true"; + } + else + { + stainherit = false; + inhstr = "false"; + } + + /* + * Any duplicates would be a cache collision and a sign that the + * import json is broken. + */ + dup_count = DatumGetInt32(pgs_datums[PGS_DUP_COUNT]); + if (dup_count != 1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Attribute duplicate count %d on attnum %d attname %s stainherit %s", + dup_count, attnum, attname, stainherit ? "t" : "f"))); + else if (DatumGetBool(pgs_datums[PGS_ATTNAME_MISS])) + { + /* Do not generate a tuple */ + skip = true; + if (require_match_oids) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("No exported attribute with name \"%s\" found.", attname))); + } + else if (DatumGetBool(pgs_datums[PGS_STAATTNUM_MISS])) + { + /* Do not generate a tuple */ + skip = true; + if (require_match_oids) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("No exported statistic found for exported attribute \"%s\" found.", + attname))); + } + + /* if we are going to skip this row, clean up first */ + if (skip) + { + pfree(attname); + continue; + } + + exported_attnum = DatumGetInt16(pgs_datums[PGS_EXP_ATTNUM]); + + if (require_match_oids) + { + Oid export_typoid = DatumGetObjectId(pgs_datums[PGS_EXP_ATTTYPID]); + Oid catalog_typoid = DatumGetObjectId(pgs_datums[PGS_ATTTYPID]); + + if (export_typoid != catalog_typoid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Attribute %d expects typoid %u but typoid %u imported", + attnum, catalog_typoid, export_typoid))); + } + + values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(RelationGetRelid(rel)); + values[Anum_pg_statistic_staattnum - 1] = pgs_datums[PGS_ATTNUM]; + values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(stainherit); + + /* + * Any nulls here will fail the when it is written to pg_statistic + * but that error message is as good as any we could create. + */ + if (pgs_nulls[PGS_STANULLFRAC]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Imported Statistics Attribute %d stainherit %s cannot have NULL %s", + exported_attnum, inhstr, "stanullfrac"))); + + if (pgs_nulls[PGS_STAWIDTH]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Imported Statistics Attribute %d stainherit %s cannot have NULL %s", + exported_attnum, inhstr, "stawidth"))); + + if (pgs_nulls[PGS_STADISTINCT]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Imported Statistics Attribute %d stainherit %s cannot have NULL %s", + exported_attnum, inhstr, "stadistinct"))); + + values[Anum_pg_statistic_stanullfrac - 1] = pgs_datums[PGS_STANULLFRAC]; + values[Anum_pg_statistic_stawidth - 1] = pgs_datums[PGS_STAWIDTH]; + values[Anum_pg_statistic_stadistinct - 1] = pgs_datums[PGS_STADISTINCT]; + + for (k = 0; k < STATISTIC_NUM_SLOTS; k++) + { + int16 kind; + Oid op; + + /* + * stakindN + * + * We can't match order of stakinds from VacAttrStats because which + * entries appear varies by the data in the table. + * + * The stakindN values assigned during ANALYZE will vary by the + * amount and quality of the data sampled. As such, there is no + * fixed set of kinds to match against for any one slot. + * + * Any NULL stakindN values will cause the row to fail. + * + */ + if (pgs_nulls[PGS_STAKIND1 + k]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Imported Statistics Attribute %d stainherit %s cannot have NULL %s%d", + exported_attnum, inhstr, "stakind", k+1))); + + values[Anum_pg_statistic_stakind1 - 1 + k] = pgs_datums[PGS_STAKIND1 + k]; + kind = DatumGetInt16(pgs_datums[PGS_STAKIND1 + k]); + + /* + * staopN + * + * We cannot resolve the exported operator back to a local Oid because + * that cannot be looked up directly in the catalog, so we have to + * instead look at the exported operator name, choose the op from + * the typecache, and then if we're requiring matching oids we can + * compare that to the exported oid. + * + */ + /* Possibly validate operator must be OidIsValid when stakindN <> 0 */ + if (pgs_nulls[PGS_EXP_OPRNAME1 + k]) + op = InvalidOid; + else + { + char *exp_oprname; + + exp_oprname = TextDatumGetCString(pgs_datums[PGS_EXP_OPRNAME1 + k]); + if (strcmp(exp_oprname, "=") == 0) + { + /* + * MCELEM stat arrays are of the same type as the + * array base element type and are eqopr + */ + if ((kind == STATISTIC_KIND_MCELEM) || + (kind == STATISTIC_KIND_DECHIST)) + op = attrinfo->baseeqopr; + else + op = attrinfo->eqopr; + } + else if (strcmp(exp_oprname, "<") == 0) + op = attrinfo->ltopr; + else + op = InvalidOid; + pfree(exp_oprname); + } + + if (require_match_oids) + { + if (pgs_nulls[PGS_EXP_STAOP1 + k]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Attribute %d staop%d kind %d expects Oid %u but NULL imported", + attnum, k+1, kind, op))); + else + { + Oid export_op = DatumGetObjectId(pgs_datums[PGS_EXP_STAOP1 + k]); + if (export_op != op) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Attribute %d staop%d kind %d expects Oid %u but Oid %u imported", + attnum, k+1, kind, op, export_op))); + } + } + values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(op); + + /* Any NULL stacollN will fail the row */ + if (pgs_nulls[PGS_STACOLL1 + k]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Imported Statistics Attribute %d stainherit %s cannot have NULL %s%d", + exported_attnum, inhstr, "stacoll", k+1))); + values[Anum_pg_statistic_stacoll1 - 1 + k] = pgs_datums[PGS_STACOLL1 + k]; + + if (require_match_oids) + { + Oid export_coll = DatumGetObjectId(pgs_datums[PGS_EXP_STACOLL1 + k]); + Oid import_coll = DatumGetObjectId(pgs_datums[PGS_STACOLL1 + k]); + + if (export_coll != import_coll) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Attribute %d stacoll%d expects Oid %u but Oid %u imported", + attnum, k+1, export_coll, import_coll))); + } + + /* stanumbersN - the import query did the required type coercion. */ + values[Anum_pg_statistic_stanumbers1 - 1 + k] = + pgs_datums[PGS_STANUMBERS1 + k]; + nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = + pgs_nulls[PGS_STANUMBERS1 + k]; + + /* stavaluesN */ + if (pgs_nulls[PGS_STAVALUES1 + k]) + { + nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true; + values[Anum_pg_statistic_stavalues1 - 1 + k] = (Datum) 0; + } + else + { + char *s = TextDatumGetCString(pgs_datums[PGS_STAVALUES1 + k]); + + values[Anum_pg_statistic_stavalues1 - 1 + k] = + FunctionCall3(&finfo, CStringGetDatum(s), + ObjectIdGetDatum(attrinfo->basetypid), + Int32GetDatum(attrinfo->typmod)); + + pfree(s); + } + } + + /* Add valid tuple to the list */ + rettuples[tupctr++] = heap_form_tuple(RelationGetDescr(sd), values, nulls); + } + + pfree(relattrinfo); + *ntuples = tupctr; + return rettuples; +} + +/* + * Import statistics for a given relation. + * + * The statistics json format is: + * + * { + * "server_version_num": number, -- SHOW server_version on source system + * "relname": string, -- pgclass.relname of the exported relation + * "nspname": string, -- schema name for the exported relation + * "reltuples": number, -- pg_class.reltuples + * "relpages": number, -- pg_class.relpages + * "types": [ + * -- export of all pg_type referenced in this json doc + * { + * "oid": number, -- pg_type.oid + * "typname": string, -- pg_type.typname + * "nspname": string -- schema name for the pg_type + * } + * ], + * "collations": [ + * -- export all pg_collation reference in this json doc + * { + * "oid": number, -- pg_collation.oid + * "collname": string, -- pg_collation.collname + * "nspname": string -- schema name for the pg_collation + * } + * ], + * "operators": [ + * -- export all pg_operator reference in this json doc + * { + * "oid": number, -- pg_operator.oid + * "collname": string, -- pg_oprname + * "nspname": string -- schema name for the pg_operator + * } + * ], + * "attributes": [ + * -- export all pg_attribute for the exported relation + * { + * "attnum": number, -- pg_attribute.attnum + * "attname": string, -- pg_attribute.attname + * "atttypid": number, -- pg_attribute.atttypid + * "attcollation": number -- pg_attribute.attcollation + * } + * ], + * "statistics": [ + * -- export all pg_statistic for the exported relation + * { + * "staattnum": number, -- pg_statistic.staattnum + * "stainherit": bool, -- pg_statistic.stainherit + * "stanullfrac": number, -- pg_statistic.stanullfrac + * "stawidth": number, -- pg_statistic.stawidth + * "stadistinct": number, -- pg_statistic.stadistinct + * "stakind1": number, -- pg_statistic.stakind1 + * "stakind2": number, -- pg_statistic.stakind2 + * "stakind3": number, -- pg_statistic.stakind3 + * "stakind4": number, -- pg_statistic.stakind4 + * "stakind5": number, -- pg_statistic.stakind5 + * "staop1": number, -- pg_statistic.staop1 + * "staop2": number, -- pg_statistic.staop2 + * "staop3": number, -- pg_statistic.staop3 + * "staop4": number, -- pg_statistic.staop4 + * "staop5": number, -- pg_statistic.staop5 + * "stacoll1": number, -- pg_statistic.stacoll1 + * "stacoll2": number, -- pg_statistic.stacoll2 + * "stacoll3": number, -- pg_statistic.stacoll3 + * "stacoll4": number, -- pg_statistic.stacoll4 + * "stacoll5": number, -- pg_statistic.stacoll5 + * -- stanumbersN are cast to string to aid array_in() + * "stanumbers1": string, -- pg_statistic.stanumbers1::text + * "stanumbers2": string, -- pg_statistic.stanumbers2::text + * "stanumbers3": string, -- pg_statistic.stanumbers3::text + * "stanumbers4": string, -- pg_statistic.stanumbers4::text + * "stanumbers5": string, -- pg_statistic.stanumbers5::text + * -- stavaluesN are cast to string to aid array_in() + * "stavalues1": string, -- pg_statistic.stavalues1::text + * "stavalues2": string, -- pg_statistic.stavalues2::text + * "stavalues3": string, -- pg_statistic.stavalues3::text + * "stavalues4": string, -- pg_statistic.stavalues4::text + * "stavalues5": string -- pg_statistic.stavalues5::text + * } + * ] + * } + * + * Each server verion exports a subset of this format. The exported format + * can and will change with each new version, and this function will have + * to account for those variations. + + */ +Datum +pg_import_rel_stats(PG_FUNCTION_ARGS) +{ + Oid relid; + bool validate; + bool require_match_oids; + + const char *sql = + "SELECT current_setting('server_version_num') AS current_version, eb.* " + "FROM jsonb_to_record($1) AS eb( " + " server_version_num integer, " + " relname text, " + " nspname text, " + " reltuples float4," + " relpages int4, " + " types jsonb, " + " collations jsonb, " + " operators jsonb, " + " attributes jsonb, " + " statistics jsonb) "; + + enum + { + BQ_CURRENT_VERSION_NUM = 0, + BQ_SERVER_VERSION_NUM, + BQ_RELNAME, + BQ_NSPNAME, + BQ_RELTUPLES, + BQ_RELPAGES, + BQ_TYPES, + BQ_COLLATIONS, + BQ_OPERATORS, + BQ_ATTRIBUTES, + BQ_STATISTICS, + NUM_BQ_COLS + }; + +#define BQ_NARGS 1 + + Oid argtypes[BQ_NARGS] = { JSONBOID }; + Datum args[BQ_NARGS]; + + int ret; + + SPITupleTable *tuptable; + + Datum datums[NUM_BQ_COLS]; + bool nulls[NUM_BQ_COLS]; + + int32 server_version_num; + int32 current_version_num; + + Relation rel; + Relation sd; + HeapTuple *sdtuples; + int nsdtuples; + int i; + + CatalogIndexState indstate = NULL; + + 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)) + PG_RETURN_BOOL(false); + args[0] = PG_GETARG_DATUM(1); + + if (PG_ARGISNULL(2)) + validate = false; + else + validate = PG_GETARG_BOOL(2); + + if (PG_ARGISNULL(3)) + require_match_oids = false; + else + require_match_oids = PG_GETARG_BOOL(3); + + /* + * Connect to SPI manager + */ + if ((ret = SPI_connect()) < 0) + elog(ERROR, "SPI connect failure - returned %d", ret); + + /* + * Fetch the base level of the stats json. The results found there will + * determine how the nested data will be handled. + */ + ret = SPI_execute_with_args(sql, BQ_NARGS, argtypes, args, NULL, true, 1); + + /* + * Only allow one qualifying tuple + */ + if (ret != SPI_OK_SELECT) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("statistic export JSON is not in proper format"))); + + if (SPI_processed > 1) + ereport(ERROR, + (errcode(ERRCODE_CARDINALITY_VIOLATION), + errmsg("statistic export JSON should return only one base object"))); + + tuptable = SPI_tuptable; + heap_deform_tuple(tuptable->vals[0], tuptable->tupdesc, datums, nulls); + + /* + * Check for valid combination of exported server_version_num to the local + * server_version_num. We won't be reusing these values in a query so use + * scratch datum/null vars. + */ + if (nulls[BQ_CURRENT_VERSION_NUM]) + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("current_version_num cannot be null"))); + + if (nulls[BQ_SERVER_VERSION_NUM]) + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("server_version_num cannot be null"))); + + current_version_num = DatumGetInt32(datums[BQ_CURRENT_VERSION_NUM]); + server_version_num = DatumGetInt32(datums[BQ_SERVER_VERSION_NUM]); + + if (server_version_num <= 100000) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("Cannot import statistics from servers below version 10.0"))); + + if (server_version_num > current_version_num) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("Cannot import statistics from server version %d to %d", + server_version_num, current_version_num))); + + rel = relation_open(relid, ShareUpdateExclusiveLock); + + if (require_match_oids) + { + char *curr_relname = SPI_getrelname(rel); + char *curr_nspname = SPI_getnspname(rel); + char *import_relname; + char *import_nspname; + + if (nulls[BQ_RELNAME]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Imported Relation Name must match relation name, but is null"))); + + if (nulls[BQ_NSPNAME]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Imported Relation Schema Name must match schema name, but is null"))); + + import_relname = TextDatumGetCString(datums[BQ_RELNAME]); + import_nspname = TextDatumGetCString(datums[BQ_NSPNAME]); + + if (strcmp(import_relname, curr_relname) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Imported Relation Name (%s) must match relation name (%s), but does not", + import_relname, curr_relname))); + + if (strcmp(import_nspname, curr_nspname) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Imported Relation Schema Name (%s) must match schema name (%s), but does not", + import_nspname, curr_nspname))); + + pfree(curr_relname); + pfree(curr_nspname); + pfree(import_relname); + pfree(import_nspname); + } + + /* + * validations + * + * Potential future validations: + * + * * all attributes.atttypid values are represented in "types" + * * all attributes.attcollation values are represented in "types" + * * attributes.attname is of acceptable length + * * all non-invalid statistics.opN values are represented in "operators" + * * all non-invalid statistics.collN values are represented in "collations" + * * statistincs.kindN values in 0-7 + * * statistics.stanullfrac in range + * * statistics.stawidth in range + * * statistics.ndistinct in rage + * + */ + if (validate) + { + validate_exported_types(datums[BQ_TYPES], nulls[BQ_TYPES]); + validate_exported_collations(datums[BQ_COLLATIONS], nulls[BQ_COLLATIONS]); + validate_exported_operators(datums[BQ_OPERATORS], nulls[BQ_OPERATORS]); + validate_exported_attributes(datums[BQ_ATTRIBUTES], nulls[BQ_ATTRIBUTES]); + validate_exported_statistics(datums[BQ_STATISTICS], nulls[BQ_STATISTICS]); + } + + sd = table_open(StatisticRelationId, RowExclusiveLock); + + sdtuples = import_pg_statistics(rel, sd, server_version_num, + datums[BQ_TYPES], nulls[BQ_TYPES], + datums[BQ_COLLATIONS], nulls[BQ_COLLATIONS], + datums[BQ_OPERATORS], nulls[BQ_OPERATORS], + datums[BQ_ATTRIBUTES], nulls[BQ_ATTRIBUTES], + datums[BQ_STATISTICS], nulls[BQ_STATISTICS], + require_match_oids, &nsdtuples); + + /* Open index information when we know we need it */ + indstate = CatalogOpenIndexes(sd); + + /* Delete existing pg_statistic rows for relation to avoid collisions */ + remove_pg_statistics(rel, sd, false); + if (RELKIND_HAS_PARTITIONS(rel->rd_rel->relkind)) + remove_pg_statistics(rel, sd, true); + + for (i = 0; i < nsdtuples; i++) + { + CatalogTupleInsertWithInfo(sd, sdtuples[i], indstate); + heap_freetuple(sdtuples[i]); + } + + CatalogCloseIndexes(indstate); + table_close(sd, RowExclusiveLock); + pfree(sdtuples); + + /* + * Update pg_class tuple directly (non-transactionally, same as + * is done in do_analyze(). + * + * Only modify pg_class row if changes are to be made + */ + if (!nulls[BQ_RELTUPLES] || !nulls[BQ_RELPAGES]) + { + 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 (!nulls[BQ_RELTUPLES]) + pgcform->reltuples = DatumGetFloat4(datums[BQ_RELTUPLES]); + + if(!nulls[BQ_RELPAGES]) + pgcform->relpages = DatumGetInt32(datums[BQ_RELPAGES]); + + heap_inplace_update(pg_class_rel, ctup); + table_close(pg_class_rel, ShareUpdateExclusiveLock); + } + + relation_close(rel, NoLock); + + SPI_finish(); + + 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..5ab51c5aa0 --- /dev/null +++ b/src/test/regress/expected/stats_export_import.out @@ -0,0 +1,530 @@ +-- set to 't' to see debug output +\set debug f +CREATE SCHEMA stats_export_import; +CREATE TYPE stats_export_import.complex_type AS ( + a integer, + b float, + c text, + d date, + e jsonb); +CREATE TABLE stats_export_import.test( + id INTEGER PRIMARY KEY, + name text, + comp stats_export_import.complex_type, + tags text[] +); +INSERT INTO stats_export_import.test +SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, array['red','green'] +UNION ALL +SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type, array['blue','yellow'] +UNION ALL +SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, array['"orange"', 'purple', 'cyan'] +UNION ALL +SELECT 4, 'four', NULL, NULL; +CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1)); +-- Generate statistics on table with data +ANALYZE stats_export_import.test; +-- Capture pg_statistic values for table and index +CREATE TABLE stats_export_import.pg_statistic_capture +AS +SELECT 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::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3, + stavalues4::text AS sv4, stavalues5::text AS sv5 +FROM pg_statistic +WHERE starelid IN ('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass); +SELECT COUNT(*) +FROM stats_export_import.pg_statistic_capture; + count +------- + 5 +(1 row) + +-- Export stats +SELECT + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', r.relname, + 'nspname', n.nspname, + 'reltuples', r.reltuples, + 'relpages', r.relpages, + 'types', + ( + SELECT array_agg(tr ORDER BY tr.oid) + FROM ( + SELECT + t.oid, + t.typname, + n.nspname + FROM pg_type AS t + JOIN pg_namespace AS n ON n.oid = t.typnamespace + WHERE t.oid IN ( + SELECT a.atttypid + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + ) + ) AS tr + ), + 'collations', + ( + SELECT array_agg(cr ORDER BY cr.oid) + FROM ( + SELECT + c.oid, + c.collname, + n.nspname + FROM pg_collation AS c + JOIN pg_namespace AS n ON n.oid = c.collnamespace + WHERE c.oid IN ( + SELECT a.attcollation AS oid + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + UNION + SELECT u.collid + FROM pg_statistic AS s + CROSS JOIN LATERAL unnest(ARRAY[ + s.stacoll1, s.stacoll2, + s.stacoll3, s.stacoll4, + s.stacoll5]) AS u(collid) + WHERE s.starelid = r.oid + ) + ) AS cr + ), + 'operators', + ( + SELECT array_agg(p ORDER BY p.oid) + FROM ( + SELECT + o.oid, + o.oprname, + n.nspname + FROM pg_operator AS o + JOIN pg_namespace AS n ON n.oid = o.oprnamespace + WHERE o.oid IN ( + SELECT u.oid + FROM pg_statistic AS s + CROSS JOIN LATERAL unnest(ARRAY[ + s.staop1, s.staop2, + s.staop3, s.staop4, + s.staop5]) AS u(opid) + WHERE s.starelid = r.oid + ) + ) AS p + ), + 'attributes', + ( + SELECT array_agg(ar ORDER BY ar.attnum) + FROM ( + SELECT + a.attnum, + a.attname, + a.atttypid, + a.attcollation + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + ) AS ar + ), + 'statistics', + ( + SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum) + FROM ( + SELECT + s.staattnum, + s.stainherit, + s.stanullfrac, + s.stawidth, + s.stadistinct, + s.stakind1, + s.stakind2, + s.stakind3, + s.stakind4, + s.stakind5, + s.staop1, + s.staop2, + s.staop3, + s.staop4, + s.staop5, + s.stacoll1, + s.stacoll2, + s.stacoll3, + s.stacoll4, + s.stacoll5, + s.stanumbers1::text AS stanumbers1, + s.stanumbers2::text AS stanumbers2, + s.stanumbers3::text AS stanumbers3, + s.stanumbers4::text AS stanumbers4, + s.stanumbers5::text AS stanumbers5, + s.stavalues1::text AS stavalues1, + s.stavalues2::text AS stavalues2, + s.stavalues3::text AS stavalues3, + s.stavalues4::text AS stavalues4, + s.stavalues5::text AS stavalues5 + FROM pg_statistic AS s + WHERE s.starelid = r.oid + ) AS sr + ) + ) AS table_stats_json +FROM pg_class AS r +JOIN pg_namespace AS n ON n.oid = r.relnamespace +WHERE r.oid = 'stats_export_import.test'::regclass +\gset +SELECT jsonb_pretty(:'table_stats_json'::jsonb) AS table_stats_json +WHERE :'debug'::boolean; + table_stats_json +------------------ +(0 rows) + +SELECT + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', r.relname, + 'nspname', n.nspname, + 'reltuples', r.reltuples, + 'relpages', r.relpages, + 'types', + ( + SELECT array_agg(tr ORDER BY tr.oid) + FROM ( + SELECT + t.oid, + t.typname, + n.nspname + FROM pg_type AS t + JOIN pg_namespace AS n ON n.oid = t.typnamespace + WHERE t.oid IN ( + SELECT a.atttypid + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + ) + ) AS tr + ), + 'collations', + ( + SELECT array_agg(cr ORDER BY cr.oid) + FROM ( + SELECT + c.oid, + c.collname, + n.nspname + FROM pg_collation AS c + JOIN pg_namespace AS n ON n.oid = c.collnamespace + WHERE c.oid IN ( + SELECT a.attcollation AS oid + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + UNION + SELECT u.collid + FROM pg_statistic AS s + CROSS JOIN LATERAL unnest(ARRAY[ + s.stacoll1, s.stacoll2, + s.stacoll3, s.stacoll4, + s.stacoll5]) AS u(collid) + WHERE s.starelid = r.oid + ) + ) AS cr + ), + 'operators', + ( + SELECT array_agg(p ORDER BY p.oid) + FROM ( + SELECT + o.oid, + o.oprname, + n.nspname + FROM pg_operator AS o + JOIN pg_namespace AS n ON n.oid = o.oprnamespace + WHERE o.oid IN ( + SELECT u.oid + FROM pg_statistic AS s + CROSS JOIN LATERAL unnest(ARRAY[ + s.staop1, s.staop2, + s.staop3, s.staop4, + s.staop5]) AS u(opid) + WHERE s.starelid = r.oid + ) + ) AS p + ), + 'attributes', + ( + SELECT array_agg(ar ORDER BY ar.attnum) + FROM ( + SELECT + a.attnum, + a.attname, + a.atttypid, + a.attcollation + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + ) AS ar + ), + 'statistics', + ( + SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum) + FROM ( + SELECT + s.staattnum, + s.stainherit, + s.stanullfrac, + s.stawidth, + s.stadistinct, + s.stakind1, + s.stakind2, + s.stakind3, + s.stakind4, + s.stakind5, + s.staop1, + s.staop2, + s.staop3, + s.staop4, + s.staop5, + s.stacoll1, + s.stacoll2, + s.stacoll3, + s.stacoll4, + s.stacoll5, + s.stanumbers1::text AS stanumbers1, + s.stanumbers2::text AS stanumbers2, + s.stanumbers3::text AS stanumbers3, + s.stanumbers4::text AS stanumbers4, + s.stanumbers5::text AS stanumbers5, + s.stavalues1::text AS stavalues1, + s.stavalues2::text AS stavalues2, + s.stavalues3::text AS stavalues3, + s.stavalues4::text AS stavalues4, + s.stavalues5::text AS stavalues5 + FROM pg_statistic AS s + WHERE s.starelid = r.oid + ) AS sr + ) + ) AS index_stats_json +FROM pg_class AS r +JOIN pg_namespace AS n ON n.oid = r.relnamespace +WHERE r.oid = 'stats_export_import.is_odd'::regclass +\gset +SELECT jsonb_pretty(:'index_stats_json'::jsonb) AS index_stats_json +WHERE :'debug'::boolean; + index_stats_json +------------------ +(0 rows) + +SELECT relname, reltuples +FROM pg_class +WHERE oid IN ('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass) +ORDER BY relname; + relname | reltuples +---------+----------- + is_odd | 4 + test | 4 +(2 rows) + +-- Move table and index out of the way +ALTER TABLE stats_export_import.test RENAME TO test_orig; +ALTER INDEX stats_export_import.is_odd RENAME TO is_odd_orig; +-- Create empty copy tables +CREATE TABLE stats_export_import.test(LIKE stats_export_import.test_orig); +CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1)); +-- Verify no stats for these new tables +SELECT COUNT(*) +FROM pg_statistic +WHERE starelid IN('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass); + count +------- + 0 +(1 row) + +SELECT relname, reltuples +FROM pg_class +WHERE oid IN ('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass) +ORDER BY relname; + relname | reltuples +---------+----------- + is_odd | 0 + test | -1 +(2 rows) + +-- Test valiation +SELECT + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', 'test', + 'nspname', 'stats_export_import', + 'types', + ( + SELECT array_agg(r) + FROM ( + SELECT 1 AS oid, 'type1' AS typname + UNION ALL + SELECT 2 AS oid, 'type2' AS typname + UNION ALL + SELECT 2 AS oid, 'type3' AS typname + ) AS r + )) AS invalid_types_doc, + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', 'test', + 'nspname', 'stats_export_import', + 'collations', + ( + SELECT array_agg(r) + FROM ( + SELECT 1 AS oid, 'coll1' AS collname + UNION ALL + SELECT 1 AS oid, 'coll2' AS collname + UNION ALL + SELECT 2 AS oid, 'coll3' AS collname + ) AS r + )) AS invalid_collations_doc, + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', 'test', + 'nspname', 'stats_export_import', + 'operators', + ( + SELECT array_agg(r) + FROM ( + SELECT 1 AS oid, 'opr1' AS oprname + UNION ALL + SELECT 3 AS oid, 'opr2' AS oprname + UNION ALL + SELECT 3 AS oid, 'opr3' AS oprname + ) AS r + )) AS invalid_operators_doc, + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', 'test', + 'nspname', 'stats_export_import', + 'attributes', + ( + SELECT array_agg(r) + FROM ( + SELECT 1 AS attnum, 'col1' AS attname + UNION ALL + SELECT 4 AS attnum, 'col2' AS attname + UNION ALL + SELECT 4 AS attnum, 'col3' AS attname + ) AS r + )) AS invalid_attributes_doc, + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', 'test', + 'nspname', 'stats_export_import', + 'statistics', + ( + SELECT array_agg(r) + FROM ( + SELECT 1 AS staattnum, false AS stainherit + UNION ALL + SELECT 5 AS staattnum, true AS stainherit + UNION ALL + SELECT 1 AS staattnum, false AS stainherit + ) AS r + )) AS invalid_statistics_doc +\gset +SELECT pg_import_rel_stats('stats_export_import.test'::regclass, + :'invalid_types_doc'::jsonb, true, true); +ERROR: statistic export JSON document "types" has duplicate rows with oid = 2 +SELECT pg_import_rel_stats('stats_export_import.test'::regclass, + :'invalid_collations_doc'::jsonb, true, true); +ERROR: statistic export JSON document "collations" has duplicate rows with oid = 1 +SELECT pg_import_rel_stats('stats_export_import.test'::regclass, + :'invalid_operators_doc'::jsonb, true, true); +ERROR: statistic export JSON document "operators" has duplicate rows with oid = 3 +SELECT pg_import_rel_stats('stats_export_import.test'::regclass, + :'invalid_attributes_doc'::jsonb, true, true); +ERROR: statistic export JSON document "attributes" has duplicate rows with attnum = 4 +SELECT pg_import_rel_stats('stats_export_import.test'::regclass, + :'invalid_statistics_doc'::jsonb, true, true); +ERROR: statistic export JSON document "statistics" has duplicate rows with staattnum = 1, stainherit = f +-- Import stats +SELECT pg_import_rel_stats( + 'stats_export_import.test'::regclass, + :'table_stats_json'::jsonb, + true, + true); + pg_import_rel_stats +--------------------- + t +(1 row) + +SELECT pg_import_rel_stats( + 'stats_export_import.is_odd'::regclass, + :'index_stats_json'::jsonb, + true, + true); + pg_import_rel_stats +--------------------- + t +(1 row) + +-- This should return 0 rows +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, + sv1, sv2, sv3, sv4, sv5 +FROM stats_export_import.pg_statistic_capture +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 +WHERE starelid IN ('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::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) + +-- This should return 0 rows +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 +WHERE starelid IN ('stats_export_import.test'::regclass, + 'stats_export_import.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, + sv1, sv2, sv3, sv4, sv5 +FROM stats_export_import.pg_statistic_capture; + 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) + +SELECT relname, reltuples +FROM pg_class +WHERE oid IN ('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass) +ORDER BY relname; + relname | reltuples +---------+----------- + is_odd | 4 + test | 4 +(2 rows) + diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 1d8a414eea..0c89ffc02d 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..9a80eebeec --- /dev/null +++ b/src/test/regress/sql/stats_export_import.sql @@ -0,0 +1,499 @@ +-- set to 't' to see debug output +\set debug f +CREATE SCHEMA stats_export_import; + +CREATE TYPE stats_export_import.complex_type AS ( + a integer, + b float, + c text, + d date, + e jsonb); + +CREATE TABLE stats_export_import.test( + id INTEGER PRIMARY KEY, + name text, + comp stats_export_import.complex_type, + tags text[] +); + +INSERT INTO stats_export_import.test +SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, array['red','green'] +UNION ALL +SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type, array['blue','yellow'] +UNION ALL +SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, array['"orange"', 'purple', 'cyan'] +UNION ALL +SELECT 4, 'four', NULL, NULL; + +CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1)); + +-- Generate statistics on table with data +ANALYZE stats_export_import.test; + +-- Capture pg_statistic values for table and index +CREATE TABLE stats_export_import.pg_statistic_capture +AS +SELECT 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::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3, + stavalues4::text AS sv4, stavalues5::text AS sv5 +FROM pg_statistic +WHERE starelid IN ('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass); + +SELECT COUNT(*) +FROM stats_export_import.pg_statistic_capture; + +-- Export stats +SELECT + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', r.relname, + 'nspname', n.nspname, + 'reltuples', r.reltuples, + 'relpages', r.relpages, + 'types', + ( + SELECT array_agg(tr ORDER BY tr.oid) + FROM ( + SELECT + t.oid, + t.typname, + n.nspname + FROM pg_type AS t + JOIN pg_namespace AS n ON n.oid = t.typnamespace + WHERE t.oid IN ( + SELECT a.atttypid + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + ) + ) AS tr + ), + 'collations', + ( + SELECT array_agg(cr ORDER BY cr.oid) + FROM ( + SELECT + c.oid, + c.collname, + n.nspname + FROM pg_collation AS c + JOIN pg_namespace AS n ON n.oid = c.collnamespace + WHERE c.oid IN ( + SELECT a.attcollation AS oid + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + UNION + SELECT u.collid + FROM pg_statistic AS s + CROSS JOIN LATERAL unnest(ARRAY[ + s.stacoll1, s.stacoll2, + s.stacoll3, s.stacoll4, + s.stacoll5]) AS u(collid) + WHERE s.starelid = r.oid + ) + ) AS cr + ), + 'operators', + ( + SELECT array_agg(p ORDER BY p.oid) + FROM ( + SELECT + o.oid, + o.oprname, + n.nspname + FROM pg_operator AS o + JOIN pg_namespace AS n ON n.oid = o.oprnamespace + WHERE o.oid IN ( + SELECT u.oid + FROM pg_statistic AS s + CROSS JOIN LATERAL unnest(ARRAY[ + s.staop1, s.staop2, + s.staop3, s.staop4, + s.staop5]) AS u(opid) + WHERE s.starelid = r.oid + ) + ) AS p + ), + 'attributes', + ( + SELECT array_agg(ar ORDER BY ar.attnum) + FROM ( + SELECT + a.attnum, + a.attname, + a.atttypid, + a.attcollation + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + ) AS ar + ), + 'statistics', + ( + SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum) + FROM ( + SELECT + s.staattnum, + s.stainherit, + s.stanullfrac, + s.stawidth, + s.stadistinct, + s.stakind1, + s.stakind2, + s.stakind3, + s.stakind4, + s.stakind5, + s.staop1, + s.staop2, + s.staop3, + s.staop4, + s.staop5, + s.stacoll1, + s.stacoll2, + s.stacoll3, + s.stacoll4, + s.stacoll5, + s.stanumbers1::text AS stanumbers1, + s.stanumbers2::text AS stanumbers2, + s.stanumbers3::text AS stanumbers3, + s.stanumbers4::text AS stanumbers4, + s.stanumbers5::text AS stanumbers5, + s.stavalues1::text AS stavalues1, + s.stavalues2::text AS stavalues2, + s.stavalues3::text AS stavalues3, + s.stavalues4::text AS stavalues4, + s.stavalues5::text AS stavalues5 + FROM pg_statistic AS s + WHERE s.starelid = r.oid + ) AS sr + ) + ) AS table_stats_json +FROM pg_class AS r +JOIN pg_namespace AS n ON n.oid = r.relnamespace +WHERE r.oid = 'stats_export_import.test'::regclass +\gset + +SELECT jsonb_pretty(:'table_stats_json'::jsonb) AS table_stats_json +WHERE :'debug'::boolean; + +SELECT + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', r.relname, + 'nspname', n.nspname, + 'reltuples', r.reltuples, + 'relpages', r.relpages, + 'types', + ( + SELECT array_agg(tr ORDER BY tr.oid) + FROM ( + SELECT + t.oid, + t.typname, + n.nspname + FROM pg_type AS t + JOIN pg_namespace AS n ON n.oid = t.typnamespace + WHERE t.oid IN ( + SELECT a.atttypid + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + ) + ) AS tr + ), + 'collations', + ( + SELECT array_agg(cr ORDER BY cr.oid) + FROM ( + SELECT + c.oid, + c.collname, + n.nspname + FROM pg_collation AS c + JOIN pg_namespace AS n ON n.oid = c.collnamespace + WHERE c.oid IN ( + SELECT a.attcollation AS oid + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + UNION + SELECT u.collid + FROM pg_statistic AS s + CROSS JOIN LATERAL unnest(ARRAY[ + s.stacoll1, s.stacoll2, + s.stacoll3, s.stacoll4, + s.stacoll5]) AS u(collid) + WHERE s.starelid = r.oid + ) + ) AS cr + ), + 'operators', + ( + SELECT array_agg(p ORDER BY p.oid) + FROM ( + SELECT + o.oid, + o.oprname, + n.nspname + FROM pg_operator AS o + JOIN pg_namespace AS n ON n.oid = o.oprnamespace + WHERE o.oid IN ( + SELECT u.oid + FROM pg_statistic AS s + CROSS JOIN LATERAL unnest(ARRAY[ + s.staop1, s.staop2, + s.staop3, s.staop4, + s.staop5]) AS u(opid) + WHERE s.starelid = r.oid + ) + ) AS p + ), + 'attributes', + ( + SELECT array_agg(ar ORDER BY ar.attnum) + FROM ( + SELECT + a.attnum, + a.attname, + a.atttypid, + a.attcollation + FROM pg_attribute AS a + WHERE a.attrelid = r.oid + AND NOT a.attisdropped + AND a.attnum > 0 + ) AS ar + ), + 'statistics', + ( + SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum) + FROM ( + SELECT + s.staattnum, + s.stainherit, + s.stanullfrac, + s.stawidth, + s.stadistinct, + s.stakind1, + s.stakind2, + s.stakind3, + s.stakind4, + s.stakind5, + s.staop1, + s.staop2, + s.staop3, + s.staop4, + s.staop5, + s.stacoll1, + s.stacoll2, + s.stacoll3, + s.stacoll4, + s.stacoll5, + s.stanumbers1::text AS stanumbers1, + s.stanumbers2::text AS stanumbers2, + s.stanumbers3::text AS stanumbers3, + s.stanumbers4::text AS stanumbers4, + s.stanumbers5::text AS stanumbers5, + s.stavalues1::text AS stavalues1, + s.stavalues2::text AS stavalues2, + s.stavalues3::text AS stavalues3, + s.stavalues4::text AS stavalues4, + s.stavalues5::text AS stavalues5 + FROM pg_statistic AS s + WHERE s.starelid = r.oid + ) AS sr + ) + ) AS index_stats_json +FROM pg_class AS r +JOIN pg_namespace AS n ON n.oid = r.relnamespace +WHERE r.oid = 'stats_export_import.is_odd'::regclass +\gset + +SELECT jsonb_pretty(:'index_stats_json'::jsonb) AS index_stats_json +WHERE :'debug'::boolean; + +SELECT relname, reltuples +FROM pg_class +WHERE oid IN ('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass) +ORDER BY relname; + +-- Move table and index out of the way +ALTER TABLE stats_export_import.test RENAME TO test_orig; +ALTER INDEX stats_export_import.is_odd RENAME TO is_odd_orig; + +-- Create empty copy tables +CREATE TABLE stats_export_import.test(LIKE stats_export_import.test_orig); +CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1)); + +-- Verify no stats for these new tables +SELECT COUNT(*) +FROM pg_statistic +WHERE starelid IN('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass); + +SELECT relname, reltuples +FROM pg_class +WHERE oid IN ('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass) +ORDER BY relname; + + +-- Test valiation +SELECT + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', 'test', + 'nspname', 'stats_export_import', + 'types', + ( + SELECT array_agg(r) + FROM ( + SELECT 1 AS oid, 'type1' AS typname + UNION ALL + SELECT 2 AS oid, 'type2' AS typname + UNION ALL + SELECT 2 AS oid, 'type3' AS typname + ) AS r + )) AS invalid_types_doc, + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', 'test', + 'nspname', 'stats_export_import', + 'collations', + ( + SELECT array_agg(r) + FROM ( + SELECT 1 AS oid, 'coll1' AS collname + UNION ALL + SELECT 1 AS oid, 'coll2' AS collname + UNION ALL + SELECT 2 AS oid, 'coll3' AS collname + ) AS r + )) AS invalid_collations_doc, + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', 'test', + 'nspname', 'stats_export_import', + 'operators', + ( + SELECT array_agg(r) + FROM ( + SELECT 1 AS oid, 'opr1' AS oprname + UNION ALL + SELECT 3 AS oid, 'opr2' AS oprname + UNION ALL + SELECT 3 AS oid, 'opr3' AS oprname + ) AS r + )) AS invalid_operators_doc, + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', 'test', + 'nspname', 'stats_export_import', + 'attributes', + ( + SELECT array_agg(r) + FROM ( + SELECT 1 AS attnum, 'col1' AS attname + UNION ALL + SELECT 4 AS attnum, 'col2' AS attname + UNION ALL + SELECT 4 AS attnum, 'col3' AS attname + ) AS r + )) AS invalid_attributes_doc, + jsonb_build_object( + 'server_version_num', current_setting('server_version_num'), + 'relname', 'test', + 'nspname', 'stats_export_import', + 'statistics', + ( + SELECT array_agg(r) + FROM ( + SELECT 1 AS staattnum, false AS stainherit + UNION ALL + SELECT 5 AS staattnum, true AS stainherit + UNION ALL + SELECT 1 AS staattnum, false AS stainherit + ) AS r + )) AS invalid_statistics_doc +\gset + +SELECT pg_import_rel_stats('stats_export_import.test'::regclass, + :'invalid_types_doc'::jsonb, true, true); + +SELECT pg_import_rel_stats('stats_export_import.test'::regclass, + :'invalid_collations_doc'::jsonb, true, true); + +SELECT pg_import_rel_stats('stats_export_import.test'::regclass, + :'invalid_operators_doc'::jsonb, true, true); + +SELECT pg_import_rel_stats('stats_export_import.test'::regclass, + :'invalid_attributes_doc'::jsonb, true, true); + +SELECT pg_import_rel_stats('stats_export_import.test'::regclass, + :'invalid_statistics_doc'::jsonb, true, true); + +-- Import stats +SELECT pg_import_rel_stats( + 'stats_export_import.test'::regclass, + :'table_stats_json'::jsonb, + true, + true); + +SELECT pg_import_rel_stats( + 'stats_export_import.is_odd'::regclass, + :'index_stats_json'::jsonb, + true, + true); + +-- This should return 0 rows +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, + sv1, sv2, sv3, sv4, sv5 +FROM stats_export_import.pg_statistic_capture +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 +WHERE starelid IN ('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass); + +-- This should return 0 rows +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 +WHERE starelid IN ('stats_export_import.test'::regclass, + 'stats_export_import.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, + sv1, sv2, sv3, sv4, sv5 +FROM stats_export_import.pg_statistic_capture; + +SELECT relname, reltuples +FROM pg_class +WHERE oid IN ('stats_export_import.test'::regclass, + 'stats_export_import.is_odd'::regclass) +ORDER BY relname; diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index cf3de80394..2be0a30d4d 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28732,6 +28732,71 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset in identifying the specific disk files associated with database objects. + + Database Object Statistics Import Functions + + + + + Function + + + Description + + + + + + + + + pg_import_rel_stats + + pg_import_rel_stats ( relation regclass, relation_stats jsonb, validate bool, require_match_oids bool ) + boolean + + + Replaces all statistics generated by a previous + ANALYZE for the relation + with values specified in relation_stats. + + + Specifically, the pg_statistic rows with a + statrelid matching + relation are replaced with the values derived + from relation_stats, and the + pg_class entry for + relation is modified, replacing the + reltuples and + relpages with values found in + relation_stats. + + + 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 pg_upgrade and + pg_restore to convey the statistics from the old system + version into the new one. + + + If validate is set to true, + then the function will perform a series of data consistency checks on + the data in relation_stats before attempting to + import statistics. Any inconsistencies found will raise an error. + + + If require_match_oids is set to true, + then the import will fail if the imported oids for pt_type, + pg_collation, and pg_operator do + not match the values specified in relation_json, as would be expected + in a binary upgrade. These assumptions would not be true when restoring from a dump. + + + + +
+ Database Object Location Functions -- 2.43.0