I discussed this hack with Neil and Josh in Ottawa back in May. I
took a stab at it a few weeks ago and below are the results. It is
mostly modeled after the existing inline_function. The approach taken
is to recursively cleanup the rtable.
I've been testing this in our dev environment for a few weeks now.
I'm sure it still has issues but I think it is about as clean as I can
get it without help from a wider audience. This is my first backend
patch so please be gentle.
I want to thank Neil for answering many of my supremely ignorant
questions on IRC.
diff -r -c3 postgres-8.3/src/backend/optimizer/plan/planner.c postgres-8.3-hacked/src/backend/optimizer/plan/planner.c
*** postgres-8.3/src/backend/optimizer/plan/planner.c 2007-05-26 14:23:01.000000000 -0400
--- postgres-8.3-hacked/src/backend/optimizer/plan/planner.c 2007-10-01 11:49:08.000000000 -0400
***************
*** 256,261 ****
--- 256,268 ----
parse->jointree->quals = pull_up_IN_clauses(root,
parse->jointree->quals);
+
+ /*
+ * Examine the rtable and inline any stable SQL set returning functions
+ * if possible.
+ */
+ parse->rtable = inline_stable_sql_funcs(parse->rtable);
+
/*
* Check to see if any subqueries in the rangetable can be merged into
* this query.
diff -r -c3 postgres-8.3/src/backend/optimizer/util/clauses.c postgres-8.3-hacked/src/backend/optimizer/util/clauses.c
*** postgres-8.3/src/backend/optimizer/util/clauses.c 2007-09-06 13:31:58.000000000 -0400
--- postgres-8.3-hacked/src/backend/optimizer/util/clauses.c 2007-10-01 11:49:57.000000000 -0400
***************
*** 101,106 ****
--- 101,117 ----
static void sql_inline_error_callback(void *arg);
static Expr *evaluate_expr(Expr *expr, Oid result_type, int32 result_typmod);
+ static Node *
+ substitute_actual_parameters_srf_inliner_mutator(Node *node,
+ substitute_actual_parameters_context *context);
+ static Query *
+ substitute_actual_parameters_srf_inliner(Query *querytree, int nargs, List *args,
+ int *usecounts);
+ static List * inline_stable_sql_funcs_mutator( List* rtable, eval_const_expressions_context *context);
+ static Query* inline_stable_sql_func(FuncExpr *expr, eval_const_expressions_context *context);
+ List * inline_stable_sql_funcs(List* rtable);
+
+
/*****************************************************************************
* OPERATOR clause functions
***************
*** 4419,4421 ****
--- 4430,4792 ----
else
return mutator(node, context);
}
+
+ /*
+ * Replace Param nodes with the actual arguments passed to the function.
+ */
+ static Query *
+ substitute_actual_parameters_srf_inliner(Query *querytree, int nargs, List *args,
+ int *usecounts)
+ {
+ substitute_actual_parameters_context context;
+
+ context.nargs = nargs;
+ context.args = args;
+ context.usecounts = usecounts;
+
+ return query_tree_mutator(querytree, substitute_actual_parameters_srf_inliner_mutator, &context, 0);
+ }
+
+
+ /*
+ * This mutator cleanses the RTable of function body's to be inlined. Each parameter reference
+ * in the function body must be replaced with the appropriate function argument. Sub-queries
+ * are recursed, and sublink parameters are copied as is.
+ */
+ static Node *
+ substitute_actual_parameters_srf_inliner_mutator(Node *node,
+ substitute_actual_parameters_context *context)
+ {
+ if (node == NULL)
+ return NULL;
+ if (IsA(node, Query))
+ {
+ Query *nquery;
+
+ nquery = query_tree_mutator((Query *) node, substitute_actual_parameters_srf_inliner_mutator, context, 0);
+ return (Node *) nquery;
+ }
+ if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ if(param->paramkind == PARAM_SUBLINK )
+ {
+ return copyObject( node );
+ }
+ if (param->paramkind != PARAM_SUBLINK && param->paramkind != PARAM_EXTERN)
+ {
+ elog(ERROR, "unhanled parameter type");
+ }
+ if (param->paramid <= 0 || param->paramid > context->nargs)
+ elog(ERROR, "invalid paramid: %d", param->paramid);
+
+ /* Count usage of parameter */
+ context->usecounts[param->paramid - 1]++;
+
+ /* Select the appropriate actual arg and replace the Param with it */
+ /* We don't need to copy at this time (it'll get done later) */
+ return list_nth(context->args, param->paramid - 1);
+ }
+ return expression_tree_mutator(node, substitute_actual_parameters_srf_inliner_mutator,
+ (void *) context);
+ }
+
+ /*
+ * Inline stable SQL set-returning functions
+ *
+ *
+ * Takes a function expression as a parameter. If the FuncExpr
+ * is that of a stable SQL set-returning function then we can
+ * replace the function's RTE with a subquery RTE containing the
+ * body of the function. We also recurse into the subqueries RTE
+ * looking for inlineable functions within those.
+ *
+ * Returns the inlined function body (Query) if successfull, NULL on failure.
+ */
+ static Query *
+ inline_stable_sql_func(FuncExpr *expr, eval_const_expressions_context *context)
+ {
+ Form_pg_proc funcform;
+ HeapTuple func_tuple;
+ Oid *argtypes;
+ char *src;
+ Datum tmp;
+ bool isNull;
+ MemoryContext oldcxt;
+ MemoryContext mycxt;
+ ErrorContextCallback sqlerrcontext;
+ List *rewritten_parsetree_list;
+ Query *querytree;
+ int *usecounts;
+ ListCell *arg;
+ int i;
+
+
+ /* Check for recursive function, and give up trying to expand if so */
+ if (list_member_oid(context->active_fns, expr->funcid))
+ return NULL;
+
+ /* Check permission to call function (fail later, if not) */
+ if (pg_proc_aclcheck(expr->funcid, GetUserId(), ACL_EXECUTE) != ACLCHECK_OK)
+ return NULL;
+
+
+ func_tuple = SearchSysCache(PROCOID, ObjectIdGetDatum(expr->funcid), 0, 0, 0);
+
+ /*
+ * Setup error traceback support for ereport(). This is so that we can
+ * finger the function that bad information came from.
+ */
+ sqlerrcontext.callback = sql_inline_error_callback;
+ sqlerrcontext.arg = func_tuple;
+ sqlerrcontext.previous = error_context_stack;
+ error_context_stack = &sqlerrcontext;
+
+ if (!HeapTupleIsValid(func_tuple))
+ {
+ elog(ERROR, "cache lookup failed for function %u", expr->funcid);
+ ReleaseSysCache(func_tuple);
+ error_context_stack = sqlerrcontext.previous;
+ return NULL;
+ }
+
+
+ funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
+
+ /*
+ * If the function is not SQL, doesn't return a set, or isn't stable then
+ * get out now
+ */
+ if (funcform->prolang != SQLlanguageId ||
+ !funcform->proretset ||
+ funcform->provolatile != PROVOLATILE_STABLE)
+ {
+ ReleaseSysCache(func_tuple);
+ error_context_stack = sqlerrcontext.previous;
+ return NULL;
+ }
+
+ /* If this is the top-level function then record the context */
+ mycxt = AllocSetContextCreate(CurrentMemoryContext,
+ "inline_stable_sql_func",
+ ALLOCSET_DEFAULT_MINSIZE,
+ ALLOCSET_DEFAULT_INITSIZE,
+ ALLOCSET_DEFAULT_MAXSIZE);
+ oldcxt = MemoryContextSwitchTo(mycxt);
+
+ /* Check for polymorphic arguments, and substitute actual arg types */
+ argtypes = (Oid *) palloc(funcform->pronargs * sizeof(Oid));
+ memcpy(argtypes, funcform->proargtypes.values,
+ funcform->pronargs * sizeof(Oid));
+ for (i = 0; i < funcform->pronargs; i++)
+ {
+ if (IsPolymorphicType(argtypes[i]))
+ {
+ argtypes[i] = exprType((Node *) list_nth(expr->args, i));
+ }
+ }
+
+ /* Fetch and parse and re-write the function body */
+ tmp = SysCacheGetAttr(PROCOID,
+ func_tuple,
+ Anum_pg_proc_prosrc,
+ &isNull);
+ if (isNull)
+ elog(ERROR, "null prosrc for function %u", expr->funcid);
+ src = DatumGetCString(DirectFunctionCall1(textout, tmp));
+
+ /*
+ * We punt if we detect more than one command in the function body.
+ */
+ rewritten_parsetree_list = pg_parse_and_rewrite(src, argtypes, funcform->pronargs);
+ if (list_length(rewritten_parsetree_list) != 1)
+ goto fail;
+
+ querytree = (linitial(rewritten_parsetree_list));
+
+ /*
+ * Assure that the function body is a straight SELECT statement.
+ *
+ * REVIEWER: We could probably use more sanity checks here, but certainly
+ * less then those in inline_function.
+ *
+ * I do use limit and order by in functions to pull the "top" rows in some
+ * cases and I'd hate to see those critical functions lost down the
+ * performance hole that is a function_scan. In general since we are
+ * translating into a subquery would not most anything acceptable in a
+ * subquery fly?
+ */
+ if (!IsA(querytree, Query) ||
+ querytree->commandType != CMD_SELECT ||
+ querytree->utilityStmt ||
+ querytree->intoClause ||
+ querytree->hasAggs)
+ {
+ goto fail;
+ }
+
+ usecounts = (int *) palloc0(funcform->pronargs * sizeof(int));
+
+ /*
+ * we substitute all params in the entire function querytree as the
+ * function body may contain subqueries, and there may be paramaters in
+ * the jointree, etc
+ */
+ querytree = substitute_actual_parameters_srf_inliner(querytree, funcform->pronargs, expr->args, usecounts);
+
+
+ /* Now check for parameter usage */
+
+ /*
+ * REVIEWER: Is this enough sanity checking? This is unchanged from
+ * inline_function, is it enough here?
+ */
+ i = 0;
+ foreach(arg, expr->args)
+ {
+ Node *param = lfirst(arg);
+
+ if (usecounts[i] == 0)
+ {
+ /* Param not used at all: uncool if func is strict */
+ if (funcform->proisstrict)
+ goto fail;
+ }
+ else if (usecounts[i] != 1)
+ {
+ /* Param used multiple times: uncool if expensive or volatile */
+ QualCost eval_cost;
+
+ /*
+ * We define "expensive" as "contains any subplan or more than 10
+ * operators". Note that the subplan search has to be done
+ * explicitly, since cost_qual_eval() will barf on unplanned
+ * subselects.
+ */
+ if (contain_subplans(param))
+ goto fail;
+ cost_qual_eval(&eval_cost, list_make1(param), NULL);
+ if (eval_cost.startup + eval_cost.per_tuple >
+ 10 * cpu_operator_cost)
+ goto fail;
+
+ /*
+ * Check volatility last since this is more expensive than the
+ * above tests
+ */
+ if (contain_volatile_functions(param))
+ goto fail;
+ }
+ i++;
+ }
+ ReleaseSysCache(func_tuple);
+
+ /* scan the function body for inlinable functions and recurse */
+ context->active_fns = lcons_oid(expr->funcid, context->active_fns);
+ querytree->rtable = inline_stable_sql_funcs_mutator(querytree->rtable, context);
+ context->active_fns = list_delete_first(context->active_fns);
+
+ /*
+ * Whew --- we can make the substitution. Copy the modified expression
+ * out of the temporary memory context, and clean up.
+ */
+ MemoryContextSwitchTo(oldcxt);
+
+ querytree = copyObject(querytree);
+
+ MemoryContextDelete(mycxt);
+ error_context_stack = sqlerrcontext.previous;
+
+ return querytree;
+
+ fail:
+ ReleaseSysCache(func_tuple);
+ MemoryContextSwitchTo(oldcxt);
+ MemoryContextDelete(mycxt);
+ error_context_stack = sqlerrcontext.previous;
+ return NULL;
+ }
+
+ /*
+ * Walk the range-table inlining stable SQL set functions along the way
+ *
+ * Based heavily off of range_table_mutator which we can not use
+ * since we are transforming some RTE_FUNCTION nodes into RTE_SUBQUERY nodes.
+ * Scans the range table for inlinable function expressions. For stable SQL set
+ * functions we replace the function's RTE with a RTE_SUBQUERY containing the
+ * body of the function. We also recurse into the subqueries RTE
+ * looking for inlineable functions within those.
+ */
+ List *
+ inline_stable_sql_funcs(List *rtable)
+ {
+ eval_const_expressions_context context;
+
+ context.boundParams = NULL; /* don't use any bound params */
+ context.active_fns = NIL; /* nothing being recursively simplified */
+ context.case_val = NULL; /* no CASE being examined */
+ context.estimate = false; /* safe transformations only */
+
+ return inline_stable_sql_funcs_mutator(rtable, &context);
+ }
+
+ static List *
+ inline_stable_sql_funcs_mutator(List *rtable, eval_const_expressions_context *context)
+ {
+ List *newrt = NIL;
+ ListCell *rt;
+ Query *func_query;
+
+ foreach(rt, rtable)
+ {
+ RangeTblEntry *rte = (RangeTblEntry *) lfirst(rt);
+ RangeTblEntry *newrte;
+
+ FLATCOPY(newrte, rte, RangeTblEntry);
+ switch (rte->rtekind)
+ {
+ case RTE_RELATION:
+ case RTE_SPECIAL:
+ /* we don't bother to copy eref, aliases, etc; OK? */
+ break;
+ case RTE_SUBQUERY:
+ {
+ newrte->subquery = copyObject(rte->subquery);
+
+ /*
+ * recurse into sub-queries to inline any stable SQL
+ * functions there
+ */
+
+ newrte->subquery->rtable = inline_stable_sql_funcs_mutator(newrte->subquery->rtable, context);
+ }
+ break;
+ case RTE_JOIN:
+ {
+ /* copy join aliases as-is */
+ newrte->joinaliasvars = copyObject(rte->joinaliasvars);
+ }
+ break;
+ case RTE_FUNCTION:
+ {
+ func_query = inline_stable_sql_func((FuncExpr *) rte->funcexpr, context);
+ if (func_query != NULL)
+ {
+ newrte->rtekind = RTE_SUBQUERY;
+ newrte->subquery = func_query;
+ }
+ else
+ {
+ newrte->funcexpr = copyObject(rte->funcexpr);
+ }
+ }
+ break;
+ case RTE_VALUES:
+ newrte->values_lists = copyObject(rte->values_lists);
+ break;
+ }
+ newrt = lappend(newrt, newrte);
+ }
+ return newrt;
+ }
diff -r -c3 postgres-8.3/src/include/optimizer/prep.h postgres-8.3-hacked/src/include/optimizer/prep.h
*** postgres-8.3/src/include/optimizer/prep.h 2007-01-05 17:19:56.000000000 -0500
--- postgres-8.3-hacked/src/include/optimizer/prep.h 2007-09-19 23:18:21.000000000 -0400
***************
*** 24,29 ****
--- 24,32 ----
extern Node *pull_up_IN_clauses(PlannerInfo *root, Node *node);
extern Node *pull_up_subqueries(PlannerInfo *root, Node *jtnode,
bool below_outer_join, bool append_rel_member);
+
+ extern List *inline_stable_sql_funcs( List* rtable );
+
extern void reduce_outer_joins(PlannerInfo *root);
extern Relids get_relids_in_jointree(Node *jtnode);
extern Relids get_relids_for_join(PlannerInfo *root, int joinrelid);
---------------------------(end of broadcast)---------------------------
TIP 7: You can help support the PostgreSQL project by donating at
http://www.postgresql.org/about/donate