Add pg_clear_extended_stats()
authorMichael Paquier <[email protected]>
Thu, 15 Jan 2026 23:13:30 +0000 (08:13 +0900)
committerMichael Paquier <[email protected]>
Thu, 15 Jan 2026 23:13:30 +0000 (08:13 +0900)
This function is able to clear the data associated to an extended
statistics object, making things so as the object looks as
newly-created.

The caller of this function needs the following arguments for the
extended stats to clear:
- The name of the relation.
- The schema name of the relation.
- The name of the extended stats object.
- The schema name of the extended stats object.
- If the stats are inherited or not.

The first two parameters are especially important to ensure a consistent
lookup and ACL checks for the relation on which is based the extended
stats object that will be cleared, relying first on a RangeVar lookup
where permissions are checked without locking a relation, critical to
prevent denial-of-service attacks when using this kind of function (see
also 688dc6299a5b for a similar concern).  The third to fifth arguments
give a way to target the extended stats records to clear.

This has been extracted from a larger patch by the same author, for a
piece which is again useful on its own.  I have rewritten large portions
of it.  The tests have been extended while discussing this piece,
resulting on what this commit includes.  The intention behind this
feature is to add support for the import of extended statistics across
dumps and upgrades, this change building one piece that we will be able
to rely on for the rest of the changes.

Bump catalog version.

Author: Corey Huinker <[email protected]>
Co-authored-by: Michael Paquier <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CADkLM=dpz3KFnqP-dgJ-zvRvtjsa8UZv8wDAQdqho=qN3kX0Zg@mail.gmail.com

doc/src/sgml/func/func-admin.sgml
src/backend/statistics/Makefile
src/backend/statistics/extended_stats_funcs.c [new file with mode: 0644]
src/backend/statistics/meson.build
src/include/catalog/catversion.h
src/include/catalog/pg_proc.dat
src/test/regress/expected/stats_import.out
src/test/regress/sql/stats_import.sql

index 2896cd9e4290901fbf6c6e21fbeb96e371501f16..e7ea16f73b31eb156c27ce51cdf3bac558e36d98 100644 (file)
@@ -2165,6 +2165,35 @@ SELECT pg_restore_attribute_stats(
         </para>
        </entry>
       </row>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_clear_extended_stats</primary>
+         </indexterm>
+         <function>pg_clear_extended_stats</function> (
+         <parameter>schemaname</parameter> <type>name</type>,
+         <parameter>relname</parameter> <type>name</type>,
+         <parameter>statistics_schemaname</parameter> <type>name</type>,
+         <parameter>statistics_name</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Clears data of an extended statistics object, as though the object
+         was newly-created. The required arguments are
+         <literal>schemaname</literal> and <literal>relname</literal> to
+         specify the schema and table name of the relation whose statistics
+         are cleared, as well as <literal>statistics_schemaname</literal>
+         and <literal>statistics_name</literal> to specify the schema and
+         extended statistics name of the extended statistics object to clear.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
index 4672bd90f225b47a3458ee4fc25628958a665f85..7ff5938b02731553c3f1d243e6901df9eaccba45 100644 (file)
@@ -16,6 +16,7 @@ OBJS = \
    attribute_stats.o \
    dependencies.o \
    extended_stats.o \
+   extended_stats_funcs.o \
    mcv.o \
    mvdistinct.o \
    relation_stats.o \
diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
new file mode 100644 (file)
index 0000000..22b9750
--- /dev/null
@@ -0,0 +1,232 @@
+/*-------------------------------------------------------------------------
+ *
+ * extended_stats_funcs.c
+ *   Functions for manipulating extended statistics.
+ *
+ * This file includes the set of facilities required to support the direct
+ * manipulations of extended statistics objects.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *   src/backend/statistics/extended_stats_funcs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_statistic_ext_data.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "statistics/stat_utils.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+
+/*
+ * Index of the arguments for the SQL functions.
+ */
+enum extended_stats_argnum
+{
+   RELSCHEMA_ARG = 0,
+   RELNAME_ARG,
+   STATSCHEMA_ARG,
+   STATNAME_ARG,
+   INHERITED_ARG,
+   NUM_EXTENDED_STATS_ARGS,
+};
+
+/*
+ * The argument names and type OIDs of the arguments for the SQL
+ * functions.
+ */
+static struct StatsArgInfo extarginfo[] =
+{
+   [RELSCHEMA_ARG] = {"schemaname", TEXTOID},
+   [RELNAME_ARG] = {"relname", TEXTOID},
+   [STATSCHEMA_ARG] = {"statistics_schemaname", TEXTOID},
+   [STATNAME_ARG] = {"statistics_name", TEXTOID},
+   [INHERITED_ARG] = {"inherited", BOOLOID},
+   [NUM_EXTENDED_STATS_ARGS] = {0},
+};
+
+static HeapTuple get_pg_statistic_ext(Relation pg_stext, Oid nspoid,
+                                     const char *stxname);
+static bool delete_pg_statistic_ext_data(Oid stxoid, bool inherited);
+
+/*
+ * Fetch a pg_statistic_ext row by name and namespace OID.
+ */
+static HeapTuple
+get_pg_statistic_ext(Relation pg_stext, Oid nspoid, const char *stxname)
+{
+   ScanKeyData key[2];
+   SysScanDesc scan;
+   HeapTuple   tup;
+   Oid         stxoid = InvalidOid;
+
+   ScanKeyInit(&key[0],
+               Anum_pg_statistic_ext_stxname,
+               BTEqualStrategyNumber,
+               F_NAMEEQ,
+               CStringGetDatum(stxname));
+   ScanKeyInit(&key[1],
+               Anum_pg_statistic_ext_stxnamespace,
+               BTEqualStrategyNumber,
+               F_OIDEQ,
+               ObjectIdGetDatum(nspoid));
+
+   /*
+    * Try to find matching pg_statistic_ext row.
+    */
+   scan = systable_beginscan(pg_stext,
+                             StatisticExtNameIndexId,
+                             true,
+                             NULL,
+                             2,
+                             key);
+
+   /* Lookup is based on a unique index, so we get either 0 or 1 tuple. */
+   tup = systable_getnext(scan);
+
+   if (HeapTupleIsValid(tup))
+       stxoid = ((Form_pg_statistic_ext) GETSTRUCT(tup))->oid;
+
+   systable_endscan(scan);
+
+   if (!OidIsValid(stxoid))
+       return NULL;
+
+   return SearchSysCacheCopy1(STATEXTOID, ObjectIdGetDatum(stxoid));
+}
+
+/*
+ * Remove an existing pg_statistic_ext_data row for a given pg_statistic_ext
+ * row and "inherited" pair.
+ */
+static bool
+delete_pg_statistic_ext_data(Oid stxoid, bool inherited)
+{
+   Relation    sed = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+   HeapTuple   oldtup;
+   bool        result = false;
+
+   /* Is there already a pg_statistic tuple for this attribute? */
+   oldtup = SearchSysCache2(STATEXTDATASTXOID,
+                            ObjectIdGetDatum(stxoid),
+                            BoolGetDatum(inherited));
+
+   if (HeapTupleIsValid(oldtup))
+   {
+       CatalogTupleDelete(sed, &oldtup->t_self);
+       ReleaseSysCache(oldtup);
+       result = true;
+   }
+
+   table_close(sed, RowExclusiveLock);
+
+   CommandCounterIncrement();
+
+   return result;
+}
+
+/*
+ * Delete statistics for the given statistics object.
+ */
+Datum
+pg_clear_extended_stats(PG_FUNCTION_ARGS)
+{
+   char       *relnspname;
+   char       *relname;
+   char       *nspname;
+   Oid         nspoid;
+   Oid         relid;
+   char       *stxname;
+   bool        inherited;
+   Relation    pg_stext;
+   HeapTuple   tup;
+   Form_pg_statistic_ext stxform;
+   Oid         locked_table = InvalidOid;
+
+   /* relation arguments */
+   stats_check_required_arg(fcinfo, extarginfo, RELSCHEMA_ARG);
+   relnspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
+   stats_check_required_arg(fcinfo, extarginfo, RELNAME_ARG);
+   relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
+
+   /* extended statistics arguments */
+   stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG);
+   nspname = TextDatumGetCString(PG_GETARG_DATUM(STATSCHEMA_ARG));
+   stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG);
+   stxname = TextDatumGetCString(PG_GETARG_DATUM(STATNAME_ARG));
+   stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG);
+   inherited = PG_GETARG_BOOL(INHERITED_ARG);
+
+   if (RecoveryInProgress())
+   {
+       ereport(WARNING,
+               errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+               errmsg("recovery is in progress"),
+               errhint("Statistics cannot be modified during recovery."));
+       PG_RETURN_VOID();
+   }
+
+   /*
+    * First open the relation where we expect to find the statistics.  This
+    * is similar to relation and attribute statistics, so as ACL checks are
+    * done before any locks are taken, even before any attempts related to
+    * the extended stats object.
+    */
+   relid = RangeVarGetRelidExtended(makeRangeVar(relnspname, relname, -1),
+                                    ShareUpdateExclusiveLock, 0,
+                                    RangeVarCallbackForStats, &locked_table);
+
+   /* Now check if the namespace of the stats object exists. */
+   nspoid = get_namespace_oid(nspname, true);
+   if (nspoid == InvalidOid)
+   {
+       ereport(WARNING,
+               errcode(ERRCODE_UNDEFINED_OBJECT),
+               errmsg("could not find schema \"%s\"", nspname));
+       PG_RETURN_VOID();
+   }
+
+   pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock);
+   tup = get_pg_statistic_ext(pg_stext, nspoid, stxname);
+
+   if (!HeapTupleIsValid(tup))
+   {
+       table_close(pg_stext, RowExclusiveLock);
+       ereport(WARNING,
+               errcode(ERRCODE_UNDEFINED_OBJECT),
+               errmsg("could not find extended statistics object \"%s\".\"%s\"",
+                      nspname, stxname));
+       PG_RETURN_VOID();
+   }
+
+   stxform = (Form_pg_statistic_ext) GETSTRUCT(tup);
+
+   /*
+    * This should be consistent, based on the lock taken on the table when we
+    * started.
+    */
+   if (stxform->stxrelid != relid)
+       elog(ERROR, "cache lookup failed for extended stats %u: found relation %u but expected %u",
+            stxform->oid, stxform->stxrelid, relid);
+
+   delete_pg_statistic_ext_data(stxform->oid, inherited);
+   heap_freetuple(tup);
+
+   table_close(pg_stext, RowExclusiveLock);
+
+   PG_RETURN_VOID();
+}
index 5c89f869812c58b7d62bc19bdad79869c678191b..9a7bf55e3014ee19d03ec2a925d7e2300173f065 100644 (file)
@@ -4,6 +4,7 @@ backend_sources += files(
   'attribute_stats.c',
   'dependencies.c',
   'extended_stats.c',
+  'extended_stats_funcs.c',
   'mcv.c',
   'mvdistinct.c',
   'relation_stats.c',
index ab38a69f0f8915461be7817955b49fd15843e978..13ac6be613e1ad5c22e8511f1dca73bb4c03071b 100644 (file)
@@ -57,6 +57,6 @@
  */
 
 /*                         yyyymmddN */
-#define CATALOG_VERSION_NO 202601081
+#define CATALOG_VERSION_NO 202601161
 
 #endif
index 2ac69bf2df55ae0148cc5822b12f02f99fa54110..894b6a1b6d6b95a9862cdb9c3d428fdd5270a17e 100644 (file)
   proname => 'gist_translate_cmptype_common', prorettype => 'int2',
   proargtypes => 'int4', prosrc => 'gist_translate_cmptype_common' },
 
+# Extended Statistics functions
+{ oid => '9948', descr => 'clear statistics on extended statistics object',
+  proname => 'pg_clear_extended_stats', proisstrict => 'f', provolatile => 'v',
+  proparallel => 'u', prorettype => 'void', proargtypes => 'text text text text bool',
+  proargnames => '{schemaname,relname,statistics_schemaname,statistics_name,inherited}',
+  prosrc => 'pg_clear_extended_stats' },
+
 # AIO related functions
 { oid => '6399', descr => 'information about in-progress asynchronous IOs',
   proname => 'pg_get_aios', prorows => '100', proretset => 't',
index 98ce7dc284102638234e6376bff4138dbebc961a..68ddd619edb1c87476d17e5333053d540000b750 100644 (file)
@@ -132,8 +132,36 @@ CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
+-- This ensures the presence of extended statistics marked with
+-- inherited = true.
+CREATE STATISTICS stats_import.part_parent_stat
+  ON i, (i % 2)
+  FROM stats_import.part_parent;
 CREATE INDEX part_parent_i ON stats_import.part_parent(i);
+INSERT INTO stats_import.part_parent
+SELECT g.g
+FROM generate_series(0,9) AS g(g);
+SELECT COUNT(*) FROM stats_import.part_parent;
+ count 
+-------
+    10
+(1 row)
+
+SELECT COUNT(*) FROM stats_import.part_child_1;
+ count 
+-------
+    10
+(1 row)
+
 ANALYZE stats_import.part_parent;
+SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'part_parent_stat' GROUP BY e.inherited;
+ count | inherited 
+-------+-----------
+     1 | t
+(1 row)
+
 SELECT relpages
 FROM pg_class
 WHERE oid = 'stats_import.part_parent'::regclass;
@@ -1084,11 +1112,17 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+CREATE STATISTICS stats_import.test_stat
+  ON name, comp, lower(arange), array_length(tags,1)
+  FROM stats_import.test;
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
     WITH (autovacuum_enabled = false);
 CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+CREATE STATISTICS stats_import.test_stat_clone
+  ON name, comp, lower(arange), array_length(tags,1)
+  FROM stats_import.test_clone;
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
@@ -1342,6 +1376,182 @@ AND attname = 'i';
 (1 row)
 
 DROP TABLE stats_temp;
+-- Tests for pg_clear_extended_stats().
+--  Invalid argument values.
+SELECT pg_clear_extended_stats(schemaname => NULL,
+  relname => 'rel_foo',
+  statistics_schemaname => 'schema_foo',
+  statistics_name => 'stat_bar',
+  inherited => false);
+ERROR:  argument "schemaname" must not be null
+SELECT pg_clear_extended_stats(schemaname => 'schema_foo',
+  relname => NULL,
+  statistics_schemaname => 'schema_foo',
+  statistics_name => 'stat_bar',
+  inherited => false);
+ERROR:  argument "relname" must not be null
+SELECT pg_clear_extended_stats(schemaname => 'schema_foo',
+  relname => 'rel_foo',
+  statistics_schemaname => NULL,
+  statistics_name => 'stat_bar',
+  inherited => false);
+ERROR:  argument "statistics_schemaname" must not be null
+SELECT pg_clear_extended_stats(schemaname => 'schema_foo',
+  relname => 'rel_foo',
+  statistics_schemaname => 'schema_foo',
+  statistics_name => NULL,
+  inherited => false);
+ERROR:  argument "statistics_name" must not be null
+SELECT pg_clear_extended_stats(schemaname => 'schema_foo',
+  relname => 'rel_foo',
+  statistics_schemaname => 'schema_foo',
+  statistics_name => 'stat_bar',
+  inherited => NULL);
+ERROR:  argument "inherited" must not be null
+-- Missing objects
+SELECT pg_clear_extended_stats(schemaname => 'schema_not_exist',
+  relname => 'test',
+  statistics_schemaname => 'schema_not_exist',
+  statistics_name => 'test_stat',
+  inherited => false);
+ERROR:  schema "schema_not_exist" does not exist
+SELECT pg_clear_extended_stats(schemaname => 'stats_import',
+  relname => 'table_not_exist',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'test_stat',
+  inherited => false);
+ERROR:  relation "stats_import.table_not_exist" does not exist
+SELECT pg_clear_extended_stats(schemaname => 'stats_import',
+  relname => 'test',
+  statistics_schemaname => 'schema_not_exist',
+  statistics_name => 'test_stat',
+  inherited => false);
+WARNING:  could not find schema "schema_not_exist"
+ pg_clear_extended_stats 
+-------------------------
+(1 row)
+
+SELECT pg_clear_extended_stats(schemaname => 'stats_import',
+  relname => 'test',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'ext_stats_not_exist',
+  inherited => false);
+WARNING:  could not find extended statistics object "stats_import"."ext_stats_not_exist"
+ pg_clear_extended_stats 
+-------------------------
+(1 row)
+
+-- Check that records are removed after a valid clear call.
+SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'test_stat' GROUP BY e.inherited;
+ count | inherited 
+-------+-----------
+     1 | f
+(1 row)
+
+SELECT COUNT(*), e.inherited FROM pg_stats_ext_exprs AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'test_stat' GROUP BY e.inherited;
+ count | inherited 
+-------+-----------
+     2 | f
+(1 row)
+
+BEGIN;
+SELECT pg_catalog.pg_clear_extended_stats(
+  schemaname => 'stats_import',
+  relname => 'test',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'test_stat',
+  inherited => false);
+ pg_clear_extended_stats 
+-------------------------
+(1 row)
+
+SELECT mode FROM pg_locks WHERE locktype = 'relation' AND
+  relation = 'stats_import.test'::regclass AND
+  pid = pg_backend_pid();
+           mode           
+--------------------------
+ ShareUpdateExclusiveLock
+(1 row)
+
+COMMIT;
+SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'test_stat' GROUP BY e.inherited;
+ count | inherited 
+-------+-----------
+(0 rows)
+
+SELECT COUNT(*), e.inherited FROM pg_stats_ext_exprs AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'test_stat' GROUP BY e.inherited;
+ count | inherited 
+-------+-----------
+     2 | 
+(1 row)
+
+-- And before/after on inherited stats
+SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'part_parent_stat' GROUP BY e.inherited;
+ count | inherited 
+-------+-----------
+     1 | t
+(1 row)
+
+SELECT pg_catalog.pg_clear_extended_stats(
+  schemaname => 'stats_import',
+  relname => 'part_parent',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'part_parent_stat',
+  inherited => true);
+ pg_clear_extended_stats 
+-------------------------
+(1 row)
+
+SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'part_parent_stat' GROUP BY e.inherited;
+ count | inherited 
+-------+-----------
+(0 rows)
+
+-- Check that MAINTAIN is required when clearing statistics.
+CREATE ROLE regress_test_extstat_clear;
+GRANT ALL ON SCHEMA stats_import TO regress_test_extstat_clear;
+SET ROLE regress_test_extstat_clear;
+SELECT pg_catalog.pg_clear_extended_stats(
+  schemaname => 'stats_import',
+  relname => 'test',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'test_stat',
+  inherited => false);
+ERROR:  permission denied for table test
+RESET ROLE;
+GRANT MAINTAIN ON stats_import.test TO regress_test_extstat_clear;
+SET ROLE regress_test_extstat_clear;
+SELECT pg_catalog.pg_clear_extended_stats(
+  schemaname => 'stats_import',
+  relname => 'test',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'test_stat',
+  inherited => false);
+ pg_clear_extended_stats 
+-------------------------
+(1 row)
+
+RESET ROLE;
+REVOKE MAINTAIN ON stats_import.test FROM regress_test_extstat_clear;
+REVOKE ALL ON SCHEMA stats_import FROM regress_test_extstat_clear;
+DROP ROLE regress_test_extstat_clear;
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
index d140733a750289002da825b15f28a165af5f43bc..04d15202e654f79c66f3203cb1d29078bf2188b5 100644 (file)
@@ -108,10 +108,27 @@ CREATE TABLE stats_import.part_child_1
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
 
+-- This ensures the presence of extended statistics marked with
+-- inherited = true.
+CREATE STATISTICS stats_import.part_parent_stat
+  ON i, (i % 2)
+  FROM stats_import.part_parent;
+
 CREATE INDEX part_parent_i ON stats_import.part_parent(i);
 
+INSERT INTO stats_import.part_parent
+SELECT g.g
+FROM generate_series(0,9) AS g(g);
+
+SELECT COUNT(*) FROM stats_import.part_parent;
+SELECT COUNT(*) FROM stats_import.part_child_1;
+
 ANALYZE stats_import.part_parent;
 
+SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'part_parent_stat' GROUP BY e.inherited;
+
 SELECT relpages
 FROM pg_class
 WHERE oid = 'stats_import.part_parent'::regclass;
@@ -766,6 +783,10 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+CREATE STATISTICS stats_import.test_stat
+  ON name, comp, lower(arange), array_length(tags,1)
+  FROM stats_import.test;
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
@@ -774,6 +795,10 @@ CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
 
 CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
 
+CREATE STATISTICS stats_import.test_stat_clone
+  ON name, comp, lower(arange), array_length(tags,1)
+  FROM stats_import.test_clone;
+
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
@@ -970,4 +995,116 @@ AND tablename = 'stats_temp'
 AND inherited = false
 AND attname = 'i';
 DROP TABLE stats_temp;
+
+-- Tests for pg_clear_extended_stats().
+--  Invalid argument values.
+SELECT pg_clear_extended_stats(schemaname => NULL,
+  relname => 'rel_foo',
+  statistics_schemaname => 'schema_foo',
+  statistics_name => 'stat_bar',
+  inherited => false);
+SELECT pg_clear_extended_stats(schemaname => 'schema_foo',
+  relname => NULL,
+  statistics_schemaname => 'schema_foo',
+  statistics_name => 'stat_bar',
+  inherited => false);
+SELECT pg_clear_extended_stats(schemaname => 'schema_foo',
+  relname => 'rel_foo',
+  statistics_schemaname => NULL,
+  statistics_name => 'stat_bar',
+  inherited => false);
+SELECT pg_clear_extended_stats(schemaname => 'schema_foo',
+  relname => 'rel_foo',
+  statistics_schemaname => 'schema_foo',
+  statistics_name => NULL,
+  inherited => false);
+SELECT pg_clear_extended_stats(schemaname => 'schema_foo',
+  relname => 'rel_foo',
+  statistics_schemaname => 'schema_foo',
+  statistics_name => 'stat_bar',
+  inherited => NULL);
+-- Missing objects
+SELECT pg_clear_extended_stats(schemaname => 'schema_not_exist',
+  relname => 'test',
+  statistics_schemaname => 'schema_not_exist',
+  statistics_name => 'test_stat',
+  inherited => false);
+SELECT pg_clear_extended_stats(schemaname => 'stats_import',
+  relname => 'table_not_exist',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'test_stat',
+  inherited => false);
+SELECT pg_clear_extended_stats(schemaname => 'stats_import',
+  relname => 'test',
+  statistics_schemaname => 'schema_not_exist',
+  statistics_name => 'test_stat',
+  inherited => false);
+SELECT pg_clear_extended_stats(schemaname => 'stats_import',
+  relname => 'test',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'ext_stats_not_exist',
+  inherited => false);
+
+-- Check that records are removed after a valid clear call.
+SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'test_stat' GROUP BY e.inherited;
+SELECT COUNT(*), e.inherited FROM pg_stats_ext_exprs AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'test_stat' GROUP BY e.inherited;
+BEGIN;
+SELECT pg_catalog.pg_clear_extended_stats(
+  schemaname => 'stats_import',
+  relname => 'test',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'test_stat',
+  inherited => false);
+SELECT mode FROM pg_locks WHERE locktype = 'relation' AND
+  relation = 'stats_import.test'::regclass AND
+  pid = pg_backend_pid();
+COMMIT;
+SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'test_stat' GROUP BY e.inherited;
+SELECT COUNT(*), e.inherited FROM pg_stats_ext_exprs AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'test_stat' GROUP BY e.inherited;
+-- And before/after on inherited stats
+SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'part_parent_stat' GROUP BY e.inherited;
+SELECT pg_catalog.pg_clear_extended_stats(
+  schemaname => 'stats_import',
+  relname => 'part_parent',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'part_parent_stat',
+  inherited => true);
+SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e
+  WHERE e.statistics_schemaname = 'stats_import' AND
+  e.statistics_name = 'part_parent_stat' GROUP BY e.inherited;
+
+-- Check that MAINTAIN is required when clearing statistics.
+CREATE ROLE regress_test_extstat_clear;
+GRANT ALL ON SCHEMA stats_import TO regress_test_extstat_clear;
+SET ROLE regress_test_extstat_clear;
+SELECT pg_catalog.pg_clear_extended_stats(
+  schemaname => 'stats_import',
+  relname => 'test',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'test_stat',
+  inherited => false);
+RESET ROLE;
+GRANT MAINTAIN ON stats_import.test TO regress_test_extstat_clear;
+SET ROLE regress_test_extstat_clear;
+SELECT pg_catalog.pg_clear_extended_stats(
+  schemaname => 'stats_import',
+  relname => 'test',
+  statistics_schemaname => 'stats_import',
+  statistics_name => 'test_stat',
+  inherited => false);
+RESET ROLE;
+REVOKE MAINTAIN ON stats_import.test FROM regress_test_extstat_clear;
+REVOKE ALL ON SCHEMA stats_import FROM regress_test_extstat_clear;
+DROP ROLE regress_test_extstat_clear;
+
 DROP SCHEMA stats_import CASCADE;