Ignore extended statistics for inheritance trees
authorTomas Vondra <[email protected]>
Sat, 15 Jan 2022 01:15:23 +0000 (02:15 +0100)
committerTomas Vondra <[email protected]>
Sat, 15 Jan 2022 02:05:06 +0000 (03:05 +0100)
Since commit 859b3003de we only build extended statistics for individual
relations, ignoring the child relations. This resolved the issue with
updating catalog tuple twice, but we still tried to use the statistics
when calculating estimates for the whole inheritance tree. When the
relations contain very distinct data, it may produce bogus estimates.

This is roughly the same issue 427c6b5b9 addressed ~15 years ago, and we
fix it the same way - by ignoring extended statistics when calculating
estimates for the inheritance tree as a whole. We still consider
extended statistics when calculating estimates for individual child
relations, of course.

This may result in plan changes due to different estimates, but if the
old statistics were not describing the inheritance tree particularly
well it's quite likely the new plans is actually better.

Report and patch by Justin Pryzby, minor fixes and cleanup by me.
Backpatch all the way back to PostgreSQL 10, where extended statistics
were introduced (same as 859b3003de).

Author: Justin Pryzby
Reported-by: Justin Pryzby
Backpatch-through: 10
Discussion: https://postgr.es/m/20210923212624.GI831%40telsasoft.com

src/backend/statistics/dependencies.c
src/backend/statistics/extended_stats.c
src/backend/utils/adt/selfuncs.c
src/test/regress/expected/stats_ext.out
src/test/regress/sql/stats_ext.sql

index 75595e8fa24db7cfbc039c03e4ee36337a3f1335..9c7e7d9bfde0a2bbcd6a33616ab93db1d3e0f4a2 100644 (file)
@@ -23,6 +23,7 @@
 #include "optimizer/var.h"
 #include "nodes/nodes.h"
 #include "nodes/relation.h"
+#include "parser/parsetree.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
 #include "utils/bytea.h"
@@ -961,10 +962,18 @@ dependencies_clauselist_selectivity(PlannerInfo *root,
    MVDependencies *dependencies;
    AttrNumber *list_attnums;
    int         listidx;
+   RangeTblEntry *rte = planner_rt_fetch(rel->relid, root);
 
    /* initialize output argument */
    *estimatedclauses = NULL;
 
+   /*
+    * When dealing with inheritance trees, ignore extended stats (which were
+    * built without data from child rels, and thus do not represent them).
+    */
+   if (rte->inh)
+       return 1.0;
+
    /* check if there's any stats that might be useful for us. */
    if (!has_stats_of_kind(rel->statlist, STATS_EXT_DEPENDENCIES))
        return 1.0;
index 8cb469ceed4fddea33be7ef93c3a888882eee9ac..a5afc751b77a1a55990ad978b68522ab60cd44bd 100644 (file)
@@ -23,6 +23,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_statistic_ext.h"
 #include "nodes/relation.h"
+#include "parser/parsetree.h"
 #include "postmaster/autovacuum.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
index 6a2a2c7481f8a68e40a5bd290e59ea8cdb16bfc1..af247043f45601264368d559af36e33f42afc521 100644 (file)
@@ -3749,6 +3749,14 @@ estimate_multivariate_ndistinct(PlannerInfo *root, RelOptInfo *rel,
    Oid         statOid = InvalidOid;
    MVNDistinct *stats;
    Bitmapset  *matched = NULL;
+   RangeTblEntry       *rte = planner_rt_fetch(rel->relid, root);
+
+   /*
+    * When dealing with inheritance trees, ignore extended stats (which were
+    * built without data from child rels, and thus do not represent them).
+    */
+   if (rte->inh)
+       return false;
 
    /* bail out immediately if the table has no extended statistics */
    if (!rel->statlist)
index bb714596483ce7b613610aadc0d927e0d9b4bcf7..66e770ab45a90b3c27557fe918716e395bd8b47a 100644 (file)
@@ -1,4 +1,24 @@
 -- Generic extended statistics support
+-- check the number of estimated/actual rows in the top node
+create function check_estimated_rows(text) returns table (estimated int, actual int)
+language plpgsql as
+$$
+declare
+    ln text;
+    tmp text[];
+    first_row bool := true;
+begin
+    for ln in
+        execute format('explain analyze %s', $1)
+    loop
+        if first_row then
+            first_row := false;
+            tmp := regexp_match(ln, 'rows=(\d*) .* rows=(\d*)');
+            return query select tmp[1]::int, tmp[2]::int;
+        end if;
+    end loop;
+end;
+$$;
 -- We will be checking execution plans without/with statistics, so
 -- let's make sure we get simple non-parallel plans. Also set the
 -- work_mem low so that we can use small amounts of data.
@@ -107,6 +127,51 @@ CREATE STATISTICS ab1_a_b_stats ON a, b FROM ab1;
 ANALYZE ab1;
 DROP TABLE ab1 CASCADE;
 NOTICE:  drop cascades to table ab1c
+-- Tests for stats with inheritance
+CREATE TABLE stxdinh(a int, b int);
+CREATE TABLE stxdinh1() INHERITS(stxdinh);
+CREATE TABLE stxdinh2() INHERITS(stxdinh);
+INSERT INTO stxdinh SELECT mod(a,50), mod(a,100) FROM generate_series(0, 1999) a;
+INSERT INTO stxdinh1 SELECT mod(a,100), mod(a,100) FROM generate_series(0, 999) a;
+INSERT INTO stxdinh2 SELECT mod(a,100), mod(a,100) FROM generate_series(0, 999) a;
+VACUUM ANALYZE stxdinh;
+VACUUM ANALYZE stxdinh1;
+VACUUM ANALYZE stxdinh2;
+-- Ensure non-inherited stats are not applied to inherited query
+-- Without stats object, it looks like this
+SELECT * FROM check_estimated_rows('SELECT a, b FROM stxdinh* GROUP BY 1, 2');
+ estimated | actual 
+-----------+--------
+       400 |    150
+(1 row)
+
+SELECT * FROM check_estimated_rows('SELECT a, b FROM stxdinh* WHERE a = 0 AND b = 0');
+ estimated | actual 
+-----------+--------
+         3 |     40
+(1 row)
+
+CREATE STATISTICS stxdinh ON a, b FROM stxdinh;
+VACUUM ANALYZE stxdinh;
+VACUUM ANALYZE stxdinh1;
+VACUUM ANALYZE stxdinh2;
+-- Since the stats object does not include inherited stats, it should not
+-- affect the estimates
+SELECT * FROM check_estimated_rows('SELECT a, b FROM stxdinh* GROUP BY 1, 2');
+ estimated | actual 
+-----------+--------
+       400 |    150
+(1 row)
+
+-- Dependencies are applied at individual relations (within append), so
+-- this estimate changes a bit because we improve estimates for the parent
+SELECT * FROM check_estimated_rows('SELECT a, b FROM stxdinh* WHERE a = 0 AND b = 0');
+ estimated | actual 
+-----------+--------
+        22 |     40
+(1 row)
+
+DROP TABLE stxdinh, stxdinh1, stxdinh2;
 -- Verify supported object types for extended statistics
 CREATE schema tststats;
 CREATE TABLE tststats.t (a int, b int, c text);
index 7a13d26e8d9c1a0c6d5514bc8f850690d1605c2e..8ef6d56c3ee5258018c4cfae9c08c3045e3a208a 100644 (file)
@@ -1,5 +1,26 @@
 -- Generic extended statistics support
 
+-- check the number of estimated/actual rows in the top node
+create function check_estimated_rows(text) returns table (estimated int, actual int)
+language plpgsql as
+$$
+declare
+    ln text;
+    tmp text[];
+    first_row bool := true;
+begin
+    for ln in
+        execute format('explain analyze %s', $1)
+    loop
+        if first_row then
+            first_row := false;
+            tmp := regexp_match(ln, 'rows=(\d*) .* rows=(\d*)');
+            return query select tmp[1]::int, tmp[2]::int;
+        end if;
+    end loop;
+end;
+$$;
+
 -- We will be checking execution plans without/with statistics, so
 -- let's make sure we get simple non-parallel plans. Also set the
 -- work_mem low so that we can use small amounts of data.
@@ -74,6 +95,32 @@ CREATE STATISTICS ab1_a_b_stats ON a, b FROM ab1;
 ANALYZE ab1;
 DROP TABLE ab1 CASCADE;
 
+-- Tests for stats with inheritance
+CREATE TABLE stxdinh(a int, b int);
+CREATE TABLE stxdinh1() INHERITS(stxdinh);
+CREATE TABLE stxdinh2() INHERITS(stxdinh);
+INSERT INTO stxdinh SELECT mod(a,50), mod(a,100) FROM generate_series(0, 1999) a;
+INSERT INTO stxdinh1 SELECT mod(a,100), mod(a,100) FROM generate_series(0, 999) a;
+INSERT INTO stxdinh2 SELECT mod(a,100), mod(a,100) FROM generate_series(0, 999) a;
+VACUUM ANALYZE stxdinh;
+VACUUM ANALYZE stxdinh1;
+VACUUM ANALYZE stxdinh2;
+-- Ensure non-inherited stats are not applied to inherited query
+-- Without stats object, it looks like this
+SELECT * FROM check_estimated_rows('SELECT a, b FROM stxdinh* GROUP BY 1, 2');
+SELECT * FROM check_estimated_rows('SELECT a, b FROM stxdinh* WHERE a = 0 AND b = 0');
+CREATE STATISTICS stxdinh ON a, b FROM stxdinh;
+VACUUM ANALYZE stxdinh;
+VACUUM ANALYZE stxdinh1;
+VACUUM ANALYZE stxdinh2;
+-- Since the stats object does not include inherited stats, it should not
+-- affect the estimates
+SELECT * FROM check_estimated_rows('SELECT a, b FROM stxdinh* GROUP BY 1, 2');
+-- Dependencies are applied at individual relations (within append), so
+-- this estimate changes a bit because we improve estimates for the parent
+SELECT * FROM check_estimated_rows('SELECT a, b FROM stxdinh* WHERE a = 0 AND b = 0');
+DROP TABLE stxdinh, stxdinh1, stxdinh2;
+
 -- Verify supported object types for extended statistics
 CREATE schema tststats;