From d4ce5a4f4c3516e88fa34c53bcc7313db90a3c08 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sun, 12 Jan 2003 22:35:29 +0000 Subject: [PATCH] Revise cost_qual_eval() to compute both startup (one-time) and per-tuple costs for expression evaluation, not only per-tuple cost as before. This extension is needed in order to deal realistically with hashed or materialized sub-selects. --- doc/src/sgml/indexcost.sgml | 7 +- src/backend/optimizer/path/costsize.c | 184 ++++++++++++++++++------- src/backend/optimizer/plan/initsplan.c | 4 +- src/backend/optimizer/prep/prepunion.c | 4 +- src/backend/optimizer/util/relnode.c | 8 +- src/backend/utils/adt/selfuncs.c | 8 +- src/include/nodes/relation.h | 16 ++- src/include/optimizer/cost.h | 4 +- 8 files changed, 164 insertions(+), 71 deletions(-) diff --git a/doc/src/sgml/indexcost.sgml b/doc/src/sgml/indexcost.sgml index 63cf8daf22..2e62280a8b 100644 --- a/doc/src/sgml/indexcost.sgml +++ b/doc/src/sgml/indexcost.sgml @@ -1,5 +1,5 @@ @@ -237,9 +237,10 @@ amcostestimate (Query *root, * Also, we charge for evaluation of the indexquals at each index tuple. * All the costs are assumed to be paid incrementally during the scan. */ - *indexStartupCost = 0; + cost_qual_eval(&index_qual_cost, indexQuals); + *indexStartupCost = index_qual_cost.startup; *indexTotalCost = numIndexPages + - (cpu_index_tuple_cost + cost_qual_eval(indexQuals)) * numIndexTuples; + (cpu_index_tuple_cost + index_qual_cost.per_tuple) * numIndexTuples; diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 29b23948df..1d736b34b9 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -42,7 +42,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $Header: /cvsroot/pgsql/src/backend/optimizer/path/costsize.c,v 1.98 2002/12/30 15:21:21 tgl Exp $ + * $Header: /cvsroot/pgsql/src/backend/optimizer/path/costsize.c,v 1.99 2003/01/12 22:35:29 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -87,7 +87,7 @@ bool enable_hashjoin = true; static Selectivity estimate_hash_bucketsize(Query *root, Var *var, int nbuckets); -static bool cost_qual_eval_walker(Node *node, Cost *total); +static bool cost_qual_eval_walker(Node *node, QualCost *total); static Selectivity approx_selectivity(Query *root, List *quals); static void set_rel_width(Query *root, RelOptInfo *rel); static double relation_byte_size(double tuples, int width); @@ -131,7 +131,8 @@ cost_seqscan(Path *path, Query *root, run_cost += baserel->pages; /* sequential fetches with cost 1.0 */ /* CPU costs */ - cpu_per_tuple = cpu_tuple_cost + baserel->baserestrictcost; + startup_cost += baserel->baserestrictcost.startup; + cpu_per_tuple = cpu_tuple_cost + baserel->baserestrictcost.per_tuple; run_cost += cpu_per_tuple * baserel->tuples; path->startup_cost = startup_cost; @@ -344,17 +345,25 @@ cost_index(Path *path, Query *root, * * Normally the indexquals will be removed from the list of restriction * clauses that we have to evaluate as qpquals, so we should subtract - * their costs from baserestrictcost. XXX For a lossy index, not all - * the quals will be removed and so we really shouldn't subtract their - * costs; but detecting that seems more expensive than it's worth. - * Also, if we are doing a join then some of the indexquals are join - * clauses and shouldn't be subtracted. Rather than work out exactly - * how much to subtract, we don't subtract anything. + * their costs from baserestrictcost. But if we are doing a join then + * some of the indexquals are join clauses and shouldn't be subtracted. + * Rather than work out exactly how much to subtract, we don't subtract + * anything. + * + * XXX For a lossy index, not all the quals will be removed and so we + * really shouldn't subtract their costs; but detecting that seems more + * expensive than it's worth. */ - cpu_per_tuple = cpu_tuple_cost + baserel->baserestrictcost; + startup_cost += baserel->baserestrictcost.startup; + cpu_per_tuple = cpu_tuple_cost + baserel->baserestrictcost.per_tuple; if (!is_injoin) - cpu_per_tuple -= cost_qual_eval(indexQuals); + { + QualCost index_qual_cost; + + cost_qual_eval(&index_qual_cost, indexQuals); + cpu_per_tuple -= index_qual_cost.per_tuple; + } run_cost += cpu_per_tuple * tuples_fetched; @@ -386,7 +395,8 @@ cost_tidscan(Path *path, Query *root, run_cost += random_page_cost * ntuples; /* CPU costs */ - cpu_per_tuple = cpu_tuple_cost + baserel->baserestrictcost; + startup_cost += baserel->baserestrictcost.startup; + cpu_per_tuple = cpu_tuple_cost + baserel->baserestrictcost.per_tuple; run_cost += cpu_per_tuple * ntuples; path->startup_cost = startup_cost; @@ -416,7 +426,8 @@ cost_functionscan(Path *path, Query *root, RelOptInfo *baserel) cpu_per_tuple = cpu_operator_cost; /* Add scanning CPU costs */ - cpu_per_tuple += cpu_tuple_cost + baserel->baserestrictcost; + startup_cost += baserel->baserestrictcost.startup; + cpu_per_tuple += cpu_tuple_cost + baserel->baserestrictcost.per_tuple; run_cost += cpu_per_tuple * baserel->tuples; path->startup_cost = startup_cost; @@ -656,6 +667,7 @@ cost_nestloop(Path *path, Query *root, Cost startup_cost = 0; Cost run_cost = 0; Cost cpu_per_tuple; + QualCost restrict_qual_cost; double ntuples; if (!enable_nestloop) @@ -703,7 +715,9 @@ cost_nestloop(Path *path, Query *root, ntuples *= outer_path->parent->rows; /* CPU costs */ - cpu_per_tuple = cpu_tuple_cost + cost_qual_eval(restrictlist); + cost_qual_eval(&restrict_qual_cost, restrictlist); + startup_cost += restrict_qual_cost.startup; + cpu_per_tuple = cpu_tuple_cost + restrict_qual_cost.per_tuple; run_cost += cpu_per_tuple * ntuples; path->startup_cost = startup_cost; @@ -736,6 +750,7 @@ cost_mergejoin(Path *path, Query *root, Cost startup_cost = 0; Cost run_cost = 0; Cost cpu_per_tuple; + QualCost restrict_qual_cost; RestrictInfo *firstclause; Var *leftvar; double outer_rows, @@ -850,7 +865,9 @@ cost_mergejoin(Path *path, Query *root, outer_path->parent->rows * inner_path->parent->rows; /* CPU costs */ - cpu_per_tuple = cpu_tuple_cost + cost_qual_eval(restrictlist); + cost_qual_eval(&restrict_qual_cost, restrictlist); + startup_cost += restrict_qual_cost.startup; + cpu_per_tuple = cpu_tuple_cost + restrict_qual_cost.per_tuple; run_cost += cpu_per_tuple * ntuples; path->startup_cost = startup_cost; @@ -878,6 +895,7 @@ cost_hashjoin(Path *path, Query *root, Cost startup_cost = 0; Cost run_cost = 0; Cost cpu_per_tuple; + QualCost restrict_qual_cost; double ntuples; double outerbytes = relation_byte_size(outer_path->parent->rows, outer_path->parent->width); @@ -984,7 +1002,9 @@ cost_hashjoin(Path *path, Query *root, outer_path->parent->rows * inner_path->parent->rows; /* CPU costs */ - cpu_per_tuple = cpu_tuple_cost + cost_qual_eval(restrictlist); + cost_qual_eval(&restrict_qual_cost, restrictlist); + startup_cost += restrict_qual_cost.startup; + cpu_per_tuple = cpu_tuple_cost + restrict_qual_cost.per_tuple; run_cost += cpu_per_tuple * ntuples; /* @@ -1185,16 +1205,20 @@ estimate_hash_bucketsize(Query *root, Var *var, int nbuckets) /* * cost_qual_eval - * Estimate the CPU cost of evaluating a WHERE clause (once). + * Estimate the CPU costs of evaluating a WHERE clause. * The input can be either an implicitly-ANDed list of boolean * expressions, or a list of RestrictInfo nodes. + * The result includes both a one-time (startup) component, + * and a per-evaluation component. */ -Cost -cost_qual_eval(List *quals) +void +cost_qual_eval(QualCost *cost, List *quals) { - Cost total = 0; List *l; + cost->startup = 0; + cost->per_tuple = 0; + /* We don't charge any cost for the implicit ANDing at top level ... */ foreach(l, quals) @@ -1205,31 +1229,32 @@ cost_qual_eval(List *quals) * RestrictInfo nodes contain an eval_cost field reserved for this * routine's use, so that it's not necessary to evaluate the qual * clause's cost more than once. If the clause's cost hasn't been - * computed yet, the field will contain -1. + * computed yet, the field's startup value will contain -1. */ if (qual && IsA(qual, RestrictInfo)) { RestrictInfo *restrictinfo = (RestrictInfo *) qual; - if (restrictinfo->eval_cost < 0) + if (restrictinfo->eval_cost.startup < 0) { - restrictinfo->eval_cost = 0; + restrictinfo->eval_cost.startup = 0; + restrictinfo->eval_cost.per_tuple = 0; cost_qual_eval_walker((Node *) restrictinfo->clause, &restrictinfo->eval_cost); } - total += restrictinfo->eval_cost; + cost->startup += restrictinfo->eval_cost.startup; + cost->per_tuple += restrictinfo->eval_cost.per_tuple; } else { /* If it's a bare expression, must always do it the hard way */ - cost_qual_eval_walker(qual, &total); + cost_qual_eval_walker(qual, cost); } } - return total; } static bool -cost_qual_eval_walker(Node *node, Cost *total) +cost_qual_eval_walker(Node *node, QualCost *total) { if (node == NULL) return false; @@ -1246,43 +1271,96 @@ cost_qual_eval_walker(Node *node, Cost *total) if (IsA(node, FuncExpr) || IsA(node, OpExpr) || IsA(node, DistinctExpr)) - *total += cpu_operator_cost; + { + total->per_tuple += cpu_operator_cost; + } + else if (IsA(node, SubLink)) + { + /* This routine should not be applied to un-planned expressions */ + elog(ERROR, "cost_qual_eval: can't handle unplanned sub-select"); + } else if (IsA(node, SubPlan)) { /* - * A subplan node in an expression indicates that the - * subplan will be executed on each evaluation, so charge - * accordingly. (We assume that sub-selects that can be - * executed as InitPlans have already been removed from - * the expression.) + * A subplan node in an expression typically indicates that the + * subplan will be executed on each evaluation, so charge accordingly. + * (Sub-selects that can be executed as InitPlans have already been + * removed from the expression.) + * + * An exception occurs when we have decided we can implement the + * subplan by hashing. * - * NOTE: this logic should agree with the estimates used by - * make_subplan() in plan/subselect.c. */ SubPlan *subplan = (SubPlan *) node; Plan *plan = subplan->plan; - Cost subcost; - if (subplan->subLinkType == EXISTS_SUBLINK) + if (subplan->useHashTable) { - /* we only need to fetch 1 tuple */ - subcost = plan->startup_cost + - (plan->total_cost - plan->startup_cost) / plan->plan_rows; - } - else if (subplan->subLinkType == ALL_SUBLINK || - subplan->subLinkType == ANY_SUBLINK) - { - /* assume we need 50% of the tuples */ - subcost = plan->startup_cost + - 0.50 * (plan->total_cost - plan->startup_cost); - /* XXX what if subplan has been materialized? */ + /* + * If we are using a hash table for the subquery outputs, then + * the cost of evaluating the query is a one-time cost. + * We charge one cpu_operator_cost per tuple for the work of + * loading the hashtable, too. + */ + total->startup += plan->total_cost + + cpu_operator_cost * plan->plan_rows; + /* + * The per-tuple costs include the cost of evaluating the + * lefthand expressions, plus the cost of probing the hashtable. + * Recursion into the exprs list will handle the lefthand + * expressions properly, and will count one cpu_operator_cost + * for each comparison operator. That is probably too low for + * the probing cost, but it's hard to make a better estimate, + * so live with it for now. + */ } else { - /* assume we need all tuples */ - subcost = plan->total_cost; + /* + * Otherwise we will be rescanning the subplan output on each + * evaluation. We need to estimate how much of the output + * we will actually need to scan. NOTE: this logic should + * agree with the estimates used by make_subplan() in + * plan/subselect.c. + */ + Cost plan_run_cost = plan->total_cost - plan->startup_cost; + + if (subplan->subLinkType == EXISTS_SUBLINK) + { + /* we only need to fetch 1 tuple */ + total->per_tuple += plan_run_cost / plan->plan_rows; + } + else if (subplan->subLinkType == ALL_SUBLINK || + subplan->subLinkType == ANY_SUBLINK) + { + /* assume we need 50% of the tuples */ + total->per_tuple += 0.50 * plan_run_cost; + /* also charge a cpu_operator_cost per row examined */ + total->per_tuple += 0.50 * plan->plan_rows * cpu_operator_cost; + } + else + { + /* assume we need all tuples */ + total->per_tuple += plan_run_cost; + } + /* + * Also account for subplan's startup cost. + * If the subplan is uncorrelated or undirect correlated, + * AND its topmost node is a Sort or Material node, assume + * that we'll only need to pay its startup cost once; + * otherwise assume we pay the startup cost every time. + */ + if (subplan->parParam == NIL && + (IsA(plan, Sort) || + IsA(plan, Material))) + { + total->startup += plan->startup_cost; + } + else + { + total->per_tuple += plan->startup_cost; + } } - *total += subcost; } return expression_tree_walker(node, cost_qual_eval_walker, @@ -1388,7 +1466,7 @@ set_baserel_size_estimates(Query *root, RelOptInfo *rel) rel->rows = temp; - rel->baserestrictcost = cost_qual_eval(rel->baserestrictinfo); + cost_qual_eval(&rel->baserestrictcost, rel->baserestrictinfo); set_rel_width(root, rel); } @@ -1533,7 +1611,7 @@ set_function_size_estimates(Query *root, RelOptInfo *rel) rel->rows = temp; - rel->baserestrictcost = cost_qual_eval(rel->baserestrictinfo); + cost_qual_eval(&rel->baserestrictcost, rel->baserestrictinfo); set_rel_width(root, rel); } diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index 151a37a888..1c1a848ea7 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $Header: /cvsroot/pgsql/src/backend/optimizer/plan/initsplan.c,v 1.79 2002/12/17 01:18:25 tgl Exp $ + * $Header: /cvsroot/pgsql/src/backend/optimizer/plan/initsplan.c,v 1.80 2003/01/12 22:35:29 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -396,7 +396,7 @@ distribute_qual_to_rels(Query *root, Node *clause, restrictinfo->clause = (Expr *) clause; restrictinfo->subclauseindices = NIL; - restrictinfo->eval_cost = -1; /* not computed until needed */ + restrictinfo->eval_cost.startup = -1; /* not computed until needed */ restrictinfo->this_selec = -1; /* not computed until needed */ restrictinfo->mergejoinoperator = InvalidOid; restrictinfo->left_sortop = InvalidOid; diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index 4d5adc4d47..5177e210d3 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -14,7 +14,7 @@ * * * IDENTIFICATION - * $Header: /cvsroot/pgsql/src/backend/optimizer/prep/prepunion.c,v 1.84 2003/01/05 00:56:40 tgl Exp $ + * $Header: /cvsroot/pgsql/src/backend/optimizer/prep/prepunion.c,v 1.85 2003/01/12 22:35:29 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -853,7 +853,7 @@ adjust_inherited_attrs_mutator(Node *node, adjust_inherited_attrs_mutator((Node *) oldinfo->clause, context); newinfo->subclauseindices = NIL; - newinfo->eval_cost = -1; /* reset these too */ + newinfo->eval_cost.startup = -1; /* reset these too */ newinfo->this_selec = -1; newinfo->left_pathkey = NIL; /* and these */ newinfo->right_pathkey = NIL; diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index b93816bd21..296d211ad1 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $Header: /cvsroot/pgsql/src/backend/optimizer/util/relnode.c,v 1.41 2002/11/24 21:52:14 tgl Exp $ + * $Header: /cvsroot/pgsql/src/backend/optimizer/util/relnode.c,v 1.42 2003/01/12 22:35:29 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -149,7 +149,8 @@ make_base_rel(Query *root, int relid) rel->joinrti = 0; rel->joinrteids = NIL; rel->baserestrictinfo = NIL; - rel->baserestrictcost = 0; + rel->baserestrictcost.startup = 0; + rel->baserestrictcost.per_tuple = 0; rel->outerjoinset = NIL; rel->joininfo = NIL; rel->index_outer_relids = NIL; @@ -363,7 +364,8 @@ build_join_rel(Query *root, joinrel->joinrteids = nconc(listCopy(outer_rel->joinrteids), inner_rel->joinrteids); joinrel->baserestrictinfo = NIL; - joinrel->baserestrictcost = 0; + joinrel->baserestrictcost.startup = 0; + joinrel->baserestrictcost.per_tuple = 0; joinrel->outerjoinset = NIL; joinrel->joininfo = NIL; joinrel->index_outer_relids = NIL; diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index 58c63c210c..977cbe0a5e 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -15,7 +15,7 @@ * * * IDENTIFICATION - * $Header: /cvsroot/pgsql/src/backend/utils/adt/selfuncs.c,v 1.124 2002/12/17 01:18:35 tgl Exp $ + * $Header: /cvsroot/pgsql/src/backend/utils/adt/selfuncs.c,v 1.125 2003/01/12 22:35:29 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -3757,6 +3757,7 @@ genericcostestimate(Query *root, RelOptInfo *rel, { double numIndexTuples; double numIndexPages; + QualCost index_qual_cost; List *selectivityQuals = indexQuals; /* @@ -3826,9 +3827,10 @@ genericcostestimate(Query *root, RelOptInfo *rel, * tuple. All the costs are assumed to be paid incrementally during * the scan. */ - *indexStartupCost = 0; + cost_qual_eval(&index_qual_cost, indexQuals); + *indexStartupCost = index_qual_cost.startup; *indexTotalCost = numIndexPages + - (cpu_index_tuple_cost + cost_qual_eval(indexQuals)) * numIndexTuples; + (cpu_index_tuple_cost + index_qual_cost.per_tuple) * numIndexTuples; /* * Generic assumption about index correlation: there isn't any. diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h index 6da47683e6..d3fb2231f1 100644 --- a/src/include/nodes/relation.h +++ b/src/include/nodes/relation.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2002, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $Id: relation.h,v 1.74 2002/12/12 15:49:40 tgl Exp $ + * $Id: relation.h,v 1.75 2003/01/12 22:35:29 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -35,6 +35,16 @@ typedef enum CostSelector STARTUP_COST, TOTAL_COST } CostSelector; +/* + * The cost estimate produced by cost_qual_eval() includes both a one-time + * (startup) cost, and a per-tuple cost. + */ +typedef struct QualCost +{ + Cost startup; /* one-time cost */ + Cost per_tuple; /* per-evaluation cost */ +} QualCost; + /*---------- * RelOptInfo * Per-relation information for planning/optimization @@ -199,7 +209,7 @@ typedef struct RelOptInfo /* used by various scans and joins: */ List *baserestrictinfo; /* RestrictInfo structures (if * base rel) */ - Cost baserestrictcost; /* cost of evaluating the above */ + QualCost baserestrictcost; /* cost of evaluating the above */ Relids outerjoinset; /* integer list of base relids */ List *joininfo; /* JoinInfo structures */ @@ -570,7 +580,7 @@ typedef struct RestrictInfo /* subclauseindices is a List of Lists of IndexOptInfos */ /* cache space for costs (currently only used for join clauses) */ - Cost eval_cost; /* eval cost of clause; -1 if not yet set */ + QualCost eval_cost; /* eval cost of clause; -1 if not yet set */ Selectivity this_selec; /* selectivity; -1 if not yet set */ /* valid if clause is mergejoinable, else InvalidOid: */ diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h index 5bb5092571..b5fef65c5d 100644 --- a/src/include/optimizer/cost.h +++ b/src/include/optimizer/cost.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2002, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $Id: cost.h,v 1.49 2002/11/30 05:21:03 tgl Exp $ + * $Id: cost.h,v 1.50 2003/01/12 22:35:29 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -83,7 +83,7 @@ extern void cost_hashjoin(Path *path, Query *root, Path *outer_path, Path *inner_path, List *restrictlist, List *hashclauses); -extern Cost cost_qual_eval(List *quals); +extern void cost_qual_eval(QualCost *cost, List *quals); extern void set_baserel_size_estimates(Query *root, RelOptInfo *rel); extern void set_joinrel_size_estimates(Query *root, RelOptInfo *rel, RelOptInfo *outer_rel,