]> granicus.if.org Git - postgresql/commitdiff
Prevent inlining of multiply-referenced CTEs with outer recursive refs.
authorTom Lane <tgl@sss.pgh.pa.us>
Tue, 9 Apr 2019 19:47:26 +0000 (15:47 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Tue, 9 Apr 2019 19:47:35 +0000 (15:47 -0400)
This has to be prevented because inlining would result in multiple
self-references, which we don't support (and in fact that's disallowed
by the SQL spec, see statements about linearly vs. nonlinearly
recursive queries).  Bug fix for commit 608b167f9.

Per report from Yaroslav Schekin (via Andrew Gierth)

Discussion: https://postgr.es/m/87wolmg60q.fsf@news-spur.riddles.org.uk

src/backend/optimizer/plan/subselect.c
src/test/regress/expected/subselect.out
src/test/regress/sql/subselect.sql

index 555c91f61e39882b5335a1f00929e612f6fcf3ab..921ebfb5eddf9cfcce0799d43d14266bf726b15e 100644 (file)
@@ -85,6 +85,8 @@ static bool testexpr_is_hashable(Node *testexpr);
 static bool hash_ok_operator(OpExpr *expr);
 static bool contain_dml(Node *node);
 static bool contain_dml_walker(Node *node, void *context);
+static bool contain_outer_selfref(Node *node);
+static bool contain_outer_selfref_walker(Node *node, Index *depth);
 static void inline_cte(PlannerInfo *root, CommonTableExpr *cte);
 static bool inline_cte_walker(Node *node, inline_cte_walker_context *context);
 static bool simplify_EXISTS_query(PlannerInfo *root, Query *query);
@@ -867,6 +869,10 @@ SS_process_ctes(PlannerInfo *root)
                 * SELECT, or containing volatile functions.  Inlining might change
                 * the side-effects, which would be bad.
                 *
+                * 4. The CTE is multiply-referenced and contains a self-reference to
+                * a recursive CTE outside itself.  Inlining would result in multiple
+                * recursive self-references, which we don't support.
+                *
                 * Otherwise, we have an option whether to inline or not.  That should
                 * always be a win if there's just a single reference, but if the CTE
                 * is multiply-referenced then it's unclear: inlining adds duplicate
@@ -876,6 +882,9 @@ SS_process_ctes(PlannerInfo *root)
                 * the user express a preference.  Our default behavior is to inline
                 * only singly-referenced CTEs, but a CTE marked CTEMaterializeNever
                 * will be inlined even if multiply referenced.
+                *
+                * Note: we check for volatile functions last, because that's more
+                * expensive than the other tests needed.
                 */
                if ((cte->ctematerialized == CTEMaterializeNever ||
                         (cte->ctematerialized == CTEMaterializeDefault &&
@@ -883,6 +892,8 @@ SS_process_ctes(PlannerInfo *root)
                        !cte->cterecursive &&
                        cmdType == CMD_SELECT &&
                        !contain_dml(cte->ctequery) &&
+                       (cte->cterefcount <= 1 ||
+                        !contain_outer_selfref(cte->ctequery)) &&
                        !contain_volatile_functions(cte->ctequery))
                {
                        inline_cte(root, cte);
@@ -1016,6 +1027,61 @@ contain_dml_walker(Node *node, void *context)
        return expression_tree_walker(node, contain_dml_walker, context);
 }
 
+/*
+ * contain_outer_selfref: is there an external recursive self-reference?
+ */
+static bool
+contain_outer_selfref(Node *node)
+{
+       Index           depth = 0;
+
+       /*
+        * We should be starting with a Query, so that depth will be 1 while
+        * examining its immediate contents.
+        */
+       Assert(IsA(node, Query));
+
+       return contain_outer_selfref_walker(node, &depth);
+}
+
+static bool
+contain_outer_selfref_walker(Node *node, Index *depth)
+{
+       if (node == NULL)
+               return false;
+       if (IsA(node, RangeTblEntry))
+       {
+               RangeTblEntry *rte = (RangeTblEntry *) node;
+
+               /*
+                * Check for a self-reference to a CTE that's above the Query that our
+                * search started at.
+                */
+               if (rte->rtekind == RTE_CTE &&
+                       rte->self_reference &&
+                       rte->ctelevelsup >= *depth)
+                       return true;
+               return false;                   /* allow range_table_walker to continue */
+       }
+       if (IsA(node, Query))
+       {
+               /* Recurse into subquery, tracking nesting depth properly */
+               Query      *query = (Query *) node;
+               bool            result;
+
+               (*depth)++;
+
+               result = query_tree_walker(query, contain_outer_selfref_walker,
+                                                                  (void *) depth, QTW_EXAMINE_RTES_BEFORE);
+
+               (*depth)--;
+
+               return result;
+       }
+       return expression_tree_walker(node, contain_outer_selfref_walker,
+                                                                 (void *) depth);
+}
+
 /*
  * inline_cte: convert RTE_CTE references to given CTE into RTE_SUBQUERYs
  */
index 4a5410418222990d886c1b656760dc2052362ac9..2d1963d12aedbe9267cbe764cda842c91762d3ea 100644 (file)
@@ -1299,6 +1299,106 @@ select * from x, x x2 where x.n = x2.n;
                      Output: subselect_tbl_1.f1
 (11 rows)
 
+-- Multiply-referenced CTEs can't be inlined if they contain outer self-refs
+explain (verbose, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z1.a as a from z cross join z as z1
+    where length(z.a || z1.a) < 5))
+select * from x;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ CTE Scan on x
+   Output: x.a
+   CTE x
+     ->  Recursive Union
+           ->  Values Scan on "*VALUES*"
+                 Output: "*VALUES*".column1
+           ->  Nested Loop
+                 Output: (z.a || z1.a)
+                 Join Filter: (length((z.a || z1.a)) < 5)
+                 CTE z
+                   ->  WorkTable Scan on x x_1
+                         Output: x_1.a
+                 ->  CTE Scan on z
+                       Output: z.a
+                 ->  CTE Scan on z z1
+                       Output: z1.a
+(16 rows)
+
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z1.a as a from z cross join z as z1
+    where length(z.a || z1.a) < 5))
+select * from x;
+  a   
+------
+ a
+ b
+ aa
+ ab
+ ba
+ bb
+ aaaa
+ aaab
+ aaba
+ aabb
+ abaa
+ abab
+ abba
+ abbb
+ baaa
+ baab
+ baba
+ babb
+ bbaa
+ bbab
+ bbba
+ bbbb
+(22 rows)
+
+explain (verbose, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z.a as a from z
+    where length(z.a || z.a) < 5))
+select * from x;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ CTE Scan on x
+   Output: x.a
+   CTE x
+     ->  Recursive Union
+           ->  Values Scan on "*VALUES*"
+                 Output: "*VALUES*".column1
+           ->  WorkTable Scan on x x_1
+                 Output: (x_1.a || x_1.a)
+                 Filter: (length((x_1.a || x_1.a)) < 5)
+(9 rows)
+
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z.a as a from z
+    where length(z.a || z.a) < 5))
+select * from x;
+  a   
+------
+ a
+ b
+ aa
+ bb
+ aaaa
+ bbbb
+(6 rows)
+
 -- Check handling of outer references
 explain (verbose, costs off)
 with x as (select * from int4_tbl)
index 856bbff7328d4d5f759ea6437794ab9295748120..99ca69791e3ec7744a61358cf4df45e4bab895b5 100644 (file)
@@ -690,6 +690,41 @@ explain (verbose, costs off)
 with x as not materialized (select * from (select f1, now() as n from subselect_tbl) ss)
 select * from x, x x2 where x.n = x2.n;
 
+-- Multiply-referenced CTEs can't be inlined if they contain outer self-refs
+explain (verbose, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z1.a as a from z cross join z as z1
+    where length(z.a || z1.a) < 5))
+select * from x;
+
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z1.a as a from z cross join z as z1
+    where length(z.a || z1.a) < 5))
+select * from x;
+
+explain (verbose, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z.a as a from z
+    where length(z.a || z.a) < 5))
+select * from x;
+
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z.a as a from z
+    where length(z.a || z.a) < 5))
+select * from x;
+
 -- Check handling of outer references
 explain (verbose, costs off)
 with x as (select * from int4_tbl)