]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Be more careful about the shape of hashable subplan clauses.
authorTom Lane <tgl@sss.pgh.pa.us>
Sat, 15 Aug 2020 02:14:03 +0000 (22:14 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Sat, 15 Aug 2020 02:14:03 +0000 (22:14 -0400)
nodeSubplan.c expects that the testexpr for a hashable ANY SubPlan
has the form of one or more OpExprs whose LHS is an expression of the
outer query's, while the RHS is an expression over Params representing
output columns of the subquery.  However, the planner only went as far
as verifying that the clauses were all binary OpExprs.  This works
99.99% of the time, because the clauses have the right shape when
emitted by the parser --- but it's possible for function inlining to
break that, as reported by PegoraroF10.  To fix, teach the planner
to check that the LHS and RHS contain the right things, or more
accurately don't contain the wrong things.  Given that this has been
broken for years without anyone noticing, it seems sufficient to just
give up hashing when it happens, rather than go to the trouble of
commuting the clauses back again (which wouldn't necessarily work
anyway).

While poking at that, I also noticed that nodeSubplan.c had a baked-in
assumption that the number of hash clauses is identical to the number
of subquery output columns.  Again, that's fine as far as parser output
goes, but it's not hard to break it via function inlining.  There seems
little reason for that assumption though --- AFAICS, the only thing
it's buying us is not having to store the number of hash clauses
explicitly.  Adding code to the planner to reject such cases would take
more code than getting nodeSubplan.c to cope, so I fixed it that way.

This has been broken for as long as we've had hashable SubPlans,
so back-patch to all supported branches.

Discussion: https://postgr.es/m/1549209182255-0.post@n3.nabble.com

src/backend/executor/nodeSubplan.c
src/backend/optimizer/plan/subselect.c
src/backend/optimizer/util/clauses.c
src/include/nodes/execnodes.h
src/include/optimizer/clauses.h
src/test/regress/expected/subselect.out
src/test/regress/sql/subselect.sql

index 01518df8b2561e924db4263094f44ec678f9cbd1..9aeceb2e3d08b419fa860063566810abe652ce42 100644 (file)
@@ -481,7 +481,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 {
        SubPlan    *subplan = (SubPlan *) node->xprstate.expr;
        PlanState  *planstate = node->planstate;
-       int                     ncols = list_length(subplan->paramIds);
+       int                     ncols = node->numCols;
        ExprContext *innerecontext = node->innerecontext;
        MemoryContext oldcontext;
        long            nbuckets;
@@ -795,11 +795,6 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent)
                                                                  ALLOCSET_SMALL_SIZES);
                /* and a short-lived exprcontext for function evaluation */
                sstate->innerecontext = CreateExprContext(estate);
-               /* Silly little array of column numbers 1..n */
-               ncols = list_length(subplan->paramIds);
-               sstate->keyColIdx = (AttrNumber *) palloc(ncols * sizeof(AttrNumber));
-               for (i = 0; i < ncols; i++)
-                       sstate->keyColIdx[i] = i + 1;
 
                /*
                 * We use ExecProject to evaluate the lefthand and righthand
@@ -833,10 +828,12 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent)
                                 (int) nodeTag(sstate->testexpr->expr));
                        oplist = NIL;           /* keep compiler quiet */
                }
-               Assert(list_length(oplist) == ncols);
+               ncols = list_length(oplist);
 
                lefttlist = righttlist = NIL;
                leftptlist = rightptlist = NIL;
+               sstate->numCols = ncols;
+               sstate->keyColIdx = (AttrNumber *) palloc(ncols * sizeof(AttrNumber));
                sstate->tab_hash_funcs = (FmgrInfo *) palloc(ncols * sizeof(FmgrInfo));
                sstate->tab_eq_funcs = (FmgrInfo *) palloc(ncols * sizeof(FmgrInfo));
                sstate->lhs_hash_funcs = (FmgrInfo *) palloc(ncols * sizeof(FmgrInfo));
@@ -905,6 +902,9 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent)
                        fmgr_info(left_hashfn, &sstate->lhs_hash_funcs[i - 1]);
                        fmgr_info(right_hashfn, &sstate->tab_hash_funcs[i - 1]);
 
+                       /* keyColIdx is just column numbers 1..n */
+                       sstate->keyColIdx[i - 1] = i;
+
                        i++;
                }
 
index 8ae048fc968b584ab61f530eb0c8d321bb33c3c0..91714fdcc60ba043246e7fd1b534379d5fa91d3e 100644 (file)
@@ -61,7 +61,7 @@ typedef struct finalize_primnode_context
 static Node *build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
                          List *plan_params,
                          SubLinkType subLinkType, int subLinkId,
-                         Node *testexpr, bool adjust_testexpr,
+                         Node *testexpr, List *testexpr_paramids,
                          bool unknownEqFalse);
 static List *generate_subquery_params(PlannerInfo *root, List *tlist,
                                                 List **paramIds);
@@ -73,7 +73,8 @@ static Node *convert_testexpr(PlannerInfo *root,
 static Node *convert_testexpr_mutator(Node *node,
                                                 convert_testexpr_context *context);
 static bool subplan_is_hashable(Plan *plan);
-static bool testexpr_is_hashable(Node *testexpr);
+static bool testexpr_is_hashable(Node *testexpr, List *param_ids);
+static bool test_opexpr_is_hashable(OpExpr *testexpr, List *param_ids);
 static bool hash_ok_operator(OpExpr *expr);
 static bool simplify_EXISTS_query(PlannerInfo *root, Query *query);
 static Query *convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect,
@@ -240,7 +241,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
        /* And convert to SubPlan or InitPlan format. */
        result = build_subplan(root, plan, subroot, plan_params,
                                                   subLinkType, subLinkId,
-                                                  testexpr, true, isTopQual);
+                                                  testexpr, NIL, isTopQual);
 
        /*
         * If it's a correlated EXISTS with an unimportant targetlist, we might be
@@ -293,13 +294,12 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
                                                                                                         plan_params,
                                                                                                         ANY_SUBLINK, 0,
                                                                                                         newtestexpr,
-                                                                                                        false, true);
+                                                                                                        paramIds,
+                                                                                                        true);
                                /* Check we got what we expected */
                                Assert(IsA(hashplan, SubPlan));
                                Assert(hashplan->parParam == NIL);
                                Assert(hashplan->useHashTable);
-                               /* build_subplan won't have filled in paramIds */
-                               hashplan->paramIds = paramIds;
 
                                /* Leave it to the executor to decide which plan to use */
                                asplan = makeNode(AlternativeSubPlan);
@@ -322,7 +322,7 @@ static Node *
 build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
                          List *plan_params,
                          SubLinkType subLinkType, int subLinkId,
-                         Node *testexpr, bool adjust_testexpr,
+                         Node *testexpr, List *testexpr_paramids,
                          bool unknownEqFalse)
 {
        Node       *result;
@@ -486,10 +486,10 @@ build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
        else
        {
                /*
-                * Adjust the Params in the testexpr, unless caller said it's not
-                * needed.
+                * Adjust the Params in the testexpr, unless caller already took care
+                * of it (as indicated by passing a list of Param IDs).
                 */
-               if (testexpr && adjust_testexpr)
+               if (testexpr && testexpr_paramids == NIL)
                {
                        List       *params;
 
@@ -501,7 +501,10 @@ build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
                                                                                           params);
                }
                else
+               {
                        splan->testexpr = testexpr;
+                       splan->paramIds = testexpr_paramids;
+               }
 
                /*
                 * We can't convert subplans of ALL_SUBLINK or ANY_SUBLINK types to
@@ -513,7 +516,7 @@ build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
                if (subLinkType == ANY_SUBLINK &&
                        splan->parParam == NIL &&
                        subplan_is_hashable(plan) &&
-                       testexpr_is_hashable(splan->testexpr))
+                       testexpr_is_hashable(splan->testexpr, splan->paramIds))
                        splan->useHashTable = true;
 
                /*
@@ -735,24 +738,20 @@ subplan_is_hashable(Plan *plan)
 
 /*
  * testexpr_is_hashable: is an ANY SubLink's test expression hashable?
+ *
+ * To identify LHS vs RHS of the hash expression, we must be given the
+ * list of output Param IDs of the SubLink's subquery.
  */
 static bool
-testexpr_is_hashable(Node *testexpr)
+testexpr_is_hashable(Node *testexpr, List *param_ids)
 {
        /*
         * The testexpr must be a single OpExpr, or an AND-clause containing only
-        * OpExprs.
-        *
-        * The combining operators must be hashable and strict. The need for
-        * hashability is obvious, since we want to use hashing. Without
-        * strictness, behavior in the presence of nulls is too unpredictable.  We
-        * actually must assume even more than plain strictness: they can't yield
-        * NULL for non-null inputs, either (see nodeSubplan.c).  However, hash
-        * indexes and hash joins assume that too.
+        * OpExprs, each of which satisfy test_opexpr_is_hashable().
         */
        if (testexpr && IsA(testexpr, OpExpr))
        {
-               if (hash_ok_operator((OpExpr *) testexpr))
+               if (test_opexpr_is_hashable((OpExpr *) testexpr, param_ids))
                        return true;
        }
        else if (and_clause(testexpr))
@@ -765,7 +764,7 @@ testexpr_is_hashable(Node *testexpr)
 
                        if (!IsA(andarg, OpExpr))
                                return false;
-                       if (!hash_ok_operator((OpExpr *) andarg))
+                       if (!test_opexpr_is_hashable((OpExpr *) andarg, param_ids))
                                return false;
                }
                return true;
@@ -774,6 +773,40 @@ testexpr_is_hashable(Node *testexpr)
        return false;
 }
 
+static bool
+test_opexpr_is_hashable(OpExpr *testexpr, List *param_ids)
+{
+       /*
+        * The combining operator must be hashable and strict.  The need for
+        * hashability is obvious, since we want to use hashing.  Without
+        * strictness, behavior in the presence of nulls is too unpredictable.  We
+        * actually must assume even more than plain strictness: it can't yield
+        * NULL for non-null inputs, either (see nodeSubplan.c).  However, hash
+        * indexes and hash joins assume that too.
+        */
+       if (!hash_ok_operator(testexpr))
+               return false;
+
+       /*
+        * The left and right inputs must belong to the outer and inner queries
+        * respectively; hence Params that will be supplied by the subquery must
+        * not appear in the LHS, and Vars of the outer query must not appear in
+        * the RHS.  (Ordinarily, this must be true because of the way that the
+        * parser builds an ANY SubLink's testexpr ... but inlining of functions
+        * could have changed the expression's structure, so we have to check.
+        * Such cases do not occur often enough to be worth trying to optimize, so
+        * we don't worry about trying to commute the clause or anything like
+        * that; we just need to be sure not to build an invalid plan.)
+        */
+       if (list_length(testexpr->args) != 2)
+               return false;
+       if (contain_exec_param((Node *) linitial(testexpr->args), param_ids))
+               return false;
+       if (contain_var_clause((Node *) lsecond(testexpr->args)))
+               return false;
+       return true;
+}
+
 /*
  * Check expression is hashable + strict
  *
index fa49b1c525be7a31c546f040ddec51b63d8f137d..02866f363d6fa31e8b287283e5d01754453fe585 100644 (file)
@@ -106,6 +106,7 @@ static bool contain_volatile_functions_not_nextval_walker(Node *node, void *cont
 static bool has_parallel_hazard_walker(Node *node,
                                                   has_parallel_hazard_arg *context);
 static bool contain_nonstrict_functions_walker(Node *node, void *context);
+static bool contain_exec_param_walker(Node *node, List *param_ids);
 static bool contain_context_dependent_node(Node *clause);
 static bool contain_context_dependent_node_walker(Node *node, int *flags);
 static bool contain_leaked_vars_walker(Node *node, void *context);
@@ -1360,6 +1361,40 @@ contain_nonstrict_functions_walker(Node *node, void *context)
                                                                  context);
 }
 
+/*****************************************************************************
+ *             Check clauses for Params
+ *****************************************************************************/
+
+/*
+ * contain_exec_param
+ *       Recursively search for PARAM_EXEC Params within a clause.
+ *
+ * Returns true if the clause contains any PARAM_EXEC Param with a paramid
+ * appearing in the given list of Param IDs.  Does not descend into
+ * subqueries!
+ */
+bool
+contain_exec_param(Node *clause, List *param_ids)
+{
+       return contain_exec_param_walker(clause, param_ids);
+}
+
+static bool
+contain_exec_param_walker(Node *node, List *param_ids)
+{
+       if (node == NULL)
+               return false;
+       if (IsA(node, Param))
+       {
+               Param      *p = (Param *) node;
+
+               if (p->paramkind == PARAM_EXEC &&
+                       list_member_int(param_ids, p->paramid))
+                       return true;
+       }
+       return expression_tree_walker(node, contain_exec_param_walker, param_ids);
+}
+
 /*****************************************************************************
  *             Check clauses for context-dependent nodes
  *****************************************************************************/
index f303c951530e48ca63fd8e5f30278bdcbc104500..d7a376e835783086d89fef903be3413ea91c590e 100644 (file)
@@ -782,11 +782,13 @@ typedef struct SubPlanState
        MemoryContext hashtablecxt; /* memory context containing hash tables */
        MemoryContext hashtempcxt;      /* temp memory context for hash tables */
        ExprContext *innerecontext; /* econtext for computing inner tuples */
+       /* each of the following fields is an array of length numCols: */
        AttrNumber *keyColIdx;          /* control data for hash tables */
        FmgrInfo   *tab_hash_funcs; /* hash functions for table datatype(s) */
        FmgrInfo   *tab_eq_funcs;       /* equality functions for table datatype(s) */
        FmgrInfo   *lhs_hash_funcs; /* hash functions for lefthand datatype(s) */
        FmgrInfo   *cur_eq_funcs;       /* equality functions for LHS vs. table */
+       int                     numCols;                /* number of columns being hashed */
 } SubPlanState;
 
 /* ----------------
index be7c639f7b93a6b21de532748f98b886960474d8..1662445dbcabd65560066b0170442717321ac53b 100644 (file)
@@ -63,6 +63,7 @@ extern bool contain_volatile_functions(Node *clause);
 extern bool contain_volatile_functions_not_nextval(Node *clause);
 extern bool has_parallel_hazard(Node *node, bool allow_restricted);
 extern bool contain_nonstrict_functions(Node *clause);
+extern bool contain_exec_param(Node *clause, List *param_ids);
 extern bool contain_leaked_vars(Node *clause);
 
 extern Relids find_nonnullable_rels(Node *clause);
index 363755edad745926672ab4f782df18391c5023e9..91558cfb9e1940576a8621b0d871720638af462a 100644 (file)
@@ -709,6 +709,85 @@ select '1'::text in (select '1'::name union all select '1'::name);
  t
 (1 row)
 
+--
+-- Test that we don't try to use a hashed subplan if the simplified
+-- testexpr isn't of the right shape
+--
+create temp table inner_text (c1 text, c2 text);
+insert into inner_text values ('a', null);
+insert into inner_text values ('123', '456');
+-- this fails by default, of course
+select * from int8_tbl where q1 in (select c1 from inner_text);
+ERROR:  operator does not exist: bigint = text
+LINE 1: select * from int8_tbl where q1 in (select c1 from inner_tex...
+                                        ^
+HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
+begin;
+-- make an operator to allow it to succeed
+create function bogus_int8_text_eq(int8, text) returns boolean
+language sql as 'select $1::text = $2';
+create operator = (procedure=bogus_int8_text_eq, leftarg=int8, rightarg=text);
+explain (costs off)
+select * from int8_tbl where q1 in (select c1 from inner_text);
+           QUERY PLAN           
+--------------------------------
+ Seq Scan on int8_tbl
+   Filter: (hashed SubPlan 1)
+   SubPlan 1
+     ->  Seq Scan on inner_text
+(4 rows)
+
+select * from int8_tbl where q1 in (select c1 from inner_text);
+ q1  |        q2        
+-----+------------------
+ 123 |              456
+ 123 | 4567890123456789
+(2 rows)
+
+-- inlining of this function results in unusual number of hash clauses,
+-- which we can still cope with
+create or replace function bogus_int8_text_eq(int8, text) returns boolean
+language sql as 'select $1::text = $2 and $1::text = $2';
+explain (costs off)
+select * from int8_tbl where q1 in (select c1 from inner_text);
+           QUERY PLAN           
+--------------------------------
+ Seq Scan on int8_tbl
+   Filter: (hashed SubPlan 1)
+   SubPlan 1
+     ->  Seq Scan on inner_text
+(4 rows)
+
+select * from int8_tbl where q1 in (select c1 from inner_text);
+ q1  |        q2        
+-----+------------------
+ 123 |              456
+ 123 | 4567890123456789
+(2 rows)
+
+-- inlining of this function causes LHS and RHS to be switched,
+-- which we can't cope with, so hashing should be abandoned
+create or replace function bogus_int8_text_eq(int8, text) returns boolean
+language sql as 'select $2 = $1::text';
+explain (costs off)
+select * from int8_tbl where q1 in (select c1 from inner_text);
+              QUERY PLAN              
+--------------------------------------
+ Seq Scan on int8_tbl
+   Filter: (SubPlan 1)
+   SubPlan 1
+     ->  Materialize
+           ->  Seq Scan on inner_text
+(5 rows)
+
+select * from int8_tbl where q1 in (select c1 from inner_text);
+ q1  |        q2        
+-----+------------------
+ 123 |              456
+ 123 | 4567890123456789
+(2 rows)
+
+rollback;  -- to get rid of the bogus operator
 --
 -- Test case for planner bug with nested EXISTS handling
 --
index fd74fbc6fa426b3a06564cd68c6ae176c830aecb..ee8fdf2916da48d07d9cc3a44e579266a96a00d2 100644 (file)
@@ -413,6 +413,50 @@ select * from outer_7597 where (f1, f2) not in (select * from inner_7597);
 
 select '1'::text in (select '1'::name union all select '1'::name);
 
+--
+-- Test that we don't try to use a hashed subplan if the simplified
+-- testexpr isn't of the right shape
+--
+
+create temp table inner_text (c1 text, c2 text);
+insert into inner_text values ('a', null);
+insert into inner_text values ('123', '456');
+
+-- this fails by default, of course
+select * from int8_tbl where q1 in (select c1 from inner_text);
+
+begin;
+
+-- make an operator to allow it to succeed
+create function bogus_int8_text_eq(int8, text) returns boolean
+language sql as 'select $1::text = $2';
+
+create operator = (procedure=bogus_int8_text_eq, leftarg=int8, rightarg=text);
+
+explain (costs off)
+select * from int8_tbl where q1 in (select c1 from inner_text);
+select * from int8_tbl where q1 in (select c1 from inner_text);
+
+-- inlining of this function results in unusual number of hash clauses,
+-- which we can still cope with
+create or replace function bogus_int8_text_eq(int8, text) returns boolean
+language sql as 'select $1::text = $2 and $1::text = $2';
+
+explain (costs off)
+select * from int8_tbl where q1 in (select c1 from inner_text);
+select * from int8_tbl where q1 in (select c1 from inner_text);
+
+-- inlining of this function causes LHS and RHS to be switched,
+-- which we can't cope with, so hashing should be abandoned
+create or replace function bogus_int8_text_eq(int8, text) returns boolean
+language sql as 'select $2 = $1::text';
+
+explain (costs off)
+select * from int8_tbl where q1 in (select c1 from inner_text);
+select * from int8_tbl where q1 in (select c1 from inner_text);
+
+rollback;  -- to get rid of the bogus operator
+
 --
 -- Test case for planner bug with nested EXISTS handling
 --