Commit 7266d099 authored by Tom Lane's avatar Tom Lane

Allow functions-in-FROM to be pulled up if they reduce to constants.

This allows simplification of the plan tree in some common usage
patterns: we can get rid of a join to the function RTE.

In principle we could pull up any immutable expression, but restricting
it to Consts avoids the risk that multiple evaluations of the expression
might cost more than we can save.  (Possibly this could be improved in
future --- but we've more or less promised people that putting a function
in FROM guarantees single evaluation, so we'd have to tread carefully.)

To do this, we need to rearrange when eval_const_expressions()
happens for expressions in function RTEs.  I moved it to
inline_set_returning_functions(), which already has to iterate over
every function RTE, and in consequence renamed that function to
preprocess_function_rtes().  A useful consequence is that
inline_set_returning_function() no longer has to do this for itself,
simplifying that code.

In passing, break out pull_up_simple_subquery's code that knows where
everything that needs pullup_replace_vars() processing is, so that
the new pull_up_constant_function() routine can share it.  We'd
gotten away with one-and-a-half copies of that code so far, since
pull_up_simple_values() could assume that a lot of cases didn't apply
to it --- but I don't think pull_up_constant_function() can make any
simplifying assumptions.  Might as well make pull_up_simple_values()
use it too.

(Possibly this refactoring should go further: maybe we could share
some of the code to fill in the pullup_replace_vars_context struct?
For now, I left it that the callers fill that completely.)

Note: the one existing test case that this patch changes has to be
changed because inlining its function RTEs would destroy the point
of the test, namely to check join order.

Alexander Kuzmenkov and Aleksandr Parfenov, reviewed by
Antonin Houska and Anastasia Lubennikova, and whacked around
some more by me

Discussion: https://postgr.es/m/402356c32eeb93d4fed01f66d6c7fe2d@postgrespro.ru
parent a8d6a95e
......@@ -659,11 +659,12 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
pull_up_sublinks(root);
/*
* Scan the rangetable for set-returning functions, and inline them if
* possible (producing subqueries that might get pulled up next).
* Recursion issues here are handled in the same way as for SubLinks.
* Scan the rangetable for function RTEs, do const-simplification on them,
* and then inline them if possible (producing subqueries that might get
* pulled up next). Recursion issues here are handled in the same way as
* for SubLinks.
*/
inline_set_returning_functions(root);
preprocess_function_rtes(root);
/*
* Check to see if any subqueries in the jointree can be merged into this
......@@ -1071,7 +1072,9 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind)
expr = flatten_join_alias_vars(root->parse, expr);
/*
* Simplify constant expressions.
* Simplify constant expressions. For function RTEs, this was already
* done by preprocess_function_rtes ... but we have to do it again if the
* RTE is LATERAL and might have contained join alias variables.
*
* Note: an essential effect of this is to convert named-argument function
* calls to positional notation and insert the current actual values of
......@@ -1085,6 +1088,8 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind)
* careful to maintain AND/OR flatness --- that is, do not generate a tree
* with AND directly under AND, nor OR directly under OR.
*/
if (!(kind == EXPRKIND_RTFUNC ||
(kind == EXPRKIND_RTFUNC_LATERAL && !root->hasJoinRTEs)))
expr = eval_const_expressions(root, expr);
/*
......
This diff is collapsed.
......@@ -4870,6 +4870,10 @@ evaluate_expr(Expr *expr, Oid result_type, int32 result_typmod,
* set-returning SQL function that can safely be inlined, expand the function
* and return the substitute Query structure. Otherwise, return NULL.
*
* We assume that the RTE's expression has already been put through
* eval_const_expressions(), which among other things will take care of
* default arguments and named-argument notation.
*
* This has a good deal of similarity to inline_function(), but that's
* for the non-set-returning case, and there are enough differences to
* justify separate functions.
......@@ -4888,7 +4892,6 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
bool modifyTargetList;
MemoryContext oldcxt;
MemoryContext mycxt;
List *saveInvalItems;
inline_error_callback_arg callback_arg;
ErrorContextCallback sqlerrcontext;
SQLFunctionParseInfoPtr pinfo;
......@@ -4966,7 +4969,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
* sharing the snapshot of the calling query. We also disallow returning
* SETOF VOID, because inlining would result in exposing the actual result
* of the function's last SELECT, which should not happen in that case.
* (Rechecking prokind and proretset is just paranoia.)
* (Rechecking prokind, proretset, and pronargs is just paranoia.)
*/
if (funcform->prolang != SQLlanguageId ||
funcform->prokind != PROKIND_FUNCTION ||
......@@ -4975,6 +4978,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
funcform->prorettype == VOIDOID ||
funcform->prosecdef ||
!funcform->proretset ||
list_length(fexpr->args) != funcform->pronargs ||
!heap_attisnull(func_tuple, Anum_pg_proc_proconfig, NULL))
{
ReleaseSysCache(func_tuple);
......@@ -4990,16 +4994,6 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
ALLOCSET_DEFAULT_SIZES);
oldcxt = MemoryContextSwitchTo(mycxt);
/*
* When we call eval_const_expressions below, it might try to add items to
* root->glob->invalItems. Since it is running in the temp context, those
* items will be in that context, and will need to be copied out if we're
* successful. Temporarily reset the list so that we can keep those items
* separate from the pre-existing list contents.
*/
saveInvalItems = root->glob->invalItems;
root->glob->invalItems = NIL;
/* Fetch the function body */
tmp = SysCacheGetAttr(PROCOID,
func_tuple,
......@@ -5021,24 +5015,6 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
sqlerrcontext.previous = error_context_stack;
error_context_stack = &sqlerrcontext;
/*
* Run eval_const_expressions on the function call. This is necessary to
* ensure that named-argument notation is converted to positional notation
* and any default arguments are inserted. It's a bit of overkill for the
* arguments, since they'll get processed again later, but no harm will be
* done.
*/
fexpr = (FuncExpr *) eval_const_expressions(root, (Node *) fexpr);
/* It should still be a call of the same function, but let's check */
if (!IsA(fexpr, FuncExpr) ||
fexpr->funcid != func_oid)
goto fail;
/* Arg list length should now match the function */
if (list_length(fexpr->args) != funcform->pronargs)
goto fail;
/*
* Set up to handle parameters while parsing the function body. We can
* use the FuncExpr just created as the input for
......@@ -5129,10 +5105,6 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
querytree = copyObject(querytree);
/* copy up any new invalItems, too */
root->glob->invalItems = list_concat(saveInvalItems,
copyObject(root->glob->invalItems));
MemoryContextDelete(mycxt);
error_context_stack = sqlerrcontext.previous;
ReleaseSysCache(func_tuple);
......@@ -5153,7 +5125,6 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
/* Here if func is not inlinable: release temp memory and return NULL */
fail:
MemoryContextSwitchTo(oldcxt);
root->glob->invalItems = saveInvalItems;
MemoryContextDelete(mycxt);
error_context_stack = sqlerrcontext.previous;
ReleaseSysCache(func_tuple);
......
......@@ -23,7 +23,7 @@
*/
extern void replace_empty_jointree(Query *parse);
extern void pull_up_sublinks(PlannerInfo *root);
extern void inline_set_returning_functions(PlannerInfo *root);
extern void preprocess_function_rtes(PlannerInfo *root);
extern void pull_up_subqueries(PlannerInfo *root);
extern void flatten_simple_union_all(PlannerInfo *root);
extern void reduce_outer_joins(PlannerInfo *root);
......
......@@ -3060,11 +3060,14 @@ select * from int4_tbl a full join int4_tbl b on false;
--
-- test for ability to use a cartesian join when necessary
--
create temp table q1 as select 1 as q1;
create temp table q2 as select 0 as q2;
analyze q1;
analyze q2;
explain (costs off)
select * from
tenk1 join int4_tbl on f1 = twothousand,
int4(sin(1)) q1,
int4(sin(0)) q2
q1, q2
where q1 = thousand or q2 = thousand;
QUERY PLAN
------------------------------------------------------------------------
......@@ -3072,8 +3075,8 @@ where q1 = thousand or q2 = thousand;
Hash Cond: (tenk1.twothousand = int4_tbl.f1)
-> Nested Loop
-> Nested Loop
-> Function Scan on q1
-> Function Scan on q2
-> Seq Scan on q1
-> Seq Scan on q2
-> Bitmap Heap Scan on tenk1
Recheck Cond: ((q1.q1 = thousand) OR (q2.q2 = thousand))
-> BitmapOr
......@@ -3088,8 +3091,7 @@ where q1 = thousand or q2 = thousand;
explain (costs off)
select * from
tenk1 join int4_tbl on f1 = twothousand,
int4(sin(1)) q1,
int4(sin(0)) q2
q1, q2
where thousand = (q1 + q2);
QUERY PLAN
--------------------------------------------------------------
......@@ -3097,8 +3099,8 @@ where thousand = (q1 + q2);
Hash Cond: (tenk1.twothousand = int4_tbl.f1)
-> Nested Loop
-> Nested Loop
-> Function Scan on q1
-> Function Scan on q2
-> Seq Scan on q1
-> Seq Scan on q2
-> Bitmap Heap Scan on tenk1
Recheck Cond: (thousand = (q1.q1 + q2.q2))
-> Bitmap Index Scan on tenk1_thous_tenthous
......@@ -3240,6 +3242,129 @@ where t1.unique2 < 42 and t1.stringu1 > t2.stringu2;
11 | WFAAAA | 3 | LKIAAA
(1 row)
--
-- test inlining of immutable functions
--
create function f_immutable_int4(i integer) returns integer as
$$ begin return i; end; $$ language plpgsql immutable;
-- check optimization of function scan with join
explain (costs off)
select unique1 from tenk1, (select * from f_immutable_int4(1) x) x
where x = unique1;
QUERY PLAN
----------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
Index Cond: (unique1 = 1)
(2 rows)
explain (verbose, costs off)
select unique1, x.*
from tenk1, (select *, random() from f_immutable_int4(1) x) x
where x = unique1;
QUERY PLAN
-----------------------------------------------------------
Nested Loop
Output: tenk1.unique1, (1), (random())
-> Result
Output: 1, random()
-> Index Only Scan using tenk1_unique1 on public.tenk1
Output: tenk1.unique1
Index Cond: (tenk1.unique1 = (1))
(7 rows)
explain (costs off)
select unique1 from tenk1, f_immutable_int4(1) x where x = unique1;
QUERY PLAN
----------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
Index Cond: (unique1 = 1)
(2 rows)
explain (costs off)
select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1;
QUERY PLAN
----------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
Index Cond: (unique1 = 1)
(2 rows)
explain (costs off)
select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x;
QUERY PLAN
----------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
Index Cond: (unique1 = 1)
(2 rows)
explain (costs off)
select unique1, x from tenk1 left join f_immutable_int4(1) x on unique1 = x;
QUERY PLAN
----------------------------------------------------
Nested Loop Left Join
Join Filter: (tenk1.unique1 = 1)
-> Index Only Scan using tenk1_unique1 on tenk1
-> Materialize
-> Result
(5 rows)
explain (costs off)
select unique1, x from tenk1 right join f_immutable_int4(1) x on unique1 = x;
QUERY PLAN
----------------------------------------------------
Nested Loop Left Join
-> Result
-> Index Only Scan using tenk1_unique1 on tenk1
Index Cond: (unique1 = 1)
(4 rows)
explain (costs off)
select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x;
QUERY PLAN
----------------------------------------------------
Merge Full Join
Merge Cond: (tenk1.unique1 = (1))
-> Index Only Scan using tenk1_unique1 on tenk1
-> Sort
Sort Key: (1)
-> Result
(6 rows)
-- check that pullup of a const function allows further const-folding
explain (costs off)
select unique1 from tenk1, f_immutable_int4(1) x where x = 42;
QUERY PLAN
--------------------------
Result
One-Time Filter: false
(2 rows)
-- test inlining of immutable functions with PlaceHolderVars
explain (costs off)
select nt3.id
from nt3 as nt3
left join
(select nt2.*, (nt2.b1 or i4 = 42) AS b3
from nt2 as nt2
left join
f_immutable_int4(0) i4
on i4 = nt2.nt1_id
) as ss2
on ss2.id = nt3.nt2_id
where nt3.id = 1 and ss2.b3;
QUERY PLAN
----------------------------------------------
Nested Loop Left Join
Filter: ((nt2.b1 OR ((0) = 42)))
-> Index Scan using nt3_pkey on nt3
Index Cond: (id = 1)
-> Nested Loop Left Join
Join Filter: (0 = nt2.nt1_id)
-> Index Scan using nt2_pkey on nt2
Index Cond: (id = nt3.nt2_id)
-> Result
(9 rows)
drop function f_immutable_int4(int);
--
-- test extraction of restriction OR clauses from join OR clause
-- (we used to only do this for indexable clauses)
......
......@@ -1625,6 +1625,28 @@ SELECT count(*) FROM test_tsvector WHERE a @@ to_tsquery('345&qwerty');
1
(1 row)
-- Test inlining of immutable constant functions
-- to_tsquery(text) is not immutable, so it won't be inlined
explain (costs off)
select * from test_tsquery, to_tsquery('new') q where txtsample @@ q;
QUERY PLAN
------------------------------------------------
Nested Loop
Join Filter: (test_tsquery.txtsample @@ q.q)
-> Function Scan on to_tsquery q
-> Seq Scan on test_tsquery
(4 rows)
-- to_tsquery(regconfig, text) is an immutable function.
-- That allows us to get rid of using function scan and join at all.
explain (costs off)
select * from test_tsquery, to_tsquery('english', 'new') q where txtsample @@ q;
QUERY PLAN
---------------------------------------------
Seq Scan on test_tsquery
Filter: (txtsample @@ '''new'''::tsquery)
(2 rows)
-- test finding items in GIN's pending list
create temp table pendtest (ts tsvector);
create index pendtest_idx on pendtest using gin(ts);
......
......@@ -914,18 +914,21 @@ select * from int4_tbl a full join int4_tbl b on false;
-- test for ability to use a cartesian join when necessary
--
create temp table q1 as select 1 as q1;
create temp table q2 as select 0 as q2;
analyze q1;
analyze q2;
explain (costs off)
select * from
tenk1 join int4_tbl on f1 = twothousand,
int4(sin(1)) q1,
int4(sin(0)) q2
q1, q2
where q1 = thousand or q2 = thousand;
explain (costs off)
select * from
tenk1 join int4_tbl on f1 = twothousand,
int4(sin(1)) q1,
int4(sin(0)) q2
q1, q2
where thousand = (q1 + q2);
--
......@@ -1015,6 +1018,60 @@ select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from
on (subq1.y1 = t2.unique1)
where t1.unique2 < 42 and t1.stringu1 > t2.stringu2;
--
-- test inlining of immutable functions
--
create function f_immutable_int4(i integer) returns integer as
$$ begin return i; end; $$ language plpgsql immutable;
-- check optimization of function scan with join
explain (costs off)
select unique1 from tenk1, (select * from f_immutable_int4(1) x) x
where x = unique1;
explain (verbose, costs off)
select unique1, x.*
from tenk1, (select *, random() from f_immutable_int4(1) x) x
where x = unique1;
explain (costs off)
select unique1 from tenk1, f_immutable_int4(1) x where x = unique1;
explain (costs off)
select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1;
explain (costs off)
select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x;
explain (costs off)
select unique1, x from tenk1 left join f_immutable_int4(1) x on unique1 = x;
explain (costs off)
select unique1, x from tenk1 right join f_immutable_int4(1) x on unique1 = x;
explain (costs off)
select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x;
-- check that pullup of a const function allows further const-folding
explain (costs off)
select unique1 from tenk1, f_immutable_int4(1) x where x = 42;
-- test inlining of immutable functions with PlaceHolderVars
explain (costs off)
select nt3.id
from nt3 as nt3
left join
(select nt2.*, (nt2.b1 or i4 = 42) AS b3
from nt2 as nt2
left join
f_immutable_int4(0) i4
on i4 = nt2.nt1_id
) as ss2
on ss2.id = nt3.nt2_id
where nt3.id = 1 and ss2.b3;
drop function f_immutable_int4(int);
--
-- test extraction of restriction OR clauses from join OR clause
-- (we used to only do this for indexable clauses)
......
......@@ -520,6 +520,17 @@ INSERT INTO test_tsvector (t) VALUES ('345 qwerty');
SELECT count(*) FROM test_tsvector WHERE a @@ to_tsquery('345&qwerty');
-- Test inlining of immutable constant functions
-- to_tsquery(text) is not immutable, so it won't be inlined
explain (costs off)
select * from test_tsquery, to_tsquery('new') q where txtsample @@ q;
-- to_tsquery(regconfig, text) is an immutable function.
-- That allows us to get rid of using function scan and join at all.
explain (costs off)
select * from test_tsquery, to_tsquery('english', 'new') q where txtsample @@ q;
-- test finding items in GIN's pending list
create temp table pendtest (ts tsvector);
create index pendtest_idx on pendtest using gin(ts);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment