]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix UNION planner estimate_num_groups with varno==0
authorDavid Rowley <drowley@postgresql.org>
Thu, 6 Nov 2025 03:34:55 +0000 (16:34 +1300)
committerDavid Rowley <drowley@postgresql.org>
Thu, 6 Nov 2025 03:34:55 +0000 (16:34 +1300)
03d40e4b5 added code to provide better row estimates for when a UNION
query ended up only with a single child due to other children being
found to be dummy rels.  In that case, ordinarily it would be ok to call
estimate_num_groups() on the targetlist of the only child path, however
that's not safe to do if the UNION child is the result of some other set
operation as we generate targetlists containing Vars with varno==0 for
those, which estimate_num_groups() can't handle.  This could lead to:

ERROR:  XX000: no relation entry for relid 0

Fix this by avoiding doing this when the only child is the result of
another set operation.  In that case we'll fall back on the
assume-all-rows-are-unique method.

Reported-by: Alexander Lakhin <exclusion@gmail.com>
Author: David Rowley <dgrowleyml@gmail.com>
Discussion: https://postgr.es/m/cfbc99e5-9d44-4806-ba3c-f36b57a85e21@gmail.com

src/backend/optimizer/prep/prepunion.c
src/test/regress/expected/union.out
src/test/regress/sql/union.sql

index 7253954565676530cbbd87d6c27f716bfa5c8a11..f528f096a5688ad0d00c62ee9e26cba1e5df2e66 100644 (file)
@@ -901,19 +901,22 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
                double          dNumGroups;
                bool            can_sort = grouping_is_sortable(groupList);
                bool            can_hash = grouping_is_hashable(groupList);
+               Path       *first_path = linitial(cheapest_pathlist);
 
-               if (list_length(cheapest_pathlist) == 1)
+               /*
+                * Estimate the number of UNION output rows.  In the case when only a
+                * single UNION child remains, we can use estimate_num_groups() on
+                * that child.  We must be careful not to do this when that child is
+                * the result of some other set operation as the targetlist will
+                * contain Vars with varno==0, which estimate_num_groups() wouldn't
+                * like.
+                */
+               if (list_length(cheapest_pathlist) == 1 &&
+                       first_path->parent->reloptkind != RELOPT_UPPER_REL)
                {
-                       Path       *path = linitial(cheapest_pathlist);
-
-                       /*
-                        * In the case where only one union child remains due to the
-                        * detection of one or more dummy union children, obtain an
-                        * estimate on the surviving child directly.
-                        */
                        dNumGroups = estimate_num_groups(root,
-                                                                                        path->pathtarget->exprs,
-                                                                                        path->rows,
+                                                                                        first_path->pathtarget->exprs,
+                                                                                        first_path->rows,
                                                                                         NULL,
                                                                                         NULL);
                }
index 4533967e84acb3b60b4f1d9a1191e11f01d33bc5..709c85f22948acb52feabd3c1cf0ad5de5bb6ecc 100644 (file)
@@ -1355,6 +1355,28 @@ ORDER BY 1;
          Output: tenk1.two
 (5 rows)
 
+-- Try a mixed setop case.  Ensure the right-hand UNION child gets removed.
+EXPLAIN (COSTS OFF, VERBOSE)
+SELECT two FROM tenk1 t1
+EXCEPT
+SELECT four FROM tenk1 t2
+UNION
+SELECT ten FROM tenk1 dummy WHERE 1=2;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Unique
+   Output: t1.two
+   ->  Sort
+         Output: t1.two
+         Sort Key: t1.two
+         ->  HashSetOp Except
+               Output: t1.two
+               ->  Seq Scan on public.tenk1 t1
+                     Output: t1.two
+               ->  Seq Scan on public.tenk1 t2
+                     Output: t2.four
+(11 rows)
+
 -- Test constraint exclusion of UNION ALL subqueries
 explain (costs off)
  SELECT * FROM
index 782cca23701d4a8ce758de9be9bea06c3166c526..d0c70fafbeaa40162e3c344e3d664e41f3469542 100644 (file)
@@ -523,6 +523,14 @@ EXCEPT ALL
 SELECT four FROM tenk1 WHERE 1=2
 ORDER BY 1;
 
+-- Try a mixed setop case.  Ensure the right-hand UNION child gets removed.
+EXPLAIN (COSTS OFF, VERBOSE)
+SELECT two FROM tenk1 t1
+EXCEPT
+SELECT four FROM tenk1 t2
+UNION
+SELECT ten FROM tenk1 dummy WHERE 1=2;
+
 -- Test constraint exclusion of UNION ALL subqueries
 explain (costs off)
  SELECT * FROM