diff --git a/src/backend/executor/execQual.c b/src/backend/executor/execQual.c index 1c792387de7..99e04ae27a8 100644 --- a/src/backend/executor/execQual.c +++ b/src/backend/executor/execQual.c @@ -20,7 +20,7 @@ * ExecProject - form a new tuple by projecting the given tuple * * NOTES - * The more heavily used ExecEvalExpr routines, such as ExecEvalVar(), + * The more heavily used ExecEvalExpr routines, such as ExecEvalScalarVar, * are hotspots. Making these faster will speed up the entire system. * * ExecProject() is used to make tuple projections. Rather then @@ -63,13 +63,18 @@ static bool isAssignmentIndirectionExpr(ExprState *exprstate); static Datum ExecEvalAggref(AggrefExprState *aggref, ExprContext *econtext, bool *isNull, ExprDoneCond *isDone); -static Datum ExecEvalVar(ExprState *exprstate, ExprContext *econtext, - bool *isNull, ExprDoneCond *isDone); static Datum ExecEvalScalarVar(ExprState *exprstate, ExprContext *econtext, bool *isNull, ExprDoneCond *isDone); -static Datum ExecEvalWholeRowVar(ExprState *exprstate, ExprContext *econtext, +static Datum ExecEvalScalarVarFast(ExprState *exprstate, ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone); +static Datum ExecEvalWholeRowVar(WholeRowVarExprState *wrvstate, + ExprContext *econtext, bool *isNull, ExprDoneCond *isDone); -static Datum ExecEvalWholeRowSlow(ExprState *exprstate, ExprContext *econtext, +static Datum ExecEvalWholeRowFast(WholeRowVarExprState *wrvstate, + ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone); +static Datum ExecEvalWholeRowSlow(WholeRowVarExprState *wrvstate, + ExprContext *econtext, bool *isNull, ExprDoneCond *isDone); static Datum ExecEvalConst(ExprState *exprstate, ExprContext *econtext, bool *isNull, ExprDoneCond *isDone); @@ -514,20 +519,19 @@ ExecEvalAggref(AggrefExprState *aggref, ExprContext *econtext, } /* ---------------------------------------------------------------- - * ExecEvalVar + * ExecEvalScalarVar * - * Returns a Datum whose value is the value of a range - * variable with respect to given expression context. + * Returns a Datum whose value is the value of a scalar (not whole-row) + * range variable with respect to given expression context. * - * Note: ExecEvalVar is executed only the first time through in a given plan; - * it changes the ExprState's function pointer to pass control directly to - * ExecEvalScalarVar, ExecEvalWholeRowVar, or ExecEvalWholeRowSlow after - * making one-time checks. + * Note: ExecEvalScalarVar is executed only the first time through in a given + * plan; it changes the ExprState's function pointer to pass control directly + * to ExecEvalScalarVarFast after making one-time checks. * ---------------------------------------------------------------- */ static Datum -ExecEvalVar(ExprState *exprstate, ExprContext *econtext, - bool *isNull, ExprDoneCond *isDone) +ExecEvalScalarVar(ExprState *exprstate, ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone) { Var *variable = (Var *) exprstate->expr; TupleTableSlot *slot; @@ -564,157 +568,65 @@ ExecEvalVar(ExprState *exprstate, ExprContext *econtext, break; } - if (attnum != InvalidAttrNumber) + /* This was checked by ExecInitExpr */ + Assert(attnum != InvalidAttrNumber); + + /* + * If it's a user attribute, check validity (bogus system attnums will be + * caught inside slot_getattr). What we have to check for here is the + * possibility of an attribute having been changed in type since the plan + * tree was created. Ideally the plan will get invalidated and not + * re-used, but just in case, we keep these defenses. Fortunately it's + * sufficient to check once on the first time through. + * + * Note: we allow a reference to a dropped attribute. slot_getattr will + * force a NULL result in such cases. + * + * Note: ideally we'd check typmod as well as typid, but that seems + * impractical at the moment: in many cases the tupdesc will have been + * generated by ExecTypeFromTL(), and that can't guarantee to generate an + * accurate typmod in all cases, because some expression node types don't + * carry typmod. + */ + if (attnum > 0) { - /* - * Scalar variable case. - * - * If it's a user attribute, check validity (bogus system attnums will - * be caught inside slot_getattr). What we have to check for here is - * the possibility of an attribute having been changed in type since - * the plan tree was created. Ideally the plan would get invalidated - * and not re-used, but until that day arrives, we need defenses. - * Fortunately it's sufficient to check once on the first time - * through. - * - * Note: we allow a reference to a dropped attribute. slot_getattr - * will force a NULL result in such cases. - * - * Note: ideally we'd check typmod as well as typid, but that seems - * impractical at the moment: in many cases the tupdesc will have been - * generated by ExecTypeFromTL(), and that can't guarantee to generate - * an accurate typmod in all cases, because some expression node types - * don't carry typmod. - */ - if (attnum > 0) - { - TupleDesc slot_tupdesc = slot->tts_tupleDescriptor; - Form_pg_attribute attr; - - if (attnum > slot_tupdesc->natts) /* should never happen */ - elog(ERROR, "attribute number %d exceeds number of columns %d", - attnum, slot_tupdesc->natts); - - attr = slot_tupdesc->attrs[attnum - 1]; - - /* can't check type if dropped, since atttypid is probably 0 */ - if (!attr->attisdropped) - { - if (variable->vartype != attr->atttypid) - ereport(ERROR, - (errmsg("attribute %d has wrong type", attnum), - errdetail("Table has type %s, but query expects %s.", - format_type_be(attr->atttypid), - format_type_be(variable->vartype)))); - } - } - - /* Skip the checking on future executions of node */ - exprstate->evalfunc = ExecEvalScalarVar; - - /* Fetch the value from the slot */ - return slot_getattr(slot, attnum, isNull); - } - else - { - /* - * Whole-row variable. - * - * If it's a RECORD Var, we'll use the slot's type ID info. It's - * likely that the slot's type is also RECORD; if so, make sure it's - * been "blessed", so that the Datum can be interpreted later. - * - * If the Var identifies a named composite type, we must check that - * the actual tuple type is compatible with it. - */ TupleDesc slot_tupdesc = slot->tts_tupleDescriptor; - bool needslow = false; + Form_pg_attribute attr; - if (variable->vartype == RECORDOID) + if (attnum > slot_tupdesc->natts) /* should never happen */ + elog(ERROR, "attribute number %d exceeds number of columns %d", + attnum, slot_tupdesc->natts); + + attr = slot_tupdesc->attrs[attnum - 1]; + + /* can't check type if dropped, since atttypid is probably 0 */ + if (!attr->attisdropped) { - if (slot_tupdesc->tdtypeid == RECORDOID && - slot_tupdesc->tdtypmod < 0) - assign_record_type_typmod(slot_tupdesc); - } - else - { - TupleDesc var_tupdesc; - int i; - - /* - * We really only care about number of attributes and data type. - * Also, we can ignore type mismatch on columns that are dropped - * in the destination type, so long as the physical storage - * matches. This is helpful in some cases involving out-of-date - * cached plans. Also, we have to allow the case that the slot - * has more columns than the Var's type, because we might be - * looking at the output of a subplan that includes resjunk - * columns. (XXX it would be nice to verify that the extra - * columns are all marked resjunk, but we haven't got access to - * the subplan targetlist here...) Resjunk columns should always - * be at the end of a targetlist, so it's sufficient to ignore - * them here; but we need to use ExecEvalWholeRowSlow to get rid - * of them in the eventual output tuples. - */ - var_tupdesc = lookup_rowtype_tupdesc(variable->vartype, -1); - - if (var_tupdesc->natts > slot_tupdesc->natts) + if (variable->vartype != attr->atttypid) ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("table row type and query-specified row type do not match"), - errdetail("Table row contains %d attributes, but query expects %d.", - slot_tupdesc->natts, var_tupdesc->natts))); - else if (var_tupdesc->natts < slot_tupdesc->natts) - needslow = true; - - for (i = 0; i < var_tupdesc->natts; i++) - { - Form_pg_attribute vattr = var_tupdesc->attrs[i]; - Form_pg_attribute sattr = slot_tupdesc->attrs[i]; - - if (vattr->atttypid == sattr->atttypid) - continue; /* no worries */ - if (!vattr->attisdropped) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("table row type and query-specified row type do not match"), - errdetail("Table has type %s at ordinal position %d, but query expects %s.", - format_type_be(sattr->atttypid), - i + 1, - format_type_be(vattr->atttypid)))); - - if (vattr->attlen != sattr->attlen || - vattr->attalign != sattr->attalign) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("table row type and query-specified row type do not match"), - errdetail("Physical storage mismatch on dropped attribute at ordinal position %d.", - i + 1))); - } - - ReleaseTupleDesc(var_tupdesc); + (errmsg("attribute %d has wrong type", attnum), + errdetail("Table has type %s, but query expects %s.", + format_type_be(attr->atttypid), + format_type_be(variable->vartype)))); } - - /* Skip the checking on future executions of node */ - if (needslow) - exprstate->evalfunc = ExecEvalWholeRowSlow; - else - exprstate->evalfunc = ExecEvalWholeRowVar; - - /* Fetch the value */ - return ExecEvalWholeRowVar(exprstate, econtext, isNull, isDone); } + + /* Skip the checking on future executions of node */ + exprstate->evalfunc = ExecEvalScalarVarFast; + + /* Fetch the value from the slot */ + return slot_getattr(slot, attnum, isNull); } /* ---------------------------------------------------------------- - * ExecEvalScalarVar + * ExecEvalScalarVarFast * * Returns a Datum for a scalar variable. * ---------------------------------------------------------------- */ static Datum -ExecEvalScalarVar(ExprState *exprstate, ExprContext *econtext, - bool *isNull, ExprDoneCond *isDone) +ExecEvalScalarVarFast(ExprState *exprstate, ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone) { Var *variable = (Var *) exprstate->expr; TupleTableSlot *slot; @@ -749,14 +661,184 @@ ExecEvalScalarVar(ExprState *exprstate, ExprContext *econtext, /* ---------------------------------------------------------------- * ExecEvalWholeRowVar * + * Returns a Datum whose value is the value of a whole-row range + * variable with respect to given expression context. + * + * Note: ExecEvalWholeRowVar is executed only the first time through in a + * given plan; it changes the ExprState's function pointer to pass control + * directly to ExecEvalWholeRowFast or ExecEvalWholeRowSlow after making + * one-time checks. + * ---------------------------------------------------------------- + */ +static Datum +ExecEvalWholeRowVar(WholeRowVarExprState *wrvstate, ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone) +{ + Var *variable = (Var *) wrvstate->xprstate.expr; + TupleTableSlot *slot; + TupleDesc slot_tupdesc; + bool needslow = false; + + if (isDone) + *isDone = ExprSingleResult; + + /* This was checked by ExecInitExpr */ + Assert(variable->varattno == InvalidAttrNumber); + + /* Get the input slot we want */ + Assert(variable->varno != INNER); + Assert(variable->varno != OUTER); + slot = econtext->ecxt_scantuple; + + /* + * If the input tuple came from a subquery, it might contain "resjunk" + * columns (such as GROUP BY or ORDER BY columns), which we don't want to + * keep in the whole-row result. We can get rid of such columns by + * passing the tuple through a JunkFilter --- but to make one, we have to + * lay our hands on the subquery's targetlist. Fortunately, there are not + * very many cases where this can happen, and we can identify all of them + * by examining our parent PlanState. We assume this is not an issue in + * standalone expressions that don't have parent plans. (Whole-row Vars + * can occur in such expressions, but they will always be referencing + * table rows.) + */ + if (wrvstate->parent) + { + PlanState *subplan = NULL; + + switch (nodeTag(wrvstate->parent)) + { + case T_SubqueryScanState: + subplan = ((SubqueryScanState *) wrvstate->parent)->subplan; + break; + default: + break; + } + + if (subplan) + { + bool junk_filter_needed = false; + ListCell *tlist; + + /* Detect whether subplan tlist actually has any junk columns */ + foreach(tlist, subplan->plan->targetlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(tlist); + + if (tle->resjunk) + { + junk_filter_needed = true; + break; + } + } + + /* If so, build the junkfilter in the query memory context */ + if (junk_filter_needed) + { + MemoryContext oldcontext; + + oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory); + wrvstate->wrv_junkFilter = + ExecInitJunkFilter(subplan->plan->targetlist, + ExecGetResultType(subplan)->tdhasoid, + NULL); + MemoryContextSwitchTo(oldcontext); + } + } + } + + /* Apply the junkfilter if any */ + if (wrvstate->wrv_junkFilter != NULL) + slot = ExecFilterJunk(wrvstate->wrv_junkFilter, slot); + + slot_tupdesc = slot->tts_tupleDescriptor; + + /* + * If it's a RECORD Var, we'll use the slot's type ID info. It's likely + * that the slot's type is also RECORD; if so, make sure it's been + * "blessed", so that the Datum can be interpreted later. + * + * If the Var identifies a named composite type, we must check that the + * actual tuple type is compatible with it. + */ + if (variable->vartype == RECORDOID) + { + if (slot_tupdesc->tdtypeid == RECORDOID && + slot_tupdesc->tdtypmod < 0) + assign_record_type_typmod(slot_tupdesc); + } + else + { + TupleDesc var_tupdesc; + int i; + + /* + * We really only care about numbers of attributes and data types. + * Also, we can ignore type mismatch on columns that are dropped in + * the destination type, so long as (1) the physical storage matches + * or (2) the actual column value is NULL. Case (1) is helpful in + * some cases involving out-of-date cached plans, while case (2) is + * expected behavior in situations such as an INSERT into a table with + * dropped columns (the planner typically generates an INT4 NULL + * regardless of the dropped column type). If we find a dropped + * column and cannot verify that case (1) holds, we have to use + * ExecEvalWholeRowSlow to check (2) for each row. + */ + var_tupdesc = lookup_rowtype_tupdesc(variable->vartype, -1); + + if (var_tupdesc->natts != slot_tupdesc->natts) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table row type and query-specified row type do not match"), + errdetail("Table row contains %d attributes, but query expects %d.", + slot_tupdesc->natts, var_tupdesc->natts))); + + for (i = 0; i < var_tupdesc->natts; i++) + { + Form_pg_attribute vattr = var_tupdesc->attrs[i]; + Form_pg_attribute sattr = slot_tupdesc->attrs[i]; + + if (vattr->atttypid == sattr->atttypid) + continue; /* no worries */ + if (!vattr->attisdropped) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table row type and query-specified row type do not match"), + errdetail("Table has type %s at ordinal position %d, but query expects %s.", + format_type_be(sattr->atttypid), + i + 1, + format_type_be(vattr->atttypid)))); + + if (vattr->attlen != sattr->attlen || + vattr->attalign != sattr->attalign) + needslow = true; /* need runtime check for null */ + } + + ReleaseTupleDesc(var_tupdesc); + } + + /* Skip the checking on future executions of node */ + if (needslow) + wrvstate->xprstate.evalfunc = (ExprStateEvalFunc) ExecEvalWholeRowSlow; + else + wrvstate->xprstate.evalfunc = (ExprStateEvalFunc) ExecEvalWholeRowFast; + + /* Fetch the value */ + return (*wrvstate->xprstate.evalfunc) ((ExprState *) wrvstate, econtext, + isNull, isDone); +} + +/* ---------------------------------------------------------------- + * ExecEvalWholeRowFast + * * Returns a Datum for a whole-row variable. * ---------------------------------------------------------------- */ static Datum -ExecEvalWholeRowVar(ExprState *exprstate, ExprContext *econtext, - bool *isNull, ExprDoneCond *isDone) +ExecEvalWholeRowFast(WholeRowVarExprState *wrvstate, ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone) { - Var *variable = (Var *) exprstate->expr; + Var *variable = (Var *) wrvstate->xprstate.expr; TupleTableSlot *slot = econtext->ecxt_scantuple; HeapTuple tuple; TupleDesc tupleDesc; @@ -766,6 +848,10 @@ ExecEvalWholeRowVar(ExprState *exprstate, ExprContext *econtext, *isDone = ExprSingleResult; *isNull = false; + /* Apply the junkfilter if any */ + if (wrvstate->wrv_junkFilter != NULL) + slot = ExecFilterJunk(wrvstate->wrv_junkFilter, slot); + tuple = ExecFetchSlotTuple(slot); tupleDesc = slot->tts_tupleDescriptor; @@ -804,36 +890,53 @@ ExecEvalWholeRowVar(ExprState *exprstate, ExprContext *econtext, * ---------------------------------------------------------------- */ static Datum -ExecEvalWholeRowSlow(ExprState *exprstate, ExprContext *econtext, +ExecEvalWholeRowSlow(WholeRowVarExprState *wrvstate, ExprContext *econtext, bool *isNull, ExprDoneCond *isDone) { - Var *variable = (Var *) exprstate->expr; + Var *variable = (Var *) wrvstate->xprstate.expr; TupleTableSlot *slot = econtext->ecxt_scantuple; HeapTuple tuple; + TupleDesc tupleDesc; TupleDesc var_tupdesc; HeapTupleHeader dtuple; + int i; if (isDone) *isDone = ExprSingleResult; *isNull = false; - /* - * Currently, the only case handled here is stripping of trailing resjunk - * fields, which we do in a slightly chintzy way by just adjusting the - * tuple's natts header field. Possibly there will someday be a need for - * more-extensive rearrangements, in which case it'd be worth - * disassembling and reassembling the tuple (perhaps use a JunkFilter for - * that?) - */ + /* Apply the junkfilter if any */ + if (wrvstate->wrv_junkFilter != NULL) + slot = ExecFilterJunk(wrvstate->wrv_junkFilter, slot); + + tuple = ExecFetchSlotTuple(slot); + tupleDesc = slot->tts_tupleDescriptor; + Assert(variable->vartype != RECORDOID); var_tupdesc = lookup_rowtype_tupdesc(variable->vartype, -1); - tuple = ExecFetchSlotTuple(slot); + /* Check to see if any dropped attributes are non-null */ + for (i = 0; i < var_tupdesc->natts; i++) + { + Form_pg_attribute vattr = var_tupdesc->attrs[i]; + Form_pg_attribute sattr = tupleDesc->attrs[i]; + + if (!vattr->attisdropped) + continue; /* already checked non-dropped cols */ + if (heap_attisnull(tuple, i+1)) + continue; /* null is always okay */ + if (vattr->attlen != sattr->attlen || + vattr->attalign != sattr->attalign) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table row type and query-specified row type do not match"), + errdetail("Physical storage mismatch on dropped attribute at ordinal position %d.", + i + 1))); + } /* * We have to make a copy of the tuple so we can safely insert the Datum - * overhead fields, which are not set in on-disk tuples; not to mention - * fooling with its natts field. + * overhead fields, which are not set in on-disk tuples. */ dtuple = (HeapTupleHeader) palloc(tuple->t_len); memcpy((char *) dtuple, (char *) tuple->t_data, tuple->t_len); @@ -842,9 +945,6 @@ ExecEvalWholeRowSlow(ExprState *exprstate, ExprContext *econtext, HeapTupleHeaderSetTypeId(dtuple, variable->vartype); HeapTupleHeaderSetTypMod(dtuple, variable->vartypmod); - Assert(HeapTupleHeaderGetNatts(dtuple) >= var_tupdesc->natts); - HeapTupleHeaderSetNatts(dtuple, var_tupdesc->natts); - ReleaseTupleDesc(var_tupdesc); return PointerGetDatum(dtuple); @@ -3541,7 +3641,7 @@ ExecEvalFieldSelect(FieldSelectState *fstate, } /* Check for type mismatch --- possible after ALTER COLUMN TYPE? */ - /* As in ExecEvalVar, we should but can't check typmod */ + /* As in ExecEvalScalarVar, we should but can't check typmod */ if (fselect->resulttype != attr->atttypid) ereport(ERROR, (errmsg("attribute %d has wrong type", fieldnum), @@ -3876,8 +3976,21 @@ ExecInitExpr(Expr *node, PlanState *parent) switch (nodeTag(node)) { case T_Var: - state = (ExprState *) makeNode(ExprState); - state->evalfunc = ExecEvalVar; + /* varattno == InvalidAttrNumber means it's a whole-row Var */ + if (((Var *) node)->varattno == InvalidAttrNumber) + { + WholeRowVarExprState *wstate = makeNode(WholeRowVarExprState); + + wstate->parent = parent; + wstate->wrv_junkFilter = NULL; + state = (ExprState *) wstate; + state->evalfunc = (ExprStateEvalFunc) ExecEvalWholeRowVar; + } + else + { + state = (ExprState *) makeNode(ExprState); + state->evalfunc = ExecEvalScalarVar; + } break; case T_Const: state = (ExprState *) makeNode(ExprState); diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c index d149e1280f7..b6a82566a37 100644 --- a/src/backend/executor/execUtils.c +++ b/src/backend/executor/execUtils.c @@ -575,7 +575,7 @@ ExecBuildProjectionInfo(List *targetList, * Determine whether the target list consists entirely of simple Var * references (ie, references to non-system attributes) that match the * input. If so, we can use the simpler ExecVariableList instead of - * ExecTargetList. (Note: if there is a type mismatch then ExecEvalVar + * ExecTargetList. (Note: if there is a type mismatch then ExecEvalScalarVar * will probably throw an error at runtime, but we leave that to it.) */ isVarList = true; diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index ba588b0767a..02318794ea4 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -491,6 +491,17 @@ typedef struct GenericExprState ExprState *arg; /* state of my child node */ } GenericExprState; +/* ---------------- + * WholeRowVarExprState node + * ---------------- + */ +typedef struct WholeRowVarExprState +{ + ExprState xprstate; + struct PlanState *parent; /* parent PlanState, or NULL if none */ + JunkFilter *wrv_junkFilter; /* JunkFilter to remove resjunk cols */ +} WholeRowVarExprState; + /* ---------------- * AggrefExprState node * ---------------- diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 5a6745a2141..3025368e49e 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -176,6 +176,7 @@ typedef enum NodeTag T_NullTestState, T_CoerceToDomainState, T_DomainConstraintState, + T_WholeRowVarExprState, /* will be in a more natural position in 9.3 */ /* * TAGS FOR PLANNER NODES (relation.h) diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index 468829a572c..056e05203b9 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -478,6 +478,20 @@ group by f1,f2,fs; ----+----+---- (0 rows) +-- +-- Check that whole-row Vars reading the result of a subselect don't include +-- any junk columns therein +-- +select q from (select max(f1) from int4_tbl group by f1 order by f1) q; + q +--------------- + (-2147483647) + (-123456) + (0) + (123456) + (2147483647) +(5 rows) + -- -- Test case for sublinks pushed down into subselects via join alias expansion -- diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index 6e1eda157a4..8b706c13bd9 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -310,6 +310,13 @@ select * from from t1 up) ss group by f1,f2,fs; +-- +-- Check that whole-row Vars reading the result of a subselect don't include +-- any junk columns therein +-- + +select q from (select max(f1) from int4_tbl group by f1 order by f1) q; + -- -- Test case for sublinks pushed down into subselects via join alias expansion --