diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c index e1328c0f9b84d881563d367d4923017915460718..21cf27745a4ff31530ad1e546a2bf583abc4d78b 100644 --- a/src/backend/optimizer/path/indxpath.c +++ b/src/backend/optimizer/path/indxpath.c @@ -9,7 +9,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/path/indxpath.c,v 1.172 2005/03/28 00:58:22 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/path/indxpath.c,v 1.173 2005/04/11 23:06:55 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -54,7 +54,6 @@ ((opclass) == BOOL_BTREE_OPS_OID || (opclass) == BOOL_HASH_OPS_OID) -static List *group_clauses_by_indexkey(IndexOptInfo *index); static List *group_clauses_by_indexkey_for_join(Query *root, IndexOptInfo *index, Relids outer_relids, @@ -72,8 +71,6 @@ static bool pred_test_simple_clause(Expr *predicate, Node *clause); static Relids indexable_outerrelids(IndexOptInfo *index); static Path *make_innerjoin_index_path(Query *root, IndexOptInfo *index, List *clausegroups); -static bool match_index_to_operand(Node *operand, int indexcol, - IndexOptInfo *index); static bool match_boolean_index_clause(Node *clause, int indexcol, IndexOptInfo *index); static bool match_special_index_operator(Expr *clause, Oid opclass, @@ -234,7 +231,7 @@ create_index_paths(Query *root, RelOptInfo *rel) * clauses matching column C, because the executor couldn't use them anyway. * Therefore, there are no empty sublists in the result. */ -static List * +List * group_clauses_by_indexkey(IndexOptInfo *index) { List *clausegroup_list = NIL; @@ -1774,7 +1771,7 @@ make_expr_from_indexclauses(List *indexclauses) * indexcol: the column number of the index (counting from 0) * index: the index of interest */ -static bool +bool match_index_to_operand(Node *operand, int indexcol, IndexOptInfo *index) diff --git a/src/backend/optimizer/plan/Makefile b/src/backend/optimizer/plan/Makefile index cc017bd681729efd233852154e93e5ee0dadaec2..a6690cc98daa8b560d8d66984c5c447cec152c9e 100644 --- a/src/backend/optimizer/plan/Makefile +++ b/src/backend/optimizer/plan/Makefile @@ -4,7 +4,7 @@ # Makefile for optimizer/plan # # IDENTIFICATION -# $PostgreSQL: pgsql/src/backend/optimizer/plan/Makefile,v 1.12 2003/11/29 19:51:50 pgsql Exp $ +# $PostgreSQL: pgsql/src/backend/optimizer/plan/Makefile,v 1.13 2005/04/11 23:06:55 tgl Exp $ # #------------------------------------------------------------------------- @@ -12,7 +12,8 @@ subdir = src/backend/optimizer/plan top_builddir = ../../../.. include $(top_builddir)/src/Makefile.global -OBJS = createplan.o initsplan.o planmain.o planner.o setrefs.o subselect.o +OBJS = createplan.o initsplan.o planagg.o planmain.o planner.o \ + setrefs.o subselect.o all: SUBSYS.o diff --git a/src/backend/optimizer/plan/planagg.c b/src/backend/optimizer/plan/planagg.c new file mode 100644 index 0000000000000000000000000000000000000000..299931c6156f1ba4cad456ec089ae8cd5fe9fc4d --- /dev/null +++ b/src/backend/optimizer/plan/planagg.c @@ -0,0 +1,575 @@ +/*------------------------------------------------------------------------- + * + * planagg.c + * Special planning for aggregate queries. + * + * Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * $PostgreSQL: pgsql/src/backend/optimizer/plan/planagg.c,v 1.1 2005/04/11 23:06:55 tgl Exp $ + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/skey.h" +#include "catalog/pg_aggregate.h" +#include "catalog/pg_type.h" +#include "nodes/makefuncs.h" +#include "optimizer/clauses.h" +#include "optimizer/cost.h" +#include "optimizer/pathnode.h" +#include "optimizer/paths.h" +#include "optimizer/planmain.h" +#include "optimizer/subselect.h" +#include "parser/parsetree.h" +#include "parser/parse_clause.h" +#include "parser/parse_expr.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + + +typedef struct +{ + Oid aggfnoid; /* pg_proc Oid of the aggregate */ + Oid aggsortop; /* Oid of its sort operator */ + Expr *target; /* expression we are aggregating on */ + IndexPath *path; /* access path for index scan */ + Cost pathcost; /* estimated cost to fetch first row */ + Param *param; /* param for subplan's output */ +} MinMaxAggInfo; + +static bool find_minmax_aggs_walker(Node *node, List **context); +static bool build_minmax_path(Query *root, RelOptInfo *rel, + MinMaxAggInfo *info); +static ScanDirection match_agg_to_index_col(MinMaxAggInfo *info, + IndexOptInfo *index, int indexcol); +static void make_agg_subplan(Query *root, MinMaxAggInfo *info, + List *constant_quals); +static Node *replace_aggs_with_params_mutator(Node *node, List **context); +static Oid fetch_agg_sort_op(Oid aggfnoid); + + +/* + * optimize_minmax_aggregates - check for optimizing MIN/MAX via indexes + * + * This checks to see if we can replace MIN/MAX aggregate functions by + * subqueries of the form + * (SELECT col FROM tab WHERE ... ORDER BY col ASC/DESC LIMIT 1) + * Given a suitable index on tab.col, this can be much faster than the + * generic scan-all-the-rows plan. + * + * We are passed the Query, the preprocessed tlist, and the best path + * devised for computing the input of a standard Agg node. If we are able + * to optimize all the aggregates, and the result is estimated to be cheaper + * than the generic aggregate method, then generate and return a Plan that + * does it that way. Otherwise, return NULL. + */ +Plan * +optimize_minmax_aggregates(Query *root, List *tlist, Path *best_path) +{ + RangeTblRef *rtr; + RangeTblEntry *rte; + RelOptInfo *rel; + List *aggs_list; + ListCell *l; + Cost total_cost; + Path agg_p; + Plan *plan; + Node *hqual; + QualCost tlist_cost; + List *constant_quals; + + /* Nothing to do if query has no aggregates */ + if (!root->hasAggs) + return NULL; + + Assert(!root->setOperations); /* shouldn't get here if a setop */ + Assert(root->rowMarks == NIL); /* nor if FOR UPDATE */ + + /* + * Reject unoptimizable cases. + * + * We don't handle GROUP BY, because our current implementations of + * grouping require looking at all the rows anyway, and so there's not + * much point in optimizing MIN/MAX. + */ + if (root->groupClause) + return NULL; + + /* + * We also restrict the query to reference exactly one table, since + * join conditions can't be handled reasonably. (We could perhaps + * handle a query containing cartesian-product joins, but it hardly + * seems worth the trouble.) + */ + Assert(root->jointree != NULL && IsA(root->jointree, FromExpr)); + if (list_length(root->jointree->fromlist) != 1) + return NULL; + rtr = (RangeTblRef *) linitial(root->jointree->fromlist); + if (!IsA(rtr, RangeTblRef)) + return NULL; + rte = rt_fetch(rtr->rtindex, root->rtable); + if (rte->rtekind != RTE_RELATION) + return NULL; + rel = find_base_rel(root, rtr->rtindex); + + /* + * Also reject cases with subplans or volatile functions in WHERE. + * This may be overly paranoid, but it's not entirely clear if the + * transformation is safe then. + */ + if (contain_subplans(root->jointree->quals) || + contain_volatile_functions(root->jointree->quals)) + return NULL; + + /* + * Since this optimization is not applicable all that often, we want + * to fall out before doing very much work if possible. Therefore + * we do the work in several passes. The first pass scans the tlist + * and HAVING qual to find all the aggregates and verify that + * each of them is a MIN/MAX aggregate. If that succeeds, the second + * pass looks at each aggregate to see if it is optimizable; if so + * we make an IndexPath describing how we would scan it. (We do not + * try to optimize if only some aggs are optimizable, since that means + * we'll have to scan all the rows anyway.) If that succeeds, we have + * enough info to compare costs against the generic implementation. + * Only if that test passes do we build a Plan. + */ + + /* Pass 1: find all the aggregates */ + aggs_list = NIL; + if (find_minmax_aggs_walker((Node *) tlist, &aggs_list)) + return NULL; + if (find_minmax_aggs_walker(root->havingQual, &aggs_list)) + return NULL; + + /* Pass 2: see if each one is optimizable */ + total_cost = 0; + foreach(l, aggs_list) + { + MinMaxAggInfo *info = (MinMaxAggInfo *) lfirst(l); + + if (!build_minmax_path(root, rel, info)) + return NULL; + total_cost += info->pathcost; + } + + /* + * Make the cost comparison. + * + * Note that we don't include evaluation cost of the tlist here; + * this is OK since it isn't included in best_path's cost either, + * and should be the same in either case. + */ + cost_agg(&agg_p, root, AGG_PLAIN, list_length(aggs_list), + 0, 0, + best_path->startup_cost, best_path->total_cost, + best_path->parent->rows); + + if (total_cost > agg_p.total_cost) + return NULL; /* too expensive */ + + /* + * OK, we are going to generate an optimized plan. The first thing we + * need to do is look for any non-variable WHERE clauses that query_planner + * might have removed from the basic plan. (Normal WHERE clauses will + * be properly incorporated into the sub-plans by create_plan.) If there + * are any, they will be in a gating Result node atop the best_path. + * They have to be incorporated into a gating Result in each sub-plan + * in order to produce the semantically correct result. + */ + if (IsA(best_path, ResultPath)) + { + Assert(((ResultPath *) best_path)->subpath != NULL); + constant_quals = ((ResultPath *) best_path)->constantqual; + } + else + constant_quals = NIL; + + /* Pass 3: generate subplans and output Param nodes */ + foreach(l, aggs_list) + { + make_agg_subplan(root, (MinMaxAggInfo *) lfirst(l), constant_quals); + } + + /* + * Modify the targetlist and HAVING qual to reference subquery outputs + */ + tlist = (List *) replace_aggs_with_params_mutator((Node *) tlist, + &aggs_list); + hqual = replace_aggs_with_params_mutator(root->havingQual, + &aggs_list); + + /* + * Generate the output plan --- basically just a Result + */ + plan = (Plan *) make_result(tlist, hqual, NULL); + + /* Account for evaluation cost of the tlist (make_result did the rest) */ + cost_qual_eval(&tlist_cost, tlist); + plan->startup_cost += tlist_cost.startup; + plan->total_cost += tlist_cost.startup + tlist_cost.per_tuple; + + return plan; +} + +/* + * find_minmax_aggs_walker + * Recursively scan the Aggref nodes in an expression tree, and check + * that each one is a MIN/MAX aggregate. If so, build a list of the + * distinct aggregate calls in the tree. + * + * Returns TRUE if a non-MIN/MAX aggregate is found, FALSE otherwise. + * (This seemingly-backward definition is used because expression_tree_walker + * aborts the scan on TRUE return, which is what we want.) + * + * Found aggregates are added to the list at *context; it's up to the caller + * to initialize the list to NIL. + * + * This does not descend into subqueries, and so should be used only after + * reduction of sublinks to subplans. There mustn't be outer-aggregate + * references either. + */ +static bool +find_minmax_aggs_walker(Node *node, List **context) +{ + if (node == NULL) + return false; + if (IsA(node, Aggref)) + { + Aggref *aggref = (Aggref *) node; + Oid aggsortop; + MinMaxAggInfo *info; + ListCell *l; + + Assert(aggref->agglevelsup == 0); + if (aggref->aggstar) + return true; /* foo(*) is surely not optimizable */ + /* note: we do not care if DISTINCT is mentioned ... */ + + aggsortop = fetch_agg_sort_op(aggref->aggfnoid); + if (!OidIsValid(aggsortop)) + return true; /* not a MIN/MAX aggregate */ + + /* + * Check whether it's already in the list, and add it if not. + */ + foreach(l, *context) + { + info = (MinMaxAggInfo *) lfirst(l); + if (info->aggfnoid == aggref->aggfnoid && + equal(info->target, aggref->target)) + return false; + } + + info = (MinMaxAggInfo *) palloc0(sizeof(MinMaxAggInfo)); + info->aggfnoid = aggref->aggfnoid; + info->aggsortop = aggsortop; + info->target = aggref->target; + + *context = lappend(*context, info); + + /* + * We need not recurse into the argument, since it can't contain + * any aggregates. + */ + return false; + } + Assert(!IsA(node, SubLink)); + return expression_tree_walker(node, find_minmax_aggs_walker, + (void *) context); +} + +/* + * build_minmax_path + * Given a MIN/MAX aggregate, try to find an index it can be optimized + * with. Build a Path describing the best such index path. + * + * Returns TRUE if successful, FALSE if not. In the TRUE case, info->path + * is filled in. + * + * XXX look at sharing more code with indxpath.c. + * + * Note: check_partial_indexes() must have been run previously. + */ +static bool +build_minmax_path(Query *root, RelOptInfo *rel, MinMaxAggInfo *info) +{ + IndexPath *best_path = NULL; + Cost best_cost = 0; + ListCell *l; + + foreach(l, rel->indexlist) + { + IndexOptInfo *index = (IndexOptInfo *) lfirst(l); + ScanDirection indexscandir = NoMovementScanDirection; + int indexcol; + int prevcol; + List *restrictclauses; + IndexPath *new_path; + Cost new_cost; + + /* Ignore non-btree indexes */ + if (index->relam != BTREE_AM_OID) + continue; + + /* Ignore partial indexes that do not match the query */ + if (index->indpred != NIL && !index->predOK) + continue; + + /* + * Look for a match to one of the index columns. (In a stupidly + * designed index, there could be multiple matches, but we only + * care about the first one.) + */ + for (indexcol = 0; indexcol < index->ncolumns; indexcol++) + { + indexscandir = match_agg_to_index_col(info, index, indexcol); + if (!ScanDirectionIsNoMovement(indexscandir)) + break; + } + if (ScanDirectionIsNoMovement(indexscandir)) + continue; + + /* + * If the match is not at the first index column, we have to verify + * that there are "x = something" restrictions on all the earlier + * index columns. Since we'll need the restrictclauses list anyway + * to build the path, it's convenient to extract that first and then + * look through it for the equality restrictions. + */ + restrictclauses = group_clauses_by_indexkey(index); + + if (list_length(restrictclauses) < indexcol) + continue; /* definitely haven't got enough */ + for (prevcol = 0; prevcol < indexcol; prevcol++) + { + List *rinfos = (List *) list_nth(restrictclauses, prevcol); + ListCell *ll; + + foreach(ll, rinfos) + { + RestrictInfo *rinfo = (RestrictInfo *) lfirst(ll); + int strategy; + + Assert(is_opclause(rinfo->clause)); + strategy = + get_op_opclass_strategy(((OpExpr *) rinfo->clause)->opno, + index->classlist[prevcol]); + if (strategy == BTEqualStrategyNumber) + break; + } + if (ll == NULL) + break; /* none are Equal for this index col */ + } + if (prevcol < indexcol) + continue; /* didn't find all Equal clauses */ + + /* + * Build the access path. We don't bother marking it with pathkeys. + */ + new_path = create_index_path(root, index, + restrictclauses, + NIL, + indexscandir); + + /* + * Estimate actual cost of fetching just one row. + */ + if (new_path->rows > 1.0) + new_cost = new_path->path.startup_cost + + (new_path->path.total_cost - new_path->path.startup_cost) + * 1.0 / new_path->rows; + else + new_cost = new_path->path.total_cost; + + /* + * Keep if first or if cheaper than previous best. + */ + if (best_path == NULL || new_cost < best_cost) + { + best_path = new_path; + best_cost = new_cost; + } + } + + info->path = best_path; + info->pathcost = best_cost; + return (best_path != NULL); +} + +/* + * match_agg_to_index_col + * Does an aggregate match an index column? + * + * It matches if its argument is equal to the index column's data and its + * sortop is either the LessThan or GreaterThan member of the column's opclass. + * + * We return ForwardScanDirection if match the LessThan member, + * BackwardScanDirection if match the GreaterThan member, + * and NoMovementScanDirection if there's no match. + */ +static ScanDirection +match_agg_to_index_col(MinMaxAggInfo *info, IndexOptInfo *index, int indexcol) +{ + int strategy; + + /* Check for data match */ + if (!match_index_to_operand((Node *) info->target, indexcol, index)) + return NoMovementScanDirection; + + /* Look up the operator in the opclass */ + strategy = get_op_opclass_strategy(info->aggsortop, + index->classlist[indexcol]); + if (strategy == BTLessStrategyNumber) + return ForwardScanDirection; + if (strategy == BTGreaterStrategyNumber) + return BackwardScanDirection; + return NoMovementScanDirection; +} + +/* + * Construct a suitable plan for a converted aggregate query + */ +static void +make_agg_subplan(Query *root, MinMaxAggInfo *info, List *constant_quals) +{ + Query *subquery; + Path *path; + Plan *plan; + TargetEntry *tle; + SortClause *sortcl; + + /* + * Generate a suitably modified Query node. Much of the work here is + * probably unnecessary in the normal case, but we want to make it look + * good if someone tries to EXPLAIN the result. + */ + subquery = (Query *) copyObject(root); + subquery->commandType = CMD_SELECT; + subquery->resultRelation = 0; + subquery->resultRelations = NIL; + subquery->into = NULL; + subquery->hasAggs = false; + subquery->groupClause = NIL; + subquery->havingQual = NULL; + subquery->hasHavingQual = false; + subquery->distinctClause = NIL; + + /* single tlist entry that is the aggregate target */ + tle = makeTargetEntry(copyObject(info->target), + 1, + pstrdup("agg_target"), + false); + subquery->targetList = list_make1(tle); + + /* set up the appropriate ORDER BY entry */ + sortcl = makeNode(SortClause); + sortcl->tleSortGroupRef = assignSortGroupRef(tle, subquery->targetList); + sortcl->sortop = info->aggsortop; + subquery->sortClause = list_make1(sortcl); + + /* set up LIMIT 1 */ + subquery->limitOffset = NULL; + subquery->limitCount = (Node *) makeConst(INT4OID, sizeof(int4), + Int32GetDatum(1), + false, true); + + /* + * Generate the plan for the subquery. We already have a Path for + * the basic indexscan, but we have to convert it to a Plan and + * attach a LIMIT node above it. We might need a gating Result, too, + * which is most easily added at the Path stage. + */ + path = (Path *) info->path; + + if (constant_quals) + path = (Path *) create_result_path(NULL, + path, + copyObject(constant_quals)); + + plan = create_plan(subquery, path); + + plan->targetlist = copyObject(subquery->targetList); + + plan = (Plan *) make_limit(plan, + subquery->limitOffset, + subquery->limitCount); + + /* + * Convert the plan into an InitPlan, and make a Param for its result. + */ + info->param = SS_make_initplan_from_plan(subquery, plan, + exprType((Node *) tle->expr), + -1); +} + +/* + * Replace original aggregate calls with subplan output Params + */ +static Node * +replace_aggs_with_params_mutator(Node *node, List **context) +{ + if (node == NULL) + return NULL; + if (IsA(node, Aggref)) + { + Aggref *aggref = (Aggref *) node; + ListCell *l; + + foreach(l, *context) + { + MinMaxAggInfo *info = (MinMaxAggInfo *) lfirst(l); + + if (info->aggfnoid == aggref->aggfnoid && + equal(info->target, aggref->target)) + return (Node *) info->param; + } + elog(ERROR, "failed to re-find aggregate info record"); + } + Assert(!IsA(node, SubLink)); + return expression_tree_mutator(node, replace_aggs_with_params_mutator, + (void *) context); +} + +/* + * Get the OID of the sort operator, if any, associated with an aggregate. + * Returns InvalidOid if there is no such operator. + */ +static Oid +fetch_agg_sort_op(Oid aggfnoid) +{ +#ifdef NOT_YET + HeapTuple aggTuple; + Form_pg_aggregate aggform; + Oid aggsortop; + + /* fetch aggregate entry from pg_aggregate */ + aggTuple = SearchSysCache(AGGFNOID, + ObjectIdGetDatum(aggfnoid), + 0, 0, 0); + if (!HeapTupleIsValid(aggTuple)) + return InvalidOid; + aggform = (Form_pg_aggregate) GETSTRUCT(aggTuple); + aggsortop = aggform->aggsortop; + ReleaseSysCache(aggTuple); + + return aggsortop; +#else + /* + * XXX stub implementation for testing: hardwire a few cases. + */ + if (aggfnoid == 2132) /* min(int4) -> int4lt */ + return 97; + if (aggfnoid == 2116) /* max(int4) -> int4gt */ + return 521; + if (aggfnoid == 2145) /* min(text) -> text_lt */ + return 664; + if (aggfnoid == 2129) /* max(text) -> text_gt */ + return 666; + return InvalidOid; +#endif +} diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index eea58e45a15ee90f38d1911cd269153a5e21513f..b542fef61e26a697c1fbd44dec2be43147456b70 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/plan/planner.c,v 1.183 2005/04/10 19:50:08 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/plan/planner.c,v 1.184 2005/04/11 23:06:55 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -362,43 +362,12 @@ subquery_planner(Query *parse, double tuple_fraction) /* * If any subplans were generated, or if we're inside a subplan, build - * initPlan list and extParam/allParam sets for plan nodes. + * initPlan list and extParam/allParam sets for plan nodes, and attach + * the initPlans to the top plan node. */ if (PlannerPlanId != saved_planid || PlannerQueryLevel > 1) - { - Cost initplan_cost = 0; - - /* Prepare extParam/allParam sets for all nodes in tree */ SS_finalize_plan(plan, parse->rtable); - /* - * SS_finalize_plan doesn't handle initPlans, so we have to - * manually attach them to the topmost plan node, and add their - * extParams to the topmost node's, too. - * - * We also add the total_cost of each initPlan to the startup cost of - * the top node. This is a conservative overestimate, since in - * fact each initPlan might be executed later than plan startup, - * or even not at all. - */ - plan->initPlan = PlannerInitPlan; - - foreach(l, plan->initPlan) - { - SubPlan *initplan = (SubPlan *) lfirst(l); - - plan->extParam = bms_add_members(plan->extParam, - initplan->plan->extParam); - /* allParam must include all members of extParam */ - plan->allParam = bms_add_members(plan->allParam, - plan->extParam); - initplan_cost += initplan->plan->total_cost; - } - - plan->startup_cost += initplan_cost; - plan->total_cost += initplan_cost; - } - /* Return to outer subquery context */ PlannerQueryLevel--; PlannerInitPlan = saved_initplan; @@ -692,6 +661,7 @@ grouping_planner(Query *parse, double tuple_fraction) double sub_tuple_fraction; Path *cheapest_path; Path *sorted_path; + Path *best_path; double dNumGroups = 0; long numGroups = 0; AggClauseCounts agg_counts; @@ -959,114 +929,175 @@ grouping_planner(Query *parse, double tuple_fraction) } /* - * Select the best path and create a plan to execute it. - * - * If we are doing hashed grouping, we will always read all the input - * tuples, so use the cheapest-total path. Otherwise, trust - * query_planner's decision about which to use. + * Select the best path. If we are doing hashed grouping, we will + * always read all the input tuples, so use the cheapest-total + * path. Otherwise, trust query_planner's decision about which to use. */ - if (sorted_path && !use_hashed_grouping) - { - result_plan = create_plan(parse, sorted_path); - current_pathkeys = sorted_path->pathkeys; - } + if (use_hashed_grouping || !sorted_path) + best_path = cheapest_path; else - { - result_plan = create_plan(parse, cheapest_path); - current_pathkeys = cheapest_path->pathkeys; - } + best_path = sorted_path; /* - * create_plan() returns a plan with just a "flat" tlist of - * required Vars. Usually we need to insert the sub_tlist as the - * tlist of the top plan node. However, we can skip that if we - * determined that whatever query_planner chose to return will be - * good enough. + * Check to see if it's possible to optimize MIN/MAX aggregates. + * If so, we will forget all the work we did so far to choose a + * "regular" path ... but we had to do it anyway to be able to + * tell which way is cheaper. */ - if (need_tlist_eval) + result_plan = optimize_minmax_aggregates(parse, + tlist, + best_path); + if (result_plan != NULL) + { + /* + * optimize_minmax_aggregates generated the full plan, with + * the right tlist, and it has no sort order. + */ + current_pathkeys = NIL; + } + else { /* - * If the top-level plan node is one that cannot do expression - * evaluation, we must insert a Result node to project the - * desired tlist. + * Normal case --- create a plan according to query_planner's + * results. */ - if (!is_projection_capable_plan(result_plan)) + result_plan = create_plan(parse, best_path); + current_pathkeys = best_path->pathkeys; + + /* + * create_plan() returns a plan with just a "flat" tlist of + * required Vars. Usually we need to insert the sub_tlist as the + * tlist of the top plan node. However, we can skip that if we + * determined that whatever query_planner chose to return will be + * good enough. + */ + if (need_tlist_eval) { - result_plan = (Plan *) make_result(sub_tlist, NULL, - result_plan); + /* + * If the top-level plan node is one that cannot do expression + * evaluation, we must insert a Result node to project the + * desired tlist. + */ + if (!is_projection_capable_plan(result_plan)) + { + result_plan = (Plan *) make_result(sub_tlist, NULL, + result_plan); + } + else + { + /* + * Otherwise, just replace the subplan's flat tlist with + * the desired tlist. + */ + result_plan->targetlist = sub_tlist; + } + + /* + * Also, account for the cost of evaluation of the sub_tlist. + * + * Up to now, we have only been dealing with "flat" tlists, + * containing just Vars. So their evaluation cost is zero + * according to the model used by cost_qual_eval() (or if you + * prefer, the cost is factored into cpu_tuple_cost). Thus we + * can avoid accounting for tlist cost throughout + * query_planner() and subroutines. But now we've inserted a + * tlist that might contain actual operators, sub-selects, etc + * --- so we'd better account for its cost. + * + * Below this point, any tlist eval cost for added-on nodes + * should be accounted for as we create those nodes. + * Presently, of the node types we can add on, only Agg and + * Group project new tlists (the rest just copy their input + * tuples) --- so make_agg() and make_group() are responsible + * for computing the added cost. + */ + cost_qual_eval(&tlist_cost, sub_tlist); + result_plan->startup_cost += tlist_cost.startup; + result_plan->total_cost += tlist_cost.startup + + tlist_cost.per_tuple * result_plan->plan_rows; } else { /* - * Otherwise, just replace the subplan's flat tlist with - * the desired tlist. + * Since we're using query_planner's tlist and not the one + * make_subplanTargetList calculated, we have to refigure any + * grouping-column indexes make_subplanTargetList computed. */ - result_plan->targetlist = sub_tlist; + locate_grouping_columns(parse, tlist, result_plan->targetlist, + groupColIdx); } /* - * Also, account for the cost of evaluation of the sub_tlist. - * - * Up to now, we have only been dealing with "flat" tlists, - * containing just Vars. So their evaluation cost is zero - * according to the model used by cost_qual_eval() (or if you - * prefer, the cost is factored into cpu_tuple_cost). Thus we - * can avoid accounting for tlist cost throughout - * query_planner() and subroutines. But now we've inserted a - * tlist that might contain actual operators, sub-selects, etc - * --- so we'd better account for its cost. + * Insert AGG or GROUP node if needed, plus an explicit sort step + * if necessary. * - * Below this point, any tlist eval cost for added-on nodes - * should be accounted for as we create those nodes. - * Presently, of the node types we can add on, only Agg and - * Group project new tlists (the rest just copy their input - * tuples) --- so make_agg() and make_group() are responsible - * for computing the added cost. - */ - cost_qual_eval(&tlist_cost, sub_tlist); - result_plan->startup_cost += tlist_cost.startup; - result_plan->total_cost += tlist_cost.startup + - tlist_cost.per_tuple * result_plan->plan_rows; - } - else - { - /* - * Since we're using query_planner's tlist and not the one - * make_subplanTargetList calculated, we have to refigure any - * grouping-column indexes make_subplanTargetList computed. + * HAVING clause, if any, becomes qual of the Agg or Group node. */ - locate_grouping_columns(parse, tlist, result_plan->targetlist, - groupColIdx); - } + if (use_hashed_grouping) + { + /* Hashed aggregate plan --- no sort needed */ + result_plan = (Plan *) make_agg(parse, + tlist, + (List *) parse->havingQual, + AGG_HASHED, + numGroupCols, + groupColIdx, + numGroups, + agg_counts.numAggs, + result_plan); + /* Hashed aggregation produces randomly-ordered results */ + current_pathkeys = NIL; + } + else if (parse->hasAggs) + { + /* Plain aggregate plan --- sort if needed */ + AggStrategy aggstrategy; - /* - * Insert AGG or GROUP node if needed, plus an explicit sort step - * if necessary. - * - * HAVING clause, if any, becomes qual of the Agg or Group node. - */ - if (use_hashed_grouping) - { - /* Hashed aggregate plan --- no sort needed */ - result_plan = (Plan *) make_agg(parse, - tlist, - (List *) parse->havingQual, - AGG_HASHED, - numGroupCols, - groupColIdx, - numGroups, - agg_counts.numAggs, - result_plan); - /* Hashed aggregation produces randomly-ordered results */ - current_pathkeys = NIL; - } - else if (parse->hasAggs) - { - /* Plain aggregate plan --- sort if needed */ - AggStrategy aggstrategy; + if (parse->groupClause) + { + if (!pathkeys_contained_in(group_pathkeys, + current_pathkeys)) + { + result_plan = (Plan *) + make_sort_from_groupcols(parse, + parse->groupClause, + groupColIdx, + result_plan); + current_pathkeys = group_pathkeys; + } + aggstrategy = AGG_SORTED; - if (parse->groupClause) + /* + * The AGG node will not change the sort ordering of its + * groups, so current_pathkeys describes the result too. + */ + } + else + { + aggstrategy = AGG_PLAIN; + /* Result will be only one row anyway; no sort order */ + current_pathkeys = NIL; + } + + result_plan = (Plan *) make_agg(parse, + tlist, + (List *) parse->havingQual, + aggstrategy, + numGroupCols, + groupColIdx, + numGroups, + agg_counts.numAggs, + result_plan); + } + else if (parse->groupClause) { + /* + * GROUP BY without aggregation, so insert a group node (plus + * the appropriate sort node, if necessary). + * + * Add an explicit sort if we couldn't make the path come + * out the way the GROUP node needs it. + */ if (!pathkeys_contained_in(group_pathkeys, current_pathkeys)) { result_plan = (Plan *) @@ -1076,75 +1107,34 @@ grouping_planner(Query *parse, double tuple_fraction) result_plan); current_pathkeys = group_pathkeys; } - aggstrategy = AGG_SORTED; - /* - * The AGG node will not change the sort ordering of its - * groups, so current_pathkeys describes the result too. - */ + result_plan = (Plan *) make_group(parse, + tlist, + (List *) parse->havingQual, + numGroupCols, + groupColIdx, + dNumGroups, + result_plan); + /* The Group node won't change sort ordering */ } - else + else if (parse->hasHavingQual) { - aggstrategy = AGG_PLAIN; - /* Result will be only one row anyway; no sort order */ - current_pathkeys = NIL; - } - - result_plan = (Plan *) make_agg(parse, - tlist, - (List *) parse->havingQual, - aggstrategy, - numGroupCols, - groupColIdx, - numGroups, - agg_counts.numAggs, - result_plan); - } - else if (parse->groupClause) - { - /* - * GROUP BY without aggregation, so insert a group node (plus the - * appropriate sort node, if necessary). - * - * Add an explicit sort if we couldn't make the path come - * out the way the GROUP node needs it. - */ - if (!pathkeys_contained_in(group_pathkeys, current_pathkeys)) - { - result_plan = (Plan *) - make_sort_from_groupcols(parse, - parse->groupClause, - groupColIdx, - result_plan); - current_pathkeys = group_pathkeys; + /* + * No aggregates, and no GROUP BY, but we have a HAVING qual. + * This is a degenerate case in which we are supposed to emit + * either 0 or 1 row depending on whether HAVING succeeds. + * Furthermore, there cannot be any variables in either HAVING + * or the targetlist, so we actually do not need the FROM table + * at all! We can just throw away the plan-so-far and generate + * a Result node. This is a sufficiently unusual corner case + * that it's not worth contorting the structure of this routine + * to avoid having to generate the plan in the first place. + */ + result_plan = (Plan *) make_result(tlist, + parse->havingQual, + NULL); } - - result_plan = (Plan *) make_group(parse, - tlist, - (List *) parse->havingQual, - numGroupCols, - groupColIdx, - dNumGroups, - result_plan); - /* The Group node won't change sort ordering */ - } - else if (parse->hasHavingQual) - { - /* - * No aggregates, and no GROUP BY, but we have a HAVING qual. - * This is a degenerate case in which we are supposed to emit - * either 0 or 1 row depending on whether HAVING succeeds. - * Furthermore, there cannot be any variables in either HAVING - * or the targetlist, so we actually do not need the FROM table - * at all! We can just throw away the plan-so-far and generate - * a Result node. This is a sufficiently unusual corner case - * that it's not worth contorting the structure of this routine - * to avoid having to generate the plan in the first place. - */ - result_plan = (Plan *) make_result(tlist, - parse->havingQual, - NULL); - } + } /* end of non-minmax-aggregate case */ } /* end of if (setOperations) */ /* diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index c92fb3153167fe10adffea424bdbf35b1413ec93..f1519569a85f30a3dbae3a438ebbd5ede3bd3ebb 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -7,7 +7,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/optimizer/plan/subselect.c,v 1.95 2005/04/06 16:34:05 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/optimizer/plan/subselect.c,v 1.96 2005/04/11 23:06:55 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -915,14 +915,16 @@ process_sublinks_mutator(Node *node, bool *isTopQual) /* * SS_finalize_plan - do final sublink processing for a completed Plan. * - * This recursively computes the extParam and allParam sets - * for every Plan node in the given plan tree. + * This recursively computes the extParam and allParam sets for every Plan + * node in the given plan tree. It also attaches any generated InitPlans + * to the top plan node. */ void SS_finalize_plan(Plan *plan, List *rtable) { Bitmapset *outer_params = NULL; Bitmapset *valid_params = NULL; + Cost initplan_cost = 0; int paramid; ListCell *l; @@ -959,6 +961,33 @@ SS_finalize_plan(Plan *plan, List *rtable) bms_free(outer_params); bms_free(valid_params); + + /* + * Finally, attach any initPlans to the topmost plan node, + * and add their extParams to the topmost node's, too. + * + * We also add the total_cost of each initPlan to the startup cost of + * the top node. This is a conservative overestimate, since in + * fact each initPlan might be executed later than plan startup, + * or even not at all. + */ + plan->initPlan = PlannerInitPlan; + PlannerInitPlan = NIL; /* make sure they're not attached twice */ + + foreach(l, plan->initPlan) + { + SubPlan *initplan = (SubPlan *) lfirst(l); + + plan->extParam = bms_add_members(plan->extParam, + initplan->plan->extParam); + /* allParam must include all members of extParam */ + plan->allParam = bms_add_members(plan->allParam, + plan->extParam); + initplan_cost += initplan->plan->total_cost; + } + + plan->startup_cost += initplan_cost; + plan->total_cost += initplan_cost; } /* @@ -1165,3 +1194,75 @@ finalize_primnode(Node *node, finalize_primnode_context *context) return expression_tree_walker(node, finalize_primnode, (void *) context); } + +/* + * SS_make_initplan_from_plan - given a plan tree, make it an InitPlan + * + * The plan is expected to return a scalar value of the indicated type. + * We build an EXPR_SUBLINK SubPlan node and put it into the initplan + * list for the current query level. A Param that represents the initplan's + * output is returned. + * + * We assume the plan hasn't been put through SS_finalize_plan. + */ +Param * +SS_make_initplan_from_plan(Query *root, Plan *plan, + Oid resulttype, int32 resulttypmod) +{ + List *saved_initplan = PlannerInitPlan; + SubPlan *node; + Param *prm; + Bitmapset *tmpset; + int paramid; + + /* + * Set up for a new level of subquery. This is just to keep + * SS_finalize_plan from becoming confused. + */ + PlannerQueryLevel++; + PlannerInitPlan = NIL; + + /* + * Build extParam/allParam sets for plan nodes. + */ + SS_finalize_plan(plan, root->rtable); + + /* Return to outer subquery context */ + PlannerQueryLevel--; + PlannerInitPlan = saved_initplan; + + /* + * Create a SubPlan node and add it to the outer list of InitPlans. + */ + node = makeNode(SubPlan); + node->subLinkType = EXPR_SUBLINK; + node->plan = plan; + node->plan_id = PlannerPlanId++; /* Assign unique ID to this + * SubPlan */ + + node->rtable = root->rtable; + + PlannerInitPlan = lappend(PlannerInitPlan, node); + + /* + * Make parParam list of params that current query level will pass to + * this child plan. (In current usage there probably aren't any.) + */ + tmpset = bms_copy(plan->extParam); + while ((paramid = bms_first_member(tmpset)) >= 0) + { + PlannerParamItem *pitem = list_nth(PlannerParamList, paramid); + + if (pitem->abslevel == PlannerQueryLevel) + node->parParam = lappend_int(node->parParam, paramid); + } + bms_free(tmpset); + + /* + * Make a Param that will be the subplan's output. + */ + prm = generate_new_param(resulttype, resulttypmod); + node->setParam = list_make1_int(prm->paramid); + + return prm; +} diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 3abbf65fa4833d6ef99ece190603088a49b4fac6..2c4d20576a5552da22c9e6912c261d646a711798 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -7,7 +7,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/cache/lsyscache.c,v 1.122 2005/03/31 22:46:14 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/cache/lsyscache.c,v 1.123 2005/04/11 23:06:56 tgl Exp $ * * NOTES * Eventually, the index information should go through here, too. @@ -53,6 +53,31 @@ op_in_opclass(Oid opno, Oid opclass) 0, 0); } +/* + * get_op_opclass_strategy + * + * Get the operator's strategy number within the specified opclass, + * or 0 if it's not a member of the opclass. + */ +int +get_op_opclass_strategy(Oid opno, Oid opclass) +{ + HeapTuple tp; + Form_pg_amop amop_tup; + int result; + + tp = SearchSysCache(AMOPOPID, + ObjectIdGetDatum(opno), + ObjectIdGetDatum(opclass), + 0, 0); + if (!HeapTupleIsValid(tp)) + return 0; + amop_tup = (Form_pg_amop) GETSTRUCT(tp); + result = amop_tup->amopstrategy; + ReleaseSysCache(tp); + return result; +} + /* * get_op_opclass_properties * diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 28c44444d70cd59471cbf6ac8cf8e42ecc403636..e5ca24807d0027e4221da6a45c4719b1befbc61a 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -8,7 +8,7 @@ * Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/optimizer/paths.h,v 1.80 2005/03/27 06:29:49 tgl Exp $ + * $PostgreSQL: pgsql/src/include/optimizer/paths.h,v 1.81 2005/04/11 23:06:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -38,8 +38,11 @@ extern void debug_print_rel(Query *root, RelOptInfo *rel); extern void create_index_paths(Query *root, RelOptInfo *rel); extern Path *best_inner_indexscan(Query *root, RelOptInfo *rel, Relids outer_relids, JoinType jointype); +extern List *group_clauses_by_indexkey(IndexOptInfo *index); extern List *group_clauses_by_indexkey_for_or(IndexOptInfo *index, Expr *orsubclause); +extern bool match_index_to_operand(Node *operand, int indexcol, + IndexOptInfo *index); extern List *expand_indexqual_conditions(IndexOptInfo *index, List *clausegroups); extern void check_partial_indexes(Query *root, RelOptInfo *rel); diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index e76d55ea4968ef796b3b09d9b212a9f7127bdb3d..f2a48e77c9e3bda6726ec764f43d1abc82997c2f 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/optimizer/planmain.h,v 1.80 2005/03/10 23:21:25 tgl Exp $ + * $PostgreSQL: pgsql/src/include/optimizer/planmain.h,v 1.81 2005/04/11 23:06:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -23,6 +23,12 @@ extern void query_planner(Query *root, List *tlist, double tuple_fraction, Path **cheapest_path, Path **sorted_path); +/* + * prototypes for plan/planagg.c + */ +extern Plan *optimize_minmax_aggregates(Query *root, List *tlist, + Path *best_path); + /* * prototypes for plan/createplan.c */ diff --git a/src/include/optimizer/subselect.h b/src/include/optimizer/subselect.h index c07593ad48e82306b33046b1e7e274604f6b66c4..9381321a46fa7bbc01a327a148dbe8e04ba52882 100644 --- a/src/include/optimizer/subselect.h +++ b/src/include/optimizer/subselect.h @@ -5,7 +5,7 @@ * Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/optimizer/subselect.h,v 1.23 2004/12/31 22:03:36 pgsql Exp $ + * $PostgreSQL: pgsql/src/include/optimizer/subselect.h,v 1.24 2005/04/11 23:06:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -24,5 +24,7 @@ extern Node *convert_IN_to_join(Query *parse, SubLink *sublink); extern Node *SS_replace_correlation_vars(Node *expr); extern Node *SS_process_sublinks(Node *expr, bool isQual); extern void SS_finalize_plan(Plan *plan, List *rtable); +extern Param *SS_make_initplan_from_plan(Query *root, Plan *plan, + Oid resulttype, int32 resulttypmod); #endif /* SUBSELECT_H */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 845a886ba82ff0241f7eca5b240d11e51a15ef8e..b98b53c60fc618d4c4c02e8fbd53c6f94d693c4c 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -6,7 +6,7 @@ * Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/utils/lsyscache.h,v 1.96 2005/03/31 22:46:27 tgl Exp $ + * $PostgreSQL: pgsql/src/include/utils/lsyscache.h,v 1.97 2005/04/11 23:06:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -25,6 +25,7 @@ typedef enum IOFuncSelector } IOFuncSelector; extern bool op_in_opclass(Oid opno, Oid opclass); +extern int get_op_opclass_strategy(Oid opno, Oid opclass); extern void get_op_opclass_properties(Oid opno, Oid opclass, int *strategy, Oid *subtype, bool *recheck); diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out index d07c6d195b7c7a23d7292ce86fcbad8488266928..8aed186403462a0b15b08a1060e0b613c1a6c15e 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -293,3 +293,58 @@ FROM bool_test; t | t | f | | f | t (1 row) +-- +-- Test several cases that should be optimized into indexscans instead of +-- the generic aggregate implementation. We can't actually verify that they +-- are done as indexscans, but we can check that the results are correct. +-- +-- Basic cases +select max(unique1) from tenk1; + max +------ + 9999 +(1 row) + +select max(unique1) from tenk1 where unique1 < 42; + max +----- + 41 +(1 row) + +select max(unique1) from tenk1 where unique1 > 42; + max +------ + 9999 +(1 row) + +select max(unique1) from tenk1 where unique1 > 42000; + max +----- + +(1 row) + +-- multi-column index (uses tenk1_thous_tenthous) +select max(tenthous) from tenk1 where thousand = 33; + max +------ + 9033 +(1 row) + +select min(tenthous) from tenk1 where thousand = 33; + min +----- + 33 +(1 row) + +-- check parameter propagation into an indexscan subquery +select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt +from int4_tbl; + f1 | gt +-------------+---- + 0 | 1 + 123456 | + -123456 | 0 + 2147483647 | + -2147483647 | 0 +(5 rows) + diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index 6643070e71700d75a633caef11b9d3c4f8e14a44..a7332c86716349891746a9170e2a59202e6eb5e0 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -12,6 +12,7 @@ CREATE INDEX onek_stringu1 ON onek USING btree(stringu1 name_ops); CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops); CREATE INDEX tenk1_unique2 ON tenk1 USING btree(unique2 int4_ops); CREATE INDEX tenk1_hundred ON tenk1 USING btree(hundred int4_ops); +CREATE INDEX tenk1_thous_tenthous ON tenk1 (thousand, tenthous); CREATE INDEX tenk2_unique1 ON tenk2 USING btree(unique1 int4_ops); CREATE INDEX tenk2_unique2 ON tenk2 USING btree(unique2 int4_ops); CREATE INDEX tenk2_hundred ON tenk2 USING btree(hundred int4_ops); diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index d9fdcb502fbbb1ac5a7780bd852fa79b0df9ea95..b6aba0d66b7b2a0499c1a03b5cf233716183c615 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -180,3 +180,23 @@ SELECT BOOL_OR(NOT b2) AS "f", BOOL_OR(NOT b3) AS "t" FROM bool_test; + +-- +-- Test several cases that should be optimized into indexscans instead of +-- the generic aggregate implementation. We can't actually verify that they +-- are done as indexscans, but we can check that the results are correct. +-- + +-- Basic cases +select max(unique1) from tenk1; +select max(unique1) from tenk1 where unique1 < 42; +select max(unique1) from tenk1 where unique1 > 42; +select max(unique1) from tenk1 where unique1 > 42000; + +-- multi-column index (uses tenk1_thous_tenthous) +select max(tenthous) from tenk1 where thousand = 33; +select min(tenthous) from tenk1 where thousand = 33; + +-- check parameter propagation into an indexscan subquery +select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt +from int4_tbl; diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql index 6fa0b91e83c96b62022d34e26df72ae017813c0f..71c0b0c2b0add1464ae9e5c3a419b3ed9706b785 100644 --- a/src/test/regress/sql/create_index.sql +++ b/src/test/regress/sql/create_index.sql @@ -20,6 +20,8 @@ CREATE INDEX tenk1_unique2 ON tenk1 USING btree(unique2 int4_ops); CREATE INDEX tenk1_hundred ON tenk1 USING btree(hundred int4_ops); +CREATE INDEX tenk1_thous_tenthous ON tenk1 (thousand, tenthous); + CREATE INDEX tenk2_unique1 ON tenk2 USING btree(unique1 int4_ops); CREATE INDEX tenk2_unique2 ON tenk2 USING btree(unique2 int4_ops);