diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index e284fd71d7..c6ef340f03 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -798,6 +798,41 @@ ExecInitExprRec(Expr *node, ExprState *state,
 				break;
 			}
 
+		case T_GroupedVar:
+
+			/*
+			 * If GroupedVar appears in targetlist of Agg node, it can
+			 * represent either Aggref or grouping expression.
+			 *
+			 * TODO Consider doing this expansion earlier, e.g. in setrefs.c.
+			 */
+			if (state->parent && (IsA(state->parent, AggState)))
+			{
+				GroupedVar *gvar = (GroupedVar *) node;
+
+				if (IsA(gvar->gvexpr, Aggref))
+				{
+					ExecInitExprRec((Expr *) gvar->gvexpr, state,
+									resv, resnull);
+				}
+				else
+					ExecInitExprRec((Expr *) gvar->gvexpr, state,
+									resv, resnull);
+				break;
+			}
+			else
+			{
+				/*
+				 * set_plan_refs should have replaced GroupedVar in the
+				 * targetlist with an ordinary Var.
+				 *
+				 * XXX Should we error out here? There's at least one legal
+				 * case here which we'd have to check: a Result plan with no
+				 * outer plan which represents an empty Append plan.
+				 */
+				break;
+			}
+
 		case T_GroupingFunc:
 			{
 				GroupingFunc *grp_node = (GroupingFunc *) node;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 7c8220cf65..520557d2ac 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1433,6 +1433,7 @@ _copyAggref(const Aggref *from)
 	COPY_SCALAR_FIELD(aggcollid);
 	COPY_SCALAR_FIELD(inputcollid);
 	COPY_SCALAR_FIELD(aggtranstype);
+	COPY_SCALAR_FIELD(aggcombinefn);
 	COPY_NODE_FIELD(aggargtypes);
 	COPY_NODE_FIELD(aggdirectargs);
 	COPY_NODE_FIELD(args);
@@ -2285,6 +2286,22 @@ _copyPlaceHolderVar(const PlaceHolderVar *from)
 }
 
 /*
+ * _copyGroupedVar
+ */
+static GroupedVar *
+_copyGroupedVar(const GroupedVar *from)
+{
+	GroupedVar *newnode = makeNode(GroupedVar);
+
+	COPY_NODE_FIELD(gvexpr);
+	COPY_SCALAR_FIELD(sortgroupref);
+	COPY_SCALAR_FIELD(gvid);
+	COPY_SCALAR_FIELD(width);
+
+	return newnode;
+}
+
+/*
  * _copySpecialJoinInfo
  */
 static SpecialJoinInfo *
@@ -2343,6 +2360,20 @@ _copyPlaceHolderInfo(const PlaceHolderInfo *from)
 	return newnode;
 }
 
+static GroupedVarInfo *
+_copyGroupedVarInfo(const GroupedVarInfo *from)
+{
+	GroupedVarInfo *newnode = makeNode(GroupedVarInfo);
+
+	COPY_SCALAR_FIELD(gvid);
+	COPY_NODE_FIELD(gvexpr);
+	COPY_SCALAR_FIELD(sortgroupref);
+	COPY_SCALAR_FIELD(gv_eval_at);
+	COPY_SCALAR_FIELD(derived);
+
+	return newnode;
+}
+
 /* ****************************************************************
  *					parsenodes.h copy functions
  * ****************************************************************
@@ -5101,6 +5132,9 @@ copyObjectImpl(const void *from)
 		case T_PlaceHolderVar:
 			retval = _copyPlaceHolderVar(from);
 			break;
+		case T_GroupedVar:
+			retval = _copyGroupedVar(from);
+			break;
 		case T_SpecialJoinInfo:
 			retval = _copySpecialJoinInfo(from);
 			break;
@@ -5110,6 +5144,9 @@ copyObjectImpl(const void *from)
 		case T_PlaceHolderInfo:
 			retval = _copyPlaceHolderInfo(from);
 			break;
+		case T_GroupedVarInfo:
+			retval = _copyGroupedVarInfo(from);
+			break;
 
 			/*
 			 * VALUE NODES
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 378f2facb8..9d990ba828 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -873,6 +873,14 @@ _equalPlaceHolderVar(const PlaceHolderVar *a, const PlaceHolderVar *b)
 }
 
 static bool
+_equalGroupedVar(const GroupedVar *a, const GroupedVar *b)
+{
+	COMPARE_SCALAR_FIELD(gvid);
+
+	return true;
+}
+
+static bool
 _equalSpecialJoinInfo(const SpecialJoinInfo *a, const SpecialJoinInfo *b)
 {
 	COMPARE_BITMAPSET_FIELD(min_lefthand);
@@ -3173,6 +3181,9 @@ equal(const void *a, const void *b)
 		case T_PlaceHolderVar:
 			retval = _equalPlaceHolderVar(a, b);
 			break;
+		case T_GroupedVar:
+			retval = _equalGroupedVar(a, b);
+			break;
 		case T_SpecialJoinInfo:
 			retval = _equalSpecialJoinInfo(a, b);
 			break;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index a10014f755..54d20f811f 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -259,6 +259,9 @@ exprType(const Node *expr)
 		case T_PlaceHolderVar:
 			type = exprType((Node *) ((const PlaceHolderVar *) expr)->phexpr);
 			break;
+		case T_GroupedVar:
+			type = exprType((Node *) ((const GroupedVar *) expr)->gvexpr);
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
 			type = InvalidOid;	/* keep compiler quiet */
@@ -492,6 +495,8 @@ exprTypmod(const Node *expr)
 			return ((const SetToDefault *) expr)->typeMod;
 		case T_PlaceHolderVar:
 			return exprTypmod((Node *) ((const PlaceHolderVar *) expr)->phexpr);
+		case T_GroupedVar:
+			return exprTypmod((Node *) ((const GroupedVar *) expr)->gvexpr);
 		default:
 			break;
 	}
@@ -903,6 +908,9 @@ exprCollation(const Node *expr)
 		case T_PlaceHolderVar:
 			coll = exprCollation((Node *) ((const PlaceHolderVar *) expr)->phexpr);
 			break;
+		case T_GroupedVar:
+			coll = exprCollation((Node *) ((const GroupedVar *) expr)->gvexpr);
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
 			coll = InvalidOid;	/* keep compiler quiet */
@@ -2187,6 +2195,8 @@ expression_tree_walker(Node *node,
 			break;
 		case T_PlaceHolderVar:
 			return walker(((PlaceHolderVar *) node)->phexpr, context);
+		case T_GroupedVar:
+			return walker(((GroupedVar *) node)->gvexpr, context);
 		case T_InferenceElem:
 			return walker(((InferenceElem *) node)->expr, context);
 		case T_AppendRelInfo:
@@ -2993,6 +3003,15 @@ expression_tree_mutator(Node *node,
 				return (Node *) newnode;
 			}
 			break;
+		case T_GroupedVar:
+			{
+				GroupedVar *gv = (GroupedVar *) node;
+				GroupedVar *newnode;
+
+				FLATCOPY(newnode, gv, GroupedVar);
+				MUTATE(newnode->gvexpr, gv->gvexpr, Expr *);
+				return (Node *) newnode;
+			}
 		case T_InferenceElem:
 			{
 				InferenceElem *inferenceelemdexpr = (InferenceElem *) node;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 6269f474d2..25ca3fb57b 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1207,6 +1207,7 @@ _outAggref(StringInfo str, const Aggref *node)
 	WRITE_OID_FIELD(aggcollid);
 	WRITE_OID_FIELD(inputcollid);
 	WRITE_OID_FIELD(aggtranstype);
+	WRITE_OID_FIELD(aggcombinefn);
 	WRITE_NODE_FIELD(aggargtypes);
 	WRITE_NODE_FIELD(aggdirectargs);
 	WRITE_NODE_FIELD(args);
@@ -2297,6 +2298,7 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
 	WRITE_NODE_FIELD(append_rel_list);
 	WRITE_NODE_FIELD(rowMarks);
 	WRITE_NODE_FIELD(placeholder_list);
+	WRITE_NODE_FIELD(grouped_var_list);
 	WRITE_NODE_FIELD(fkey_list);
 	WRITE_NODE_FIELD(query_pathkeys);
 	WRITE_NODE_FIELD(group_pathkeys);
@@ -2344,6 +2346,7 @@ _outRelOptInfo(StringInfo str, const RelOptInfo *node)
 	WRITE_NODE_FIELD(cheapest_parameterized_paths);
 	WRITE_BITMAPSET_FIELD(direct_lateral_relids);
 	WRITE_BITMAPSET_FIELD(lateral_relids);
+	WRITE_NODE_FIELD(agg_info);
 	WRITE_UINT_FIELD(relid);
 	WRITE_OID_FIELD(reltablespace);
 	WRITE_ENUM_FIELD(rtekind, RTEKind);
@@ -2521,6 +2524,18 @@ _outParamPathInfo(StringInfo str, const ParamPathInfo *node)
 }
 
 static void
+_outRelAggInfo(StringInfo str, const RelAggInfo *node)
+{
+	WRITE_NODE_TYPE("RELAGGINFO");
+
+	WRITE_NODE_FIELD(target);
+	WRITE_NODE_FIELD(input);
+	WRITE_NODE_FIELD(group_clauses);
+	WRITE_NODE_FIELD(group_exprs);
+	WRITE_NODE_FIELD(agg_exprs);
+}
+
+static void
 _outRestrictInfo(StringInfo str, const RestrictInfo *node)
 {
 	WRITE_NODE_TYPE("RESTRICTINFO");
@@ -2564,6 +2579,17 @@ _outPlaceHolderVar(StringInfo str, const PlaceHolderVar *node)
 }
 
 static void
+_outGroupedVar(StringInfo str, const GroupedVar *node)
+{
+	WRITE_NODE_TYPE("GROUPEDVAR");
+
+	WRITE_NODE_FIELD(gvexpr);
+	WRITE_UINT_FIELD(sortgroupref);
+	WRITE_UINT_FIELD(gvid);
+	WRITE_INT_FIELD(width);
+}
+
+static void
 _outSpecialJoinInfo(StringInfo str, const SpecialJoinInfo *node)
 {
 	WRITE_NODE_TYPE("SPECIALJOININFO");
@@ -2608,6 +2634,18 @@ _outPlaceHolderInfo(StringInfo str, const PlaceHolderInfo *node)
 }
 
 static void
+_outGroupedVarInfo(StringInfo str, const GroupedVarInfo *node)
+{
+	WRITE_NODE_TYPE("GROUPEDVARINFO");
+
+	WRITE_UINT_FIELD(gvid);
+	WRITE_NODE_FIELD(gvexpr);
+	WRITE_UINT_FIELD(sortgroupref);
+	WRITE_BITMAPSET_FIELD(gv_eval_at);
+	WRITE_BOOL_FIELD(derived);
+}
+
+static void
 _outMinMaxAggInfo(StringInfo str, const MinMaxAggInfo *node)
 {
 	WRITE_NODE_TYPE("MINMAXAGGINFO");
@@ -4134,12 +4172,18 @@ outNode(StringInfo str, const void *obj)
 			case T_ParamPathInfo:
 				_outParamPathInfo(str, obj);
 				break;
+			case T_RelAggInfo:
+				_outRelAggInfo(str, obj);
+				break;
 			case T_RestrictInfo:
 				_outRestrictInfo(str, obj);
 				break;
 			case T_PlaceHolderVar:
 				_outPlaceHolderVar(str, obj);
 				break;
+			case T_GroupedVar:
+				_outGroupedVar(str, obj);
+				break;
 			case T_SpecialJoinInfo:
 				_outSpecialJoinInfo(str, obj);
 				break;
@@ -4149,6 +4193,9 @@ outNode(StringInfo str, const void *obj)
 			case T_PlaceHolderInfo:
 				_outPlaceHolderInfo(str, obj);
 				break;
+			case T_GroupedVarInfo:
+				_outGroupedVarInfo(str, obj);
+				break;
 			case T_MinMaxAggInfo:
 				_outMinMaxAggInfo(str, obj);
 				break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3254524223..dd9573a0ef 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -534,6 +534,22 @@ _readVar(void)
 }
 
 /*
+ * _readGroupedVar
+ */
+static GroupedVar *
+_readGroupedVar(void)
+{
+	READ_LOCALS(GroupedVar);
+
+	READ_NODE_FIELD(gvexpr);
+	READ_UINT_FIELD(sortgroupref);
+	READ_UINT_FIELD(gvid);
+	READ_INT_FIELD(width);
+
+	READ_DONE();
+}
+
+/*
  * _readConst
  */
 static Const *
@@ -589,6 +605,7 @@ _readAggref(void)
 	READ_OID_FIELD(aggcollid);
 	READ_OID_FIELD(inputcollid);
 	READ_OID_FIELD(aggtranstype);
+	READ_OID_FIELD(aggcombinefn);
 	READ_NODE_FIELD(aggargtypes);
 	READ_NODE_FIELD(aggdirectargs);
 	READ_NODE_FIELD(args);
@@ -2547,6 +2564,8 @@ parseNodeString(void)
 		return_value = _readTableFunc();
 	else if (MATCH("VAR", 3))
 		return_value = _readVar();
+	else if (MATCH("GROUPEDVAR", 10))
+		return_value = _readGroupedVar();
 	else if (MATCH("CONST", 5))
 		return_value = _readConst();
 	else if (MATCH("PARAM", 5))
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 0e80aeb65c..98485f5298 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -58,6 +58,7 @@ typedef struct pushdown_safety_info
 
 /* These parameters are set by GUC */
 bool		enable_geqo = false;	/* just in case GUC doesn't set it */
+bool		enable_agg_pushdown;
 int			geqo_threshold;
 int			min_parallel_table_scan_size;
 int			min_parallel_index_scan_size;
@@ -73,16 +74,17 @@ static void set_base_rel_consider_startup(PlannerInfo *root);
 static void set_base_rel_sizes(PlannerInfo *root);
 static void set_base_rel_pathlists(PlannerInfo *root);
 static void set_rel_size(PlannerInfo *root, RelOptInfo *rel,
-			 Index rti, RangeTblEntry *rte);
+			 Index rti, RangeTblEntry *rte, bool grouped);
 static void set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
-				 Index rti, RangeTblEntry *rte);
+				 Index rti, RangeTblEntry *rte,
+				 bool grouped);
 static void set_plain_rel_size(PlannerInfo *root, RelOptInfo *rel,
-				   RangeTblEntry *rte);
+				   RangeTblEntry *rte, bool grouped);
 static void create_plain_partial_paths(PlannerInfo *root, RelOptInfo *rel);
 static void set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 						  RangeTblEntry *rte);
 static void set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
-					   RangeTblEntry *rte);
+					   RangeTblEntry *rte, bool grouped);
 static void set_tablesample_rel_size(PlannerInfo *root, RelOptInfo *rel,
 						 RangeTblEntry *rte);
 static void set_tablesample_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
@@ -92,9 +94,11 @@ static void set_foreign_size(PlannerInfo *root, RelOptInfo *rel,
 static void set_foreign_pathlist(PlannerInfo *root, RelOptInfo *rel,
 					 RangeTblEntry *rte);
 static void set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
-					Index rti, RangeTblEntry *rte);
+					Index rti, RangeTblEntry *rte,
+					bool grouped);
 static void set_append_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
-						Index rti, RangeTblEntry *rte);
+						Index rti, RangeTblEntry *rte,
+						bool grouped);
 static void generate_mergeappend_paths(PlannerInfo *root, RelOptInfo *rel,
 						   List *live_childrels,
 						   List *all_child_pathkeys,
@@ -118,7 +122,8 @@ static void set_namedtuplestore_pathlist(PlannerInfo *root, RelOptInfo *rel,
 							 RangeTblEntry *rte);
 static void set_worktable_pathlist(PlannerInfo *root, RelOptInfo *rel,
 					   RangeTblEntry *rte);
-static RelOptInfo *make_rel_from_joinlist(PlannerInfo *root, List *joinlist);
+static RelOptInfo *make_rel_from_joinlist(PlannerInfo *root,
+					   List *joinlist);
 static bool subquery_is_pushdown_safe(Query *subquery, Query *topquery,
 						  pushdown_safety_info *safetyInfo);
 static bool recurse_pushdown_safe(Node *setOp, Query *topquery,
@@ -140,7 +145,8 @@ static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
 /*
  * make_one_rel
  *	  Finds all possible access paths for executing a query, returning a
- *	  single rel that represents the join of all base rels in the query.
+ *	  single rel that represents the join of all base rels in the query. If
+ *	  possible, also return a join that contains partial aggregate(s).
  */
 RelOptInfo *
 make_one_rel(PlannerInfo *root, List *joinlist)
@@ -169,12 +175,16 @@ make_one_rel(PlannerInfo *root, List *joinlist)
 		root->all_baserels = bms_add_member(root->all_baserels, brel->relid);
 	}
 
-	/* Mark base rels as to whether we care about fast-start plans */
+	/*
+	 * Mark base rels as to whether we care about fast-start plans. XXX We
+	 * deliberately do not mark grouped rels --- see the comment on
+	 * consider_startup in build_simple_rel().
+	 */
 	set_base_rel_consider_startup(root);
 
 	/*
-	 * Compute size estimates and consider_parallel flags for each base rel,
-	 * then generate access paths.
+	 * Compute size estimates and consider_parallel flags for each plain and
+	 * each grouped base rel, then generate access paths.
 	 */
 	set_base_rel_sizes(root);
 	set_base_rel_pathlists(root);
@@ -231,6 +241,19 @@ set_base_rel_consider_startup(PlannerInfo *root)
 			RelOptInfo *rel = find_base_rel(root, varno);
 
 			rel->consider_param_startup = true;
+
+			if (rel->grouped)
+			{
+				/*
+				 * As for grouped relations, paths differ substantially by the
+				 * AggStrategy. Paths that use AGG_HASHED should not be
+				 * parameterized (because creation of hashtable would have to
+				 * be repeated for different parameters) but paths using
+				 * AGG_SORTED can be. The latter seems to justify considering
+				 * the startup cost for grouped relation in general.
+				 */
+				rel->grouped->consider_param_startup = true;
+			}
 		}
 	}
 }
@@ -278,7 +301,9 @@ set_base_rel_sizes(PlannerInfo *root)
 		if (root->glob->parallelModeOK)
 			set_rel_consider_parallel(root, rel, rte);
 
-		set_rel_size(root, rel, rti, rte);
+		set_rel_size(root, rel, rti, rte, false);
+		if (rel->grouped)
+			set_rel_size(root, rel, rti, rte, true);
 	}
 }
 
@@ -297,7 +322,9 @@ set_base_rel_pathlists(PlannerInfo *root)
 	{
 		RelOptInfo *rel = root->simple_rel_array[rti];
 
-		/* there may be empty slots corresponding to non-baserel RTEs */
+		/*
+		 * there may be empty slots corresponding to non-baserel RTEs
+		 */
 		if (rel == NULL)
 			continue;
 
@@ -307,7 +334,20 @@ set_base_rel_pathlists(PlannerInfo *root)
 		if (rel->reloptkind != RELOPT_BASEREL)
 			continue;
 
-		set_rel_pathlist(root, rel, rti, root->simple_rte_array[rti]);
+		set_rel_pathlist(root, rel, rti, root->simple_rte_array[rti], false);
+
+		/*
+		 * Create grouped paths for grouped relation if it exists.
+		 */
+		if (rel->grouped)
+		{
+			Assert(rel->grouped->agg_info != NULL);
+			Assert(rel->grouped->grouped == NULL);
+
+			set_rel_pathlist(root, rel, rti,
+							 root->simple_rte_array[rti],
+							 true);
+		}
 	}
 }
 
@@ -317,8 +357,14 @@ set_base_rel_pathlists(PlannerInfo *root)
  */
 static void
 set_rel_size(PlannerInfo *root, RelOptInfo *rel,
-			 Index rti, RangeTblEntry *rte)
+			 Index rti, RangeTblEntry *rte, bool grouped)
 {
+	/*
+	 * build_simple_rel() should not have created rels that do not match this
+	 * condition.
+	 */
+	Assert(!grouped || rte->rtekind == RTE_RELATION);
+
 	if (rel->reloptkind == RELOPT_BASEREL &&
 		relation_excluded_by_constraints(root, rel, rte))
 	{
@@ -338,7 +384,7 @@ set_rel_size(PlannerInfo *root, RelOptInfo *rel,
 	else if (rte->inh)
 	{
 		/* It's an "append relation", process accordingly */
-		set_append_rel_size(root, rel, rti, rte);
+		set_append_rel_size(root, rel, rti, rte, grouped);
 	}
 	else
 	{
@@ -348,6 +394,8 @@ set_rel_size(PlannerInfo *root, RelOptInfo *rel,
 				if (rte->relkind == RELKIND_FOREIGN_TABLE)
 				{
 					/* Foreign table */
+					/* Not supported yet, see build_simple_rel(). */
+					Assert(!grouped);
 					set_foreign_size(root, rel, rte);
 				}
 				else if (rte->relkind == RELKIND_PARTITIONED_TABLE)
@@ -356,17 +404,22 @@ set_rel_size(PlannerInfo *root, RelOptInfo *rel,
 					 * A partitioned table without any partitions is marked as
 					 * a dummy rel.
 					 */
+					if (grouped)
+						rel = rel->grouped;
+
 					set_dummy_rel_pathlist(rel);
 				}
 				else if (rte->tablesample != NULL)
 				{
 					/* Sampled relation */
+					/* Not supported yet, see build_simple_rel(). */
+					Assert(!grouped);
 					set_tablesample_rel_size(root, rel, rte);
 				}
 				else
 				{
 					/* Plain relation */
-					set_plain_rel_size(root, rel, rte);
+					set_plain_rel_size(root, rel, rte, grouped);
 				}
 				break;
 			case RTE_SUBQUERY:
@@ -420,8 +473,16 @@ set_rel_size(PlannerInfo *root, RelOptInfo *rel,
  */
 static void
 set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
-				 Index rti, RangeTblEntry *rte)
+				 Index rti, RangeTblEntry *rte, bool grouped)
 {
+	RelOptInfo *rel_plain = rel;	/* non-grouped relation */
+
+	/*
+	 * add_grouped_base_rels_to_query() should not have created rels that do
+	 * not match this condition.
+	 */
+	Assert(!grouped || rte->rtekind == RTE_RELATION);
+
 	if (IS_DUMMY_REL(rel))
 	{
 		/* We already proved the relation empty, so nothing more to do */
@@ -429,7 +490,7 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 	else if (rte->inh)
 	{
 		/* It's an "append relation", process accordingly */
-		set_append_rel_pathlist(root, rel, rti, rte);
+		set_append_rel_pathlist(root, rel, rti, rte, grouped);
 	}
 	else
 	{
@@ -439,17 +500,21 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 				if (rte->relkind == RELKIND_FOREIGN_TABLE)
 				{
 					/* Foreign table */
+					/* Not supported yet, see build_simple_rel(). */
+					Assert(!grouped);
 					set_foreign_pathlist(root, rel, rte);
 				}
 				else if (rte->tablesample != NULL)
 				{
 					/* Sampled relation */
+					/* Not supported yet, see build_simple_rel(). */
+					Assert(!grouped);
 					set_tablesample_rel_pathlist(root, rel, rte);
 				}
 				else
 				{
 					/* Plain relation */
-					set_plain_rel_pathlist(root, rel, rte);
+					set_plain_rel_pathlist(root, rel, rte, grouped);
 				}
 				break;
 			case RTE_SUBQUERY:
@@ -479,6 +544,9 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 		}
 	}
 
+	if (grouped)
+		rel = rel->grouped;
+
 	/*
 	 * If this is a baserel, we should normally consider gathering any partial
 	 * paths we may have created for it.
@@ -491,9 +559,13 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 	 * Also, if this is the topmost scan/join rel (that is, the only baserel),
 	 * we postpone this until the final scan/join targelist is available (see
 	 * grouping_planner).
+	 *
+	 * Note on aggregation push-down: parallel paths are not supported until
+	 * we implement the feature using 2-stage aggregation.
 	 */
 	if (rel->reloptkind == RELOPT_BASEREL &&
-		bms_membership(root->all_baserels) != BMS_SINGLETON)
+		bms_membership(root->all_baserels) != BMS_SINGLETON &&
+		!grouped)
 		generate_gather_paths(root, rel, false);
 
 	/*
@@ -504,6 +576,22 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 	if (set_rel_pathlist_hook)
 		(*set_rel_pathlist_hook) (root, rel, rti, rte);
 
+	/*
+	 * Get rid of the grouped relations which have no paths (and to which
+	 * generate_gather_paths() won't add any).
+	 */
+	if (grouped && rel->pathlist == NIL)
+	{
+		/*
+		 * This grouped rel should not contain any partial paths.
+		 */
+		Assert(rel->partial_pathlist == NIL);
+
+		pfree(rel_plain->grouped);
+		rel_plain->grouped = NULL;
+		return;
+	}
+
 	/* Now find the cheapest of the paths for this rel */
 	set_cheapest(rel);
 
@@ -517,8 +605,12 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
  *	  Set size estimates for a plain relation (no subquery, no inheritance)
  */
 static void
-set_plain_rel_size(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
+set_plain_rel_size(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte,
+				   bool grouped)
 {
+	if (grouped)
+		rel = rel->grouped;
+
 	/*
 	 * Test any partial indexes of rel for applicability.  We must do this
 	 * first since partial unique indexes can affect size estimates.
@@ -692,9 +784,15 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
  *	  Build access paths for a plain relation (no subquery, no inheritance)
  */
 static void
-set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
+set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte,
+					   bool grouped)
 {
 	Relids		required_outer;
+	Path	   *seq_path;
+	RelOptInfo *rel_plain = rel;
+
+	if (grouped)
+		rel = rel->grouped;
 
 	/*
 	 * We don't support pushing join clauses into the quals of a seqscan, but
@@ -703,18 +801,43 @@ set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
 	 */
 	required_outer = rel->lateral_relids;
 
-	/* Consider sequential scan */
-	add_path(rel, create_seqscan_path(root, rel, required_outer, 0));
+	/* Consider sequential scan, both plain and grouped. */
+	seq_path = create_seqscan_path(root, rel, required_outer, 0);
+
+
+	/*
+	 * It's probably not good idea to repeat hashed aggregation with different
+	 * parameters, so check if there are no parameters.
+	 */
+	if (!grouped)
+	{
+		/* Try to compute unique keys. */
+		make_uniquekeys(root, seq_path);
+
+		add_path(rel, seq_path);
+	}
+	else if (required_outer == NULL)
+	{
+		/*
+		 * Only AGG_HASHED is suitable here as it does not expect the input
+		 * set to be sorted.
+		 */
+		create_grouped_path(root, rel, seq_path, false, false, AGG_HASHED);
+	}
 
-	/* If appropriate, consider parallel sequential scan */
-	if (rel->consider_parallel && required_outer == NULL)
-		create_plain_partial_paths(root, rel);
+	/* If appropriate, consider parallel sequential scan (plain or grouped) */
+	if (rel->consider_parallel && required_outer == NULL && !grouped)
+		create_plain_partial_paths(root, rel_plain);
 
-	/* Consider index scans */
-	create_index_paths(root, rel);
+	/*
+	 * Consider index scans, possibly including the grouped and grouped
+	 * partial paths.
+	 */
+	create_index_paths(root, rel, grouped);
 
 	/* Consider TID scans */
-	create_tidscan_paths(root, rel);
+	/* TODO Regression test for these paths. */
+	create_tidscan_paths(root, rel, grouped);
 }
 
 /*
@@ -726,8 +849,7 @@ create_plain_partial_paths(PlannerInfo *root, RelOptInfo *rel)
 {
 	int			parallel_workers;
 
-	parallel_workers = compute_parallel_worker(rel, rel->pages, -1,
-											   max_parallel_workers_per_gather);
+	parallel_workers = compute_parallel_worker(rel, rel->pages, -1, max_parallel_workers_per_gather);
 
 	/* If any limit was set to zero, the user doesn't want a parallel scan. */
 	if (parallel_workers <= 0)
@@ -738,6 +860,100 @@ create_plain_partial_paths(PlannerInfo *root, RelOptInfo *rel)
 }
 
 /*
+ * Apply aggregation to a subpath and add the AggPath to the pathlist.
+ *
+ * "precheck" tells whether the aggregation path should first be checked using
+ * add_path_precheck() / add_partial_path_precheck().
+ *
+ * If "parallel" is true, the aggregation path is considered partial in terms
+ * of parallel execution.
+ *
+ * Caution: Since only grouped relation makes sense as an input for this
+ * function, "rel" is the grouped relation even though "agg_kind" is passed
+ * too. This is different from other functions that receive "agg_kind" and use
+ * it to fetch the grouped relation themselves.
+ *
+ * The return value tells whether the path was added to the pathlist.
+ *
+ * TODO Pass the plain rel and use agg_kind to retrieve the grouped one.
+ */
+bool
+create_grouped_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
+					bool precheck, bool parallel, AggStrategy aggstrategy)
+{
+	Path	   *agg_path;
+	RelAggInfo *agg_info = rel->agg_info;
+
+	Assert(agg_info != NULL);
+
+	/*
+	 * We can only support parallel paths if each worker produced a distinct
+	 * set of grouping keys, but such a special case is not known. So this
+	 * list should be empty.
+	 */
+	if (parallel)
+		return false;
+
+	/*
+	 * If the AggPath should be partial, the subpath must be too, and
+	 * therefore the subpath is essentially parallel_safe.
+	 */
+	Assert(subpath->parallel_safe || !parallel);
+
+	/*
+	 * Repeated creation of hash table does not sound like a good idea. Caller
+	 * should avoid asking us to do so.
+	 */
+	Assert(subpath->param_info == NULL || aggstrategy != AGG_HASHED);
+
+	if (aggstrategy == AGG_HASHED)
+		agg_path = (Path *) create_agg_hashed_path(root, subpath,
+												   subpath->rows);
+	else if (aggstrategy == AGG_SORTED)
+		agg_path = (Path *) create_agg_sorted_path(root, subpath,
+												   true,
+												   subpath->rows);
+	else
+		elog(ERROR, "unexpected strategy %d", aggstrategy);
+
+	/* Add the grouped path to the list of grouped base paths. */
+	if (agg_path != NULL)
+	{
+		if (precheck)
+		{
+			List	   *pathkeys;
+
+			/* AGG_HASH is not supposed to generate sorted output. */
+			pathkeys = aggstrategy == AGG_SORTED ? subpath->pathkeys : NIL;
+
+			if (!parallel &&
+				!add_path_precheck(rel, agg_path->startup_cost,
+								   agg_path->total_cost, pathkeys, NULL))
+				return false;
+
+			if (parallel &&
+				!add_partial_path_precheck(rel, agg_path->total_cost,
+										   pathkeys))
+				return false;
+		}
+
+		if (!parallel)
+		{
+			/* Try to compute unique keys. */
+			make_uniquekeys(root, (Path *) agg_path);
+
+			add_path(rel, (Path *) agg_path);
+		}
+		else
+			add_partial_path(rel, (Path *) agg_path);
+
+		return true;
+	}
+
+	return false;
+}
+
+/*
  * set_tablesample_rel_size
  *	  Set size estimates for a sampled relation
  */
@@ -866,7 +1082,7 @@ set_foreign_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
  */
 static void
 set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
-					Index rti, RangeTblEntry *rte)
+					Index rti, RangeTblEntry *rte, bool grouped)
 {
 	int			parentRTindex = rti;
 	bool		has_live_children;
@@ -1016,10 +1232,50 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
 		 * PlaceHolderVars.)  XXX we do not bother to update the cost or width
 		 * fields of childrel->reltarget; not clear if that would be useful.
 		 */
-		childrel->reltarget->exprs = (List *)
-			adjust_appendrel_attrs(root,
-								   (Node *) rel->reltarget->exprs,
-								   1, &appinfo);
+		if (grouped)
+		{
+			RelOptInfo *rel_grouped,
+					   *childrel_grouped;
+
+			Assert(childrel->grouped != NULL);
+
+			childrel_grouped = childrel->grouped;
+			rel_grouped = rel->grouped;
+
+			/*
+			 * Special attention is needed in the grouped case.
+			 *
+			 * copy_simple_rel() didn't create empty target because it's
+			 * better to start with copying one from the parent rel.
+			 */
+			Assert(childrel_grouped->reltarget == NULL &&
+				   childrel_grouped->agg_info == NULL);
+
+			/*
+			 * The parent rel should already have the info that we're setting
+			 * up now for the child.
+			 */
+			Assert(rel_grouped->reltarget != NULL &&
+				   rel_grouped->agg_info != NULL);
+
+			/*
+			 * Translate the targets and grouping expressions so they match
+			 * this child.
+			 */
+			childrel_grouped->agg_info = translate_rel_agg_info(root,
+																rel_grouped->agg_info,
+																&appinfo, 1);
+
+			/*
+			 * The relation paths will generate input for partial aggregation.
+			 */
+			childrel_grouped->reltarget = childrel_grouped->agg_info->input;
+		}
+		else
+			childrel->reltarget->exprs = (List *)
+				adjust_appendrel_attrs(root,
+									   (Node *) rel->reltarget->exprs,
+									   1, &appinfo);
 
 		/*
 		 * We have to make child entries in the EquivalenceClass data
@@ -1181,19 +1437,42 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
 								   1, &appinfo);
 
 		/*
+		 * We have to make child entries in the EquivalenceClass data
+		 * structures as well.  This is needed either if the parent
+		 * participates in some eclass joins (because we will want to consider
+		 * inner-indexscan joins on the individual children) or if the parent
+		 * has useful pathkeys (because we should try to build MergeAppend
+		 * paths that produce those sort orderings).
+		 */
+		if (rel->has_eclass_joins || has_useful_pathkeys(root, rel))
+			add_child_rel_equivalences(root, appinfo, rel, childrel);
+		childrel->has_eclass_joins = rel->has_eclass_joins;
+
+		/*
+		 * Note: we could compute appropriate attr_needed data for the child's
+		 * variables, by transforming the parent's attr_needed through the
+		 * translated_vars mapping.  However, currently there's no need
+		 * because attr_needed is only examined for base relations not
+		 * otherrels.  So we just leave the child's attr_needed empty.
+		 */
+
+		/*
 		 * If parallelism is allowable for this query in general, see whether
 		 * it's allowable for this childrel in particular.  But if we've
 		 * already decided the appendrel is not parallel-safe as a whole,
 		 * there's no point in considering parallelism for this child.  For
 		 * consistency, do this before calling set_rel_size() for the child.
+		 *
+		 * The aggregated relations do not use the consider_parallel flag.
 		 */
-		if (root->glob->parallelModeOK && rel->consider_parallel)
+		if (root->glob->parallelModeOK && rel->consider_parallel &&
+			!grouped)
 			set_rel_consider_parallel(root, childrel, childRTE);
 
 		/*
 		 * Compute the child's size.
 		 */
-		set_rel_size(root, childrel, childRTindex, childRTE);
+		set_rel_size(root, childrel, childRTindex, childRTE, grouped);
 
 		/*
 		 * It is possible that constraint exclusion detected a contradiction
@@ -1299,13 +1578,20 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
  */
 static void
 set_append_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
-						Index rti, RangeTblEntry *rte)
+						Index rti, RangeTblEntry *rte, bool grouped)
 {
 	int			parentRTindex = rti;
 	List	   *live_childrels = NIL;
 	ListCell   *l;
 
 	/*
+	 * TODO Only allow per-child AGGSPLIT_SIMPLE if the partitioning allows
+	 * it, i.e. each partition generates distinct set of grouping keys.
+	 */
+	if (grouped)
+		return;
+
+	/*
 	 * Generate access paths for each member relation, and remember the
 	 * non-dummy children.
 	 */
@@ -1323,7 +1609,7 @@ set_append_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 		/* Re-locate the child RTE and RelOptInfo */
 		childRTindex = appinfo->child_relid;
 		childRTE = root->simple_rte_array[childRTindex];
-		childrel = root->simple_rel_array[childRTindex];
+		childrel = find_base_rel(root, childRTindex);
 
 		/*
 		 * If set_append_rel_size() decided the parent appendrel was
@@ -1337,7 +1623,7 @@ set_append_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 		/*
 		 * Compute the child's access paths.
 		 */
-		set_rel_pathlist(root, childrel, childRTindex, childRTE);
+		set_rel_pathlist(root, childrel, childRTindex, childRTE, grouped);
 
 		/*
 		 * If child is dummy, ignore it.
@@ -1351,13 +1637,9 @@ set_append_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 				list_concat(rel->partitioned_child_rels,
 							list_copy(childrel->partitioned_child_rels));
 
-		/*
-		 * Child is live, so add it to the live_childrels list for use below.
-		 */
 		live_childrels = lappend(live_childrels, childrel);
 	}
 
-	/* Add paths to the append relation. */
 	add_paths_to_append_rel(root, rel, live_childrels);
 }
 
@@ -1794,6 +2076,7 @@ generate_mergeappend_paths(PlannerInfo *root, RelOptInfo *rel,
 						   List *partitioned_rels)
 {
 	ListCell   *lcp;
+	PathTarget *target = NULL;
 
 	foreach(lcp, all_child_pathkeys)
 	{
@@ -1802,23 +2085,25 @@ generate_mergeappend_paths(PlannerInfo *root, RelOptInfo *rel,
 		List	   *total_subpaths = NIL;
 		bool		startup_neq_total = false;
 		ListCell   *lcr;
+		Path	   *path;
 
 		/* Select the child paths for this ordering... */
 		foreach(lcr, live_childrels)
 		{
 			RelOptInfo *childrel = (RelOptInfo *) lfirst(lcr);
+			List	   *pathlist = childrel->pathlist;
 			Path	   *cheapest_startup,
 					   *cheapest_total;
 
 			/* Locate the right paths, if they are available. */
 			cheapest_startup =
-				get_cheapest_path_for_pathkeys(childrel->pathlist,
+				get_cheapest_path_for_pathkeys(pathlist,
 											   pathkeys,
 											   NULL,
 											   STARTUP_COST,
 											   false);
 			cheapest_total =
-				get_cheapest_path_for_pathkeys(childrel->pathlist,
+				get_cheapest_path_for_pathkeys(pathlist,
 											   pathkeys,
 											   NULL,
 											   TOTAL_COST,
@@ -1851,19 +2136,28 @@ generate_mergeappend_paths(PlannerInfo *root, RelOptInfo *rel,
 		}
 
 		/* ... and build the MergeAppend paths */
-		add_path(rel, (Path *) create_merge_append_path(root,
-														rel,
-														startup_subpaths,
-														pathkeys,
-														NULL,
-														partitioned_rels));
+		path = (Path *) create_merge_append_path(root,
+												 rel,
+												 target,
+												 startup_subpaths,
+												 pathkeys,
+												 NULL,
+												 partitioned_rels);
+
+		add_path(rel, path);
+
 		if (startup_neq_total)
-			add_path(rel, (Path *) create_merge_append_path(root,
-															rel,
-															total_subpaths,
-															pathkeys,
-															NULL,
-															partitioned_rels));
+		{
+			path = (Path *) create_merge_append_path(root,
+													 rel,
+													 target,
+													 total_subpaths,
+													 pathkeys,
+													 NULL,
+													 partitioned_rels);
+			add_path(rel, path);
+		}
+
 	}
 }
 
@@ -2665,11 +2959,22 @@ make_rel_from_joinlist(PlannerInfo *root, List *joinlist)
 		root->initial_rels = initial_rels;
 
 		if (join_search_hook)
-			return (*join_search_hook) (root, levels_needed, initial_rels);
+			return (*join_search_hook) (root, levels_needed,
+										initial_rels);
 		else if (enable_geqo && levels_needed >= geqo_threshold)
+		{
+			/*
+			 * TODO Teach GEQO about grouped relations. Don't forget that
+			 * pathlist can be NIL before set_cheapest() gets called.
+			 *
+			 * This processing makes no difference betweend plain and grouped
+			 * rels, so process them in the same loop.
+			 */
 			return geqo(root, levels_needed, initial_rels);
+		}
 		else
-			return standard_join_search(root, levels_needed, initial_rels);
+			return standard_join_search(root, levels_needed,
+										initial_rels);
 	}
 }
 
@@ -2767,6 +3072,23 @@ standard_join_search(PlannerInfo *root, int levels_needed, List *initial_rels)
 			/* Find and save the cheapest paths for this rel */
 			set_cheapest(rel);
 
+			if (rel->grouped)
+			{
+				RelOptInfo *rel_grouped;
+
+				rel_grouped = rel->grouped;
+
+				Assert(rel_grouped->partial_pathlist == NIL);
+
+				if (rel_grouped->pathlist != NIL)
+					set_cheapest(rel_grouped);
+				else
+				{
+					pfree(rel_grouped);
+					rel->grouped = NULL;
+				}
+			}
+
 #ifdef OPTIMIZER_DEBUG
 			debug_print_rel(root, rel);
 #endif
@@ -3404,6 +3726,7 @@ create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel,
 {
 	int			parallel_workers;
 	double		pages_fetched;
+	Path	   *bmhpath;
 
 	/* Compute heap pages for bitmap heap scan */
 	pages_fetched = compute_bitmap_pages(root, rel, bitmapqual, 1.0,
@@ -3415,8 +3738,21 @@ create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel,
 	if (parallel_workers <= 0)
 		return;
 
-	add_partial_path(rel, (Path *) create_bitmap_heap_path(root, rel,
-														   bitmapqual, rel->lateral_relids, 1.0, parallel_workers));
+	bmhpath = (Path *) create_bitmap_heap_path(root, rel, bitmapqual,
+											   rel->lateral_relids, 1.0,
+											   parallel_workers);
+
+	if (rel->agg_info == NULL)
+		add_partial_path(rel, bmhpath);
+	else
+	{
+		/*
+		 * Only AGG_HASHED is suitable here as it does not expect the input
+		 * set to be sorted.
+		 */
+		create_grouped_path(root, rel, (Path *) bmhpath, false, true,
+							AGG_HASHED);
+	}
 }
 
 /*
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 7bf67a0529..b904ba3f85 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -88,6 +88,7 @@
 #include "optimizer/plancat.h"
 #include "optimizer/planmain.h"
 #include "optimizer/restrictinfo.h"
+#include "optimizer/var.h"
 #include "parser/parsetree.h"
 #include "utils/lsyscache.h"
 #include "utils/selfuncs.h"
@@ -1065,6 +1066,17 @@ cost_bitmap_tree_node(Path *path, Cost *cost, Selectivity *selec)
 		*cost = path->total_cost;
 		*selec = ((BitmapOrPath *) path)->bitmapselectivity;
 	}
+	else if (IsA(path, AggPath))
+	{
+		/*
+		 * If partial aggregation was already applied, use only the input
+		 * path.
+		 *
+		 * TODO Take the aggregation into account, both cost and its effect on
+		 * selectivity (i.e. how it reduces the number of rows).
+		 */
+		cost_bitmap_tree_node(((AggPath *) path)->subpath, cost, selec);
+	}
 	else
 	{
 		elog(ERROR, "unrecognized node type: %d", nodeTag(path));
@@ -2287,6 +2299,41 @@ cost_group(Path *path, PlannerInfo *root,
 	path->total_cost = total_cost;
 }
 
+static void
+estimate_join_rows(PlannerInfo *root, Path *path, RelAggInfo *agg_info)
+{
+	bool		grouped = agg_info != NULL;
+
+	if (path->param_info)
+	{
+		double		nrows;
+
+		path->rows = path->param_info->ppi_rows;
+		if (grouped)
+		{
+			nrows = estimate_num_groups(root, agg_info->group_exprs,
+										path->rows, NULL);
+			path->rows = clamp_row_est(nrows);
+		}
+	}
+	else
+	{
+		if (!grouped)
+			path->rows = path->parent->rows;
+		else
+		{
+			/*
+			 * XXX agg_info->rows is an estimate of the output rows if we join
+			 * the non-grouped rels and aggregate the output. However the
+			 * figure can be different if an already grouped rel is joined to
+			 * non-grouped one. Is this worth adding a new field to the
+			 * agg_info?
+			 */
+			path->rows = agg_info->rows;
+		}
+	}
+}
+
 /*
  * initial_cost_nestloop
  *	  Preliminary estimate of the cost of a nestloop join path.
@@ -2408,10 +2455,7 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
 		inner_path_rows = 1;
 
 	/* Mark the path with the correct row estimate */
-	if (path->path.param_info)
-		path->path.rows = path->path.param_info->ppi_rows;
-	else
-		path->path.rows = path->path.parent->rows;
+	estimate_join_rows(root, (Path *) path, path->path.parent->agg_info);
 
 	/* For partial paths, scale row estimate. */
 	if (path->path.parallel_workers > 0)
@@ -2854,10 +2898,8 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 		inner_path_rows = 1;
 
 	/* Mark the path with the correct row estimate */
-	if (path->jpath.path.param_info)
-		path->jpath.path.rows = path->jpath.path.param_info->ppi_rows;
-	else
-		path->jpath.path.rows = path->jpath.path.parent->rows;
+	estimate_join_rows(root, (Path *) path,
+					   path->jpath.path.parent->agg_info);
 
 	/* For partial paths, scale row estimate. */
 	if (path->jpath.path.parallel_workers > 0)
@@ -3279,10 +3321,8 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 	ListCell   *hcl;
 
 	/* Mark the path with the correct row estimate */
-	if (path->jpath.path.param_info)
-		path->jpath.path.rows = path->jpath.path.param_info->ppi_rows;
-	else
-		path->jpath.path.rows = path->jpath.path.parent->rows;
+	estimate_join_rows(root, (Path *) path,
+					   path->jpath.path.parent->agg_info);
 
 	/* For partial paths, scale row estimate. */
 	if (path->jpath.path.parallel_workers > 0)
@@ -3805,8 +3845,9 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
 	 * estimated execution cost given by pg_proc.procost (remember to multiply
 	 * this by cpu_operator_cost).
 	 *
-	 * Vars and Consts are charged zero, and so are boolean operators (AND,
-	 * OR, NOT). Simplistic, but a lot better than no model at all.
+	 * Vars, GroupedVars and Consts are charged zero, and so are boolean
+	 * operators (AND, OR, NOT). Simplistic, but a lot better than no model at
+	 * all.
 	 *
 	 * Should we try to account for the possibility of short-circuit
 	 * evaluation of AND/OR?  Probably *not*, because that would make the
@@ -4287,11 +4328,13 @@ approx_tuple_count(PlannerInfo *root, JoinPath *path, List *quals)
  *		  restriction clauses).
  *	width: the estimated average output tuple width in bytes.
  *	baserestrictcost: estimated cost of evaluating baserestrictinfo clauses.
+ *	grouped: will partial aggregation be applied to each path?
  */
 void
 set_baserel_size_estimates(PlannerInfo *root, RelOptInfo *rel)
 {
 	double		nrows;
+	bool		grouped = rel->agg_info != NULL;
 
 	/* Should only be applied to base relations */
 	Assert(rel->relid > 0);
@@ -4302,12 +4345,31 @@ set_baserel_size_estimates(PlannerInfo *root, RelOptInfo *rel)
 							   0,
 							   JOIN_INNER,
 							   NULL);
-
 	rel->rows = clamp_row_est(nrows);
 
+	/*
+	 * Grouping essentially changes the number of rows.
+	 */
+	if (grouped)
+	{
+		nrows = estimate_num_groups(root,
+									rel->agg_info->group_exprs, nrows,
+									NULL);
+		rel->agg_info->rows = clamp_row_est(nrows);
+	}
+
 	cost_qual_eval(&rel->baserestrictcost, rel->baserestrictinfo, root);
 
-	set_rel_width(root, rel);
+	/*
+	 * The grouped target should have the cost and width set immediately on
+	 * creation, see create_rel_agg_info().
+	 */
+	if (!grouped)
+		set_rel_width(root, rel);
+#ifdef USE_ASSERT_CHECKING
+	else
+		Assert(rel->reltarget->width > 0);
+#endif
 }
 
 /*
@@ -4375,12 +4437,23 @@ set_joinrel_size_estimates(PlannerInfo *root, RelOptInfo *rel,
 						   SpecialJoinInfo *sjinfo,
 						   List *restrictlist)
 {
+	double		outer_rows,
+				inner_rows;
+
+	/*
+	 * Take grouping of the input rels into account.
+	 */
+	outer_rows = outer_rel->agg_info ? outer_rel->agg_info->rows :
+		outer_rel->rows;
+	inner_rows = inner_rel->agg_info ? inner_rel->agg_info->rows :
+		inner_rel->rows;
+
 	rel->rows = calc_joinrel_size_estimate(root,
 										   rel,
 										   outer_rel,
 										   inner_rel,
-										   outer_rel->rows,
-										   inner_rel->rows,
+										   outer_rows,
+										   inner_rows,
 										   sjinfo,
 										   restrictlist);
 }
@@ -5257,11 +5330,11 @@ set_pathtarget_cost_width(PlannerInfo *root, PathTarget *target)
 	foreach(lc, target->exprs)
 	{
 		Node	   *node = (Node *) lfirst(lc);
+		int32		item_width;
 
 		if (IsA(node, Var))
 		{
 			Var		   *var = (Var *) node;
-			int32		item_width;
 
 			/* We should not see any upper-level Vars here */
 			Assert(var->varlevelsup == 0);
@@ -5292,6 +5365,25 @@ set_pathtarget_cost_width(PlannerInfo *root, PathTarget *target)
 			Assert(item_width > 0);
 			tuple_width += item_width;
 		}
+		else if (IsA(node, GroupedVar))
+		{
+			GroupedVar *gvar = (GroupedVar *) node;
+			Node	   *expr;
+
+			/*
+			 * Only AggPath can evaluate GroupedVar if it's an aggregate, or
+			 * the AggPath's input path if it's a generic grouping expression.
+			 * In the other cases the GroupedVar we see here only bubbled up
+			 * from a lower AggPath, so it does not add any cost to the path
+			 * that owns this target.
+			 *
+			 * XXX Is the value worth caching in GroupedVar?
+			 */
+			expr = (Node *) gvar->gvexpr;
+			item_width = get_typavgwidth(exprType(expr), exprTypmod(expr));
+			Assert(item_width > 0);
+			tuple_width += item_width;
+		}
 		else
 		{
 			/*
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index b22b36ec0e..921e6f405b 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -65,6 +65,19 @@ static bool reconsider_outer_join_clause(PlannerInfo *root,
 static bool reconsider_full_join_clause(PlannerInfo *root,
 							RestrictInfo *rinfo);
 
+typedef struct translate_expr_context
+{
+	Var		  **keys;			/* Dictionary keys. */
+	Var		  **values;			/* Dictionary values */
+	int			nitems;			/* Number of dictionary items. */
+	Relids	   *gv_eval_at_p;	/* See GroupedVarInfo. */
+	Index		relid;			/* Translate into this relid. */
+} translate_expr_context;
+
+static Node *translate_expression_to_rels_mutator(Node *node,
+									 translate_expr_context *context);
+static int	var_dictionary_comparator(const void *a, const void *b);
+
 
 /*
  * process_equivalence
@@ -2511,3 +2524,329 @@ is_redundant_derived_clause(RestrictInfo *rinfo, List *clauselist)
 
 	return false;
 }
+
+/*
+ * translate_expression_to_rels
+ *		If the appropriate equivalence classes exist, replace vars in
+ *		gvi->gvexpr with vars whose varno is equal to relid.
+ */
+GroupedVarInfo *
+translate_expression_to_rels(PlannerInfo *root, GroupedVarInfo *gvi,
+							 Index relid)
+{
+	List	   *vars;
+	ListCell   *l1;
+	int			i,
+				j;
+	int			nkeys,
+				nkeys_resolved;
+	Var		  **keys,
+			  **values,
+			  **keys_tmp;
+	Var		   *key,
+			   *key_prev;
+	translate_expr_context context;
+	GroupedVarInfo *result;
+
+	/* Can't do anything w/o equivalence classes. */
+	if (root->eq_classes == NIL)
+		return NULL;
+
+	/*
+	 * Before actually trying to modify the expression tree, find out if all
+	 * vars can be translated.
+	 */
+	vars = pull_var_clause((Node *) gvi->gvexpr, PVC_RECURSE_AGGREGATES);
+
+	/* No vars to translate? */
+	if (vars == NIL)
+		return NULL;
+
+	/*
+	 * Search for individual replacement vars as well as the actual expression
+	 * translation will be more efficient if we use a dictionary with the keys
+	 * (i.e. the "source vars") unique and sorted.
+	 */
+	nkeys = list_length(vars);
+	keys = (Var **) palloc(nkeys * sizeof(Var *));
+	i = 0;
+	foreach(l1, vars)
+	{
+		key = lfirst_node(Var, l1);
+		keys[i++] = key;
+	}
+
+	/*
+	 * Sort the keys by varno. varattno decides where varnos are equal.
+	 */
+	if (nkeys > 1)
+		pg_qsort(keys, nkeys, sizeof(Var *), var_dictionary_comparator);
+
+	/*
+	 * Pick unique values and get rid of the vars that need no translation.
+	 */
+	keys_tmp = (Var **) palloc(nkeys * sizeof(Var *));
+	key_prev = NULL;
+	j = 0;
+	for (i = 0; i < nkeys; i++)
+	{
+		key = keys[i];
+
+		if ((key_prev == NULL || (key->varno != key_prev->varno &&
+								  key->varattno != key_prev->varattno)) &&
+			key->varno != relid)
+			keys_tmp[j++] = key;
+
+		key_prev = key;
+	}
+	pfree(keys);
+	keys = keys_tmp;
+	nkeys = j;
+
+	/*
+	 * Is there actually nothing to be translated?
+	 */
+	if (nkeys == 0)
+	{
+		pfree(keys);
+		return NULL;
+	}
+
+	nkeys_resolved = 0;
+
+	/*
+	 * Find the replacement vars.
+	 */
+	values = (Var **) palloc0(nkeys * sizeof(Var *));
+	foreach(l1, root->eq_classes)
+	{
+		EquivalenceClass *ec = lfirst_node(EquivalenceClass, l1);
+		Relids		ec_var_relids;
+		Var		  **ec_vars;
+		int			ec_nvars;
+		ListCell   *l2;
+
+		/* TODO Re-check if any other EC kind should be ignored. */
+		if (ec->ec_has_volatile || ec->ec_below_outer_join || ec->ec_broken)
+			continue;
+
+		/* Single-element EC can hardly help in translations. */
+		if (list_length(ec->ec_members) == 1)
+			continue;
+
+		/*
+		 * Collect all vars of this EC and their varnos.
+		 *
+		 * ec->ec_relids does not help because we're only interested in a
+		 * subset of EC members.
+		 */
+		ec_vars = (Var **) palloc(list_length(ec->ec_members) * sizeof(Var *));
+		ec_nvars = 0;
+		ec_var_relids = NULL;
+		foreach(l2, ec->ec_members)
+		{
+			EquivalenceMember *em = lfirst_node(EquivalenceMember, l2);
+			Var		   *ec_var;
+
+			if (!IsA(em->em_expr, Var))
+				continue;
+
+			ec_var = castNode(Var, em->em_expr);
+			ec_vars[ec_nvars++] = ec_var;
+			ec_var_relids = bms_add_member(ec_var_relids, ec_var->varno);
+		}
+
+		/*
+		 * At least two vars are needed so that the EC is usable for
+		 * translation.
+		 */
+		if (ec_nvars <= 1)
+		{
+			pfree(ec_vars);
+			bms_free(ec_var_relids);
+			continue;
+		}
+
+		/*
+		 * Now check where this EC can help.
+		 */
+		for (i = 0; i < nkeys; i++)
+		{
+			Relids		ec_rest;
+			bool		relid_ok,
+						key_found;
+			Var		   *key = keys[i];
+			Var		   *value = values[i];
+
+			/* Skip this item if it's already resolved. */
+			if (value != NULL)
+				continue;
+
+			/*
+			 * Can't translate if the EC does not mention key->varno.
+			 */
+			if (!bms_is_member(key->varno, ec_var_relids))
+				continue;
+
+			/*
+			 * Besides key, at least one EC member must belong to the relation
+			 * we're translating our expression to.
+			 */
+			ec_rest = bms_copy(ec_var_relids);
+			ec_rest = bms_del_member(ec_rest, key->varno);
+			relid_ok = bms_is_member(relid, ec_rest);
+			bms_free(ec_rest);
+			if (!relid_ok)
+				continue;
+
+			/*
+			 * The preliminary checks passed, so try to find the exact vars.
+			 */
+			key_found = false;
+			for (j = 0; j < ec_nvars; j++)
+			{
+				Var		   *ec_var = ec_vars[j];
+
+				if (!key_found && key->varno == ec_var->varno &&
+					key->varattno == ec_var->varattno)
+					key_found = true;
+
+				/*
+				 *
+				 * Is this Var useful for our dictionary?
+				 *
+				 * XXX Shouldn't ec_var be copied?
+				 */
+				if (value == NULL && ec_var->varno == relid)
+					value = ec_var;
+
+				if (key_found && value != NULL)
+					break;
+			}
+
+			/*
+			 * The replacement Var must have the same data type, otherwise the
+			 * values are not guaranteed to be grouped in the same way as
+			 * values of the original Var.
+			 */
+			if (key_found && value != NULL &&
+				key->vartype == value->vartype)
+			{
+				values[i] = value;
+				nkeys_resolved++;
+
+				if (nkeys_resolved == nkeys)
+					break;
+			}
+		}
+
+		pfree(ec_vars);
+		bms_free(ec_var_relids);
+
+		/* Don't need to check the remaining ECs? */
+		if (nkeys_resolved == nkeys)
+			break;
+	}
+
+	/* Couldn't compose usable dictionary? */
+	if (nkeys_resolved < nkeys)
+	{
+		pfree(keys);
+		pfree(values);
+		return NULL;
+	}
+
+	result = makeNode(GroupedVarInfo);
+	memcpy(result, gvi, sizeof(GroupedVarInfo));
+
+	/*
+	 * translate_expression_to_rels_mutator updates gv_eval_at.
+	 */
+	result->gv_eval_at = bms_copy(result->gv_eval_at);
+
+	/* The dictionary is ready, so perform the translation. */
+	context.keys = keys;
+	context.values = values;
+	context.nitems = nkeys;
+	context.gv_eval_at_p = &result->gv_eval_at;
+	context.relid = relid;
+	result->gvexpr = (Expr *)
+		translate_expression_to_rels_mutator((Node *) gvi->gvexpr, &context);
+	result->derived = true;
+
+	pfree(keys);
+	pfree(values);
+	return result;
+}
+
+static Node *
+translate_expression_to_rels_mutator(Node *node,
+									 translate_expr_context *context)
+{
+	if (node == NULL)
+		return NULL;
+
+	if (IsA(node, Var))
+	{
+		Var		   *var = castNode(Var, node);
+		Var		  **key_p;
+		Var		   *value;
+		int			index;
+
+		/*
+		 * Simply return the existing variable if already belongs to the
+		 * relation we're adjusting the expression to.
+		 */
+		if (var->varno == context->relid)
+			return (Node *) var;
+
+		key_p = bsearch(&var, context->keys, context->nitems, sizeof(Var *),
+						var_dictionary_comparator);
+
+		/* We shouldn't have omitted any var from the dictionary. */
+		Assert(key_p != NULL);
+
+		index = key_p - context->keys;
+		Assert(index >= 0 && index < context->nitems);
+		value = context->values[index];
+
+		/* All values should be present in the dictionary. */
+		Assert(value != NULL);
+
+		/* Update gv_eval_at accordingly. */
+		bms_del_member(*context->gv_eval_at_p, var->varno);
+		*context->gv_eval_at_p = bms_add_member(*context->gv_eval_at_p,
+												value->varno);
+
+		return (Node *) value;
+	}
+
+	return expression_tree_mutator(node, translate_expression_to_rels_mutator,
+								   (void *) context);
+}
+
+static int
+var_dictionary_comparator(const void *a, const void *b)
+{
+	Var		  **var1_p,
+			  **var2_p;
+	Var		   *var1,
+			   *var2;
+
+	var1_p = (Var **) a;
+	var1 = castNode(Var, *var1_p);
+	var2_p = (Var **) b;
+	var2 = castNode(Var, *var2_p);
+
+	if (var1->varno < var2->varno)
+		return -1;
+	else if (var1->varno > var2->varno)
+		return 1;
+
+	if (var1->varattno < var2->varattno)
+		return -1;
+	else if (var1->varattno > var2->varattno)
+		return 1;
+
+	return 0;
+}
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index f295558f76..bdbeee1a6a 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -32,6 +32,7 @@
 #include "optimizer/predtest.h"
 #include "optimizer/prep.h"
 #include "optimizer/restrictinfo.h"
+#include "optimizer/tlist.h"
 #include "optimizer/var.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
@@ -76,13 +77,13 @@ typedef struct
 	int			indexcol;		/* index column we want to match to */
 } ec_member_matches_arg;
 
-
 static void consider_index_join_clauses(PlannerInfo *root, RelOptInfo *rel,
 							IndexOptInfo *index,
 							IndexClauseSet *rclauseset,
 							IndexClauseSet *jclauseset,
 							IndexClauseSet *eclauseset,
-							List **bitindexpaths);
+							List **bitindexpaths,
+							bool grouped);
 static void consider_index_join_outer_rels(PlannerInfo *root, RelOptInfo *rel,
 							   IndexOptInfo *index,
 							   IndexClauseSet *rclauseset,
@@ -91,7 +92,8 @@ static void consider_index_join_outer_rels(PlannerInfo *root, RelOptInfo *rel,
 							   List **bitindexpaths,
 							   List *indexjoinclauses,
 							   int considered_clauses,
-							   List **considered_relids);
+							   List **considered_relids,
+							   bool grouped);
 static void get_join_index_paths(PlannerInfo *root, RelOptInfo *rel,
 					 IndexOptInfo *index,
 					 IndexClauseSet *rclauseset,
@@ -99,23 +101,28 @@ static void get_join_index_paths(PlannerInfo *root, RelOptInfo *rel,
 					 IndexClauseSet *eclauseset,
 					 List **bitindexpaths,
 					 Relids relids,
-					 List **considered_relids);
+					 List **considered_relids,
+					 bool grouped);
 static bool eclass_already_used(EquivalenceClass *parent_ec, Relids oldrelids,
 					List *indexjoinclauses);
 static bool bms_equal_any(Relids relids, List *relids_list);
 static void get_index_paths(PlannerInfo *root, RelOptInfo *rel,
 				IndexOptInfo *index, IndexClauseSet *clauses,
-				List **bitindexpaths);
+				List **bitindexpaths,
+				bool grouped);
 static List *build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 				  IndexOptInfo *index, IndexClauseSet *clauses,
 				  bool useful_predicate,
 				  ScanTypeControl scantype,
 				  bool *skip_nonnative_saop,
-				  bool *skip_lower_saop);
+				  bool *skip_lower_saop,
+				  bool grouped);
 static List *build_paths_for_OR(PlannerInfo *root, RelOptInfo *rel,
-				   List *clauses, List *other_clauses);
+				   List *clauses, List *other_clauses,
+				   bool grouped);
 static List *generate_bitmap_or_paths(PlannerInfo *root, RelOptInfo *rel,
-						 List *clauses, List *other_clauses);
+						 List *clauses, List *other_clauses,
+						 bool grouped);
 static Path *choose_bitmap_and(PlannerInfo *root, RelOptInfo *rel,
 				  List *paths);
 static int	path_usage_comparator(const void *a, const void *b);
@@ -227,7 +234,7 @@ static Const *string_to_const(const char *str, Oid datatype);
  * as meaning "unparameterized so far as the indexquals are concerned".
  */
 void
-create_index_paths(PlannerInfo *root, RelOptInfo *rel)
+create_index_paths(PlannerInfo *root, RelOptInfo *rel, bool grouped)
 {
 	List	   *indexpaths;
 	List	   *bitindexpaths;
@@ -272,8 +279,8 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel)
 		 * non-parameterized paths.  Plain paths go directly to add_path(),
 		 * bitmap paths are added to bitindexpaths to be handled below.
 		 */
-		get_index_paths(root, rel, index, &rclauseset,
-						&bitindexpaths);
+		get_index_paths(root, rel, index, &rclauseset, &bitindexpaths,
+						grouped);
 
 		/*
 		 * Identify the join clauses that can match the index.  For the moment
@@ -302,15 +309,25 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel)
 										&rclauseset,
 										&jclauseset,
 										&eclauseset,
-										&bitjoinpaths);
+										&bitjoinpaths,
+										grouped);
 	}
 
+
+	/*
+	 * Bitmap paths are currently not aggregated: AggPath does not accept the
+	 * TID bitmap as input, and even if it did, it'd seem weird to aggregate
+	 * the individual paths and then AND them together.
+	 */
+	if (rel->agg_info != NULL)
+		return;
+
 	/*
 	 * Generate BitmapOrPaths for any suitable OR-clauses present in the
 	 * restriction list.  Add these to bitindexpaths.
 	 */
-	indexpaths = generate_bitmap_or_paths(root, rel,
-										  rel->baserestrictinfo, NIL);
+	indexpaths = generate_bitmap_or_paths(root, rel, rel->baserestrictinfo,
+										  NIL, grouped);
 	bitindexpaths = list_concat(bitindexpaths, indexpaths);
 
 	/*
@@ -318,7 +335,8 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel)
 	 * the joinclause list.  Add these to bitjoinpaths.
 	 */
 	indexpaths = generate_bitmap_or_paths(root, rel,
-										  joinorclauses, rel->baserestrictinfo);
+										  joinorclauses, rel->baserestrictinfo,
+										  grouped);
 	bitjoinpaths = list_concat(bitjoinpaths, indexpaths);
 
 	/*
@@ -439,7 +457,8 @@ consider_index_join_clauses(PlannerInfo *root, RelOptInfo *rel,
 							IndexClauseSet *rclauseset,
 							IndexClauseSet *jclauseset,
 							IndexClauseSet *eclauseset,
-							List **bitindexpaths)
+							List **bitindexpaths,
+							bool grouped)
 {
 	int			considered_clauses = 0;
 	List	   *considered_relids = NIL;
@@ -475,7 +494,8 @@ consider_index_join_clauses(PlannerInfo *root, RelOptInfo *rel,
 									   bitindexpaths,
 									   jclauseset->indexclauses[indexcol],
 									   considered_clauses,
-									   &considered_relids);
+									   &considered_relids,
+									   grouped);
 		/* Consider each applicable eclass join clause */
 		considered_clauses += list_length(eclauseset->indexclauses[indexcol]);
 		consider_index_join_outer_rels(root, rel, index,
@@ -483,7 +503,8 @@ consider_index_join_clauses(PlannerInfo *root, RelOptInfo *rel,
 									   bitindexpaths,
 									   eclauseset->indexclauses[indexcol],
 									   considered_clauses,
-									   &considered_relids);
+									   &considered_relids,
+									   grouped);
 	}
 }
 
@@ -508,7 +529,8 @@ consider_index_join_outer_rels(PlannerInfo *root, RelOptInfo *rel,
 							   List **bitindexpaths,
 							   List *indexjoinclauses,
 							   int considered_clauses,
-							   List **considered_relids)
+							   List **considered_relids,
+							   bool grouped)
 {
 	ListCell   *lc;
 
@@ -575,7 +597,8 @@ consider_index_join_outer_rels(PlannerInfo *root, RelOptInfo *rel,
 								 rclauseset, jclauseset, eclauseset,
 								 bitindexpaths,
 								 bms_union(clause_relids, oldrelids),
-								 considered_relids);
+								 considered_relids,
+								 grouped);
 		}
 
 		/* Also try this set of relids by itself */
@@ -583,7 +606,8 @@ consider_index_join_outer_rels(PlannerInfo *root, RelOptInfo *rel,
 							 rclauseset, jclauseset, eclauseset,
 							 bitindexpaths,
 							 clause_relids,
-							 considered_relids);
+							 considered_relids,
+							 grouped);
 	}
 }
 
@@ -608,7 +632,8 @@ get_join_index_paths(PlannerInfo *root, RelOptInfo *rel,
 					 IndexClauseSet *eclauseset,
 					 List **bitindexpaths,
 					 Relids relids,
-					 List **considered_relids)
+					 List **considered_relids,
+					 bool grouped)
 {
 	IndexClauseSet clauseset;
 	int			indexcol;
@@ -665,7 +690,8 @@ get_join_index_paths(PlannerInfo *root, RelOptInfo *rel,
 	Assert(clauseset.nonempty);
 
 	/* Build index path(s) using the collected set of clauses */
-	get_index_paths(root, rel, index, &clauseset, bitindexpaths);
+	get_index_paths(root, rel, index, &clauseset, bitindexpaths,
+					grouped);
 
 	/*
 	 * Remember we considered paths for this set of relids.  We use lcons not
@@ -715,7 +741,6 @@ bms_equal_any(Relids relids, List *relids_list)
 	return false;
 }
 
-
 /*
  * get_index_paths
  *	  Given an index and a set of index clauses for it, construct IndexPaths.
@@ -734,7 +759,7 @@ bms_equal_any(Relids relids, List *relids_list)
 static void
 get_index_paths(PlannerInfo *root, RelOptInfo *rel,
 				IndexOptInfo *index, IndexClauseSet *clauses,
-				List **bitindexpaths)
+				List **bitindexpaths, bool grouped)
 {
 	List	   *indexpaths;
 	bool		skip_nonnative_saop = false;
@@ -746,18 +771,26 @@ get_index_paths(PlannerInfo *root, RelOptInfo *rel,
 	 * clauses only if the index AM supports them natively, and skip any such
 	 * clauses for index columns after the first (so that we produce ordered
 	 * paths if possible).
+	 *
+	 * These paths are good candidates for AGG_SORTED, so pass the output
+	 * lists for this strategy. AGG_HASHED should be applied to paths with no
+	 * pathkeys.
 	 */
 	indexpaths = build_index_paths(root, rel,
 								   index, clauses,
 								   index->predOK,
 								   ST_ANYSCAN,
 								   &skip_nonnative_saop,
-								   &skip_lower_saop);
+								   &skip_lower_saop,
+								   grouped);
 
 	/*
 	 * If we skipped any lower-order ScalarArrayOpExprs on an index with an AM
 	 * that supports them, then try again including those clauses.  This will
 	 * produce paths with more selectivity but no ordering.
+	 *
+	 * As for the grouping paths, only AGG_HASHED is considered due to the
+	 * missing ordering.
 	 */
 	if (skip_lower_saop)
 	{
@@ -767,7 +800,8 @@ get_index_paths(PlannerInfo *root, RelOptInfo *rel,
 												   index->predOK,
 												   ST_ANYSCAN,
 												   &skip_nonnative_saop,
-												   NULL));
+												   NULL,
+												   grouped));
 	}
 
 	/*
@@ -799,6 +833,9 @@ get_index_paths(PlannerInfo *root, RelOptInfo *rel,
 	 * If there were ScalarArrayOpExpr clauses that the index can't handle
 	 * natively, generate bitmap scan paths relying on executor-managed
 	 * ScalarArrayOpExpr.
+	 *
+	 * As for grouping, only AGG_HASHED is possible here. Again, because
+	 * there's no ordering.
 	 */
 	if (skip_nonnative_saop)
 	{
@@ -807,7 +844,8 @@ get_index_paths(PlannerInfo *root, RelOptInfo *rel,
 									   false,
 									   ST_BITMAPSCAN,
 									   NULL,
-									   NULL);
+									   NULL,
+									   grouped);
 		*bitindexpaths = list_concat(*bitindexpaths, indexpaths);
 	}
 }
@@ -845,13 +883,18 @@ get_index_paths(PlannerInfo *root, RelOptInfo *rel,
  * NULL, we do not ignore non-first ScalarArrayOpExpr clauses, but they will
  * result in considering the scan's output to be unordered.
  *
+ * If 'agg_info' is passed, 'agg_sorted' and / or 'agg_hashed' must be passed
+ * too. In that case AGG_SORTED and / or AGG_HASHED aggregation is applied to
+ * the index path (as long as the index path is appropriate) and the resulting
+ * grouped path is stored here.
+ *
  * 'rel' is the index's heap relation
  * 'index' is the index for which we want to generate paths
  * 'clauses' is the collection of indexable clauses (RestrictInfo nodes)
  * 'useful_predicate' indicates whether the index has a useful predicate
  * 'scantype' indicates whether we need plain or bitmap scan support
  * 'skip_nonnative_saop' indicates whether to accept SAOP if index AM doesn't
- * 'skip_lower_saop' indicates whether to accept non-first-column SAOP
+ * 'skip_lower_saop' indicates whether to accept non-first-column SAOP.
  */
 static List *
 build_index_paths(PlannerInfo *root, RelOptInfo *rel,
@@ -859,7 +902,8 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 				  bool useful_predicate,
 				  ScanTypeControl scantype,
 				  bool *skip_nonnative_saop,
-				  bool *skip_lower_saop)
+				  bool *skip_lower_saop,
+				  bool grouped)
 {
 	List	   *result = NIL;
 	IndexPath  *ipath;
@@ -876,6 +920,9 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 	bool		index_is_ordered;
 	bool		index_only_scan;
 	int			indexcol;
+	bool		can_agg_sorted,
+				can_agg_hashed;
+	AggPath    *agg_path;
 
 	/*
 	 * Check that index supports the desired scan type(s)
@@ -1029,7 +1076,12 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 	 * in the current clauses, OR the index ordering is potentially useful for
 	 * later merging or final output ordering, OR the index has a useful
 	 * predicate, OR an index-only scan is possible.
+	 *
+	 * This is where grouped path start to be considered.
 	 */
+	can_agg_sorted = true;
+	can_agg_hashed = true;
+
 	if (index_clauses != NIL || useful_pathkeys != NIL || useful_predicate ||
 		index_only_scan)
 	{
@@ -1046,7 +1098,72 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 								  outer_relids,
 								  loop_count,
 								  false);
-		result = lappend(result, ipath);
+
+		if (!grouped)
+		{
+			make_uniquekeys(root, (Path *) ipath);
+			result = lappend(result, ipath);
+		}
+		else
+		{
+			/*
+			 * Try to create the grouped paths if caller is interested in
+			 * them.
+			 */
+			if (useful_pathkeys != NIL)
+			{
+				agg_path = create_agg_sorted_path(root,
+												  (Path *) ipath,
+												  true,
+												  ipath->path.rows);
+
+				if (agg_path != NULL)
+				{
+					make_uniquekeys(root, (Path *) agg_path);
+					result = lappend(result, agg_path);
+				}
+				else
+				{
+					/*
+					 * If ipath could not be used as a source for AGG_SORTED
+					 * partial aggregation, it probably does not have the
+					 * appropriate pathkeys. Avoid trying to apply AGG_SORTED
+					 * to the next index paths because those will have the
+					 * same pathkeys.
+					 */
+					can_agg_sorted = false;
+				}
+			}
+			else
+				can_agg_sorted = false;
+
+			/*
+			 * Hashed aggregation should not be parameterized: the cost of
+			 * repeated creation of the hashtable (for different parameter
+			 * values) is probably not worth.
+			 */
+			if (outer_relids != NULL)
+			{
+				agg_path = create_agg_hashed_path(root,
+												  (Path *) ipath,
+												  ipath->path.rows);
+
+				if (agg_path != NULL)
+				{
+					make_uniquekeys(root, (Path *) agg_path);
+					result = lappend(result, agg_path);
+				}
+				else
+				{
+					/*
+					 * If ipath could not be used as a source for AGG_HASHED,
+					 * we should not expect any other path of the same index
+					 * to succeed. Avoid wasting the effort next time.
+					 */
+					can_agg_hashed = false;
+				}
+			}
+		}
 
 		/*
 		 * If appropriate, consider parallel index scan.  We don't allow
@@ -1075,7 +1192,46 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 			 * parallel workers, just free it.
 			 */
 			if (ipath->path.parallel_workers > 0)
-				add_partial_path(rel, (Path *) ipath);
+			{
+				if (!grouped)
+					add_partial_path(rel, (Path *) ipath);
+				else
+				{
+					if (useful_pathkeys != NIL && can_agg_sorted)
+					{
+						/*
+						 * No need to check the pathkeys again.
+						 */
+						agg_path = create_agg_sorted_path(root,
+														  (Path *) ipath,
+														  false,
+														  ipath->path.rows);
+
+						/*
+						 * If create_agg_sorted_path succeeded once, it should
+						 * always do.
+						 */
+						Assert(agg_path != NULL);
+
+						add_partial_path(rel, (Path *) agg_path);
+					}
+
+					if (can_agg_hashed && outer_relids == NULL)
+					{
+						agg_path = create_agg_hashed_path(root,
+														  (Path *) ipath,
+														  ipath->path.rows);
+
+						/*
+						 * If create_agg_hashed_path succeeded once, it should
+						 * always do.
+						 */
+						Assert(agg_path != NULL);
+
+						add_partial_path(rel, (Path *) agg_path);
+					}
+				}
+			}
 			else
 				pfree(ipath);
 		}
@@ -1103,7 +1259,38 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 									  outer_relids,
 									  loop_count,
 									  false);
-			result = lappend(result, ipath);
+
+			if (!grouped)
+			{
+				make_uniquekeys(root, (Path *) ipath);
+				result = lappend(result, ipath);
+			}
+			else
+			{
+				/*
+				 * As the input set ordering does not matter to AGG_HASHED,
+				 * only AGG_SORTED makes sense here. (The AGG_HASHED path we'd
+				 * create here should already exist.)
+				 *
+				 * The existing value of can_agg_sorted is not up-to-date for
+				 * the new pathkeys.
+				 */
+				can_agg_sorted = true;
+
+				/* pathkeys are new, so check them. */
+				agg_path = create_agg_sorted_path(root,
+												  (Path *) ipath,
+												  true,
+												  ipath->path.rows);
+
+				if (agg_path != NULL)
+				{
+					make_uniquekeys(root, (Path *) agg_path);
+					result = lappend(result, agg_path);
+				}
+				else
+					can_agg_sorted = false;
+			}
 
 			/* If appropriate, consider parallel index scan */
 			if (index->amcanparallel &&
@@ -1127,7 +1314,26 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 				 * using parallel workers, just free it.
 				 */
 				if (ipath->path.parallel_workers > 0)
-					add_partial_path(rel, (Path *) ipath);
+				{
+					if (!grouped)
+						add_partial_path(rel, (Path *) ipath);
+					else
+					{
+						if (can_agg_sorted)
+						{
+							/*
+							 * The non-partial path above should have been
+							 * created, so no need to check pathkeys.
+							 */
+							agg_path = create_agg_sorted_path(root,
+															  (Path *) ipath,
+															  false,
+															  ipath->path.rows);
+							Assert(agg_path != NULL);
+							add_partial_path(rel, (Path *) agg_path);
+						}
+					}
+				}
 				else
 					pfree(ipath);
 			}
@@ -1162,10 +1368,12 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
  * 'rel' is the relation for which we want to generate index paths
  * 'clauses' is the current list of clauses (RestrictInfo nodes)
  * 'other_clauses' is the list of additional upper-level clauses
+ * 'agg_info' indicates that grouped paths should be added to 'agg_hashed'.
  */
 static List *
 build_paths_for_OR(PlannerInfo *root, RelOptInfo *rel,
-				   List *clauses, List *other_clauses)
+				   List *clauses, List *other_clauses,
+				   bool grouped)
 {
 	List	   *result = NIL;
 	List	   *all_clauses = NIL;	/* not computed till needed */
@@ -1235,14 +1443,16 @@ build_paths_for_OR(PlannerInfo *root, RelOptInfo *rel,
 		match_clauses_to_index(index, other_clauses, &clauseset);
 
 		/*
-		 * Construct paths if possible.
+		 * Construct paths if possible. Forbid partial aggregation even if the
+		 * relation is grouped --- it'll be applied to the bitmap heap path.
 		 */
 		indexpaths = build_index_paths(root, rel,
 									   index, &clauseset,
 									   useful_predicate,
 									   ST_BITMAPSCAN,
 									   NULL,
-									   NULL);
+									   NULL,
+									   grouped);
 		result = list_concat(result, indexpaths);
 	}
 
@@ -1261,7 +1471,8 @@ build_paths_for_OR(PlannerInfo *root, RelOptInfo *rel,
  */
 static List *
 generate_bitmap_or_paths(PlannerInfo *root, RelOptInfo *rel,
-						 List *clauses, List *other_clauses)
+						 List *clauses, List *other_clauses,
+						 bool grouped)
 {
 	List	   *result = NIL;
 	List	   *all_clauses;
@@ -1301,13 +1512,15 @@ generate_bitmap_or_paths(PlannerInfo *root, RelOptInfo *rel,
 
 				indlist = build_paths_for_OR(root, rel,
 											 andargs,
-											 all_clauses);
+											 all_clauses,
+											 grouped);
 
 				/* Recurse in case there are sub-ORs */
 				indlist = list_concat(indlist,
 									  generate_bitmap_or_paths(root, rel,
 															   andargs,
-															   all_clauses));
+															   all_clauses,
+															   grouped));
 			}
 			else
 			{
@@ -1319,7 +1532,8 @@ generate_bitmap_or_paths(PlannerInfo *root, RelOptInfo *rel,
 
 				indlist = build_paths_for_OR(root, rel,
 											 orargs,
-											 all_clauses);
+											 all_clauses,
+											 grouped);
 			}
 
 			/*
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 642f951093..cb45f03fe2 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -51,10 +51,12 @@ static void try_partial_mergejoin_path(PlannerInfo *root,
 						   JoinPathExtraData *extra);
 static void sort_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
-					 JoinType jointype, JoinPathExtraData *extra);
+					 JoinType jointype, JoinPathExtraData *extra,
+					 bool grouped, bool do_aggregate);
 static void match_unsorted_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
-					 JoinType jointype, JoinPathExtraData *extra);
+					 JoinType jointype, JoinPathExtraData *extra,
+					 bool grouped, bool do_aggregate);
 static void consider_parallel_nestloop(PlannerInfo *root,
 						   RelOptInfo *joinrel,
 						   RelOptInfo *outerrel,
@@ -67,10 +69,13 @@ static void consider_parallel_mergejoin(PlannerInfo *root,
 							RelOptInfo *innerrel,
 							JoinType jointype,
 							JoinPathExtraData *extra,
-							Path *inner_cheapest_total);
+							Path *inner_cheapest_total,
+							bool grouped,
+							bool do_aggregate);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
-					 JoinType jointype, JoinPathExtraData *extra);
+					 JoinType jointype, JoinPathExtraData *extra,
+					 bool grouped, bool do_aggregate);
 static List *select_mergejoin_clauses(PlannerInfo *root,
 						 RelOptInfo *joinrel,
 						 RelOptInfo *outerrel,
@@ -87,7 +92,9 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
 						 List *merge_pathkeys,
-						 bool is_partial);
+						 bool is_partial,
+						 bool grouped,
+						 bool do_aggregate);
 
 
 /*
@@ -120,7 +127,9 @@ add_paths_to_joinrel(PlannerInfo *root,
 					 RelOptInfo *innerrel,
 					 JoinType jointype,
 					 SpecialJoinInfo *sjinfo,
-					 List *restrictlist)
+					 List *restrictlist,
+					 bool grouped,
+					 bool do_aggregate)
 {
 	JoinPathExtraData extra;
 	bool		mergejoin_allowed = true;
@@ -267,7 +276,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 */
 	if (mergejoin_allowed)
 		sort_inner_and_outer(root, joinrel, outerrel, innerrel,
-							 jointype, &extra);
+							 jointype, &extra, grouped, do_aggregate);
 
 	/*
 	 * 2. Consider paths where the outer relation need not be explicitly
@@ -278,7 +287,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 */
 	if (mergejoin_allowed)
 		match_unsorted_outer(root, joinrel, outerrel, innerrel,
-							 jointype, &extra);
+							 jointype, &extra, grouped, do_aggregate);
 
 #ifdef NOT_USED
 
@@ -305,7 +314,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 */
 	if (enable_hashjoin || jointype == JOIN_FULL)
 		hash_inner_and_outer(root, joinrel, outerrel, innerrel,
-							 jointype, &extra);
+							 jointype, &extra, grouped, do_aggregate);
 
 	/*
 	 * 5. If inner and outer relations are foreign tables (or joins) belonging
@@ -366,7 +375,9 @@ try_nestloop_path(PlannerInfo *root,
 				  Path *inner_path,
 				  List *pathkeys,
 				  JoinType jointype,
-				  JoinPathExtraData *extra)
+				  JoinPathExtraData *extra,
+				  bool grouped,
+				  bool do_aggregate)
 {
 	Relids		required_outer;
 	JoinCostWorkspace workspace;
@@ -376,6 +387,11 @@ try_nestloop_path(PlannerInfo *root,
 	Relids		outerrelids;
 	Relids		inner_paramrels = PATH_REQ_OUTER(inner_path);
 	Relids		outer_paramrels = PATH_REQ_OUTER(outer_path);
+	bool		success = false;
+	RelOptInfo *joinrel_plain = joinrel;	/* Non-grouped joinrel. */
+
+	if (grouped)
+		joinrel = joinrel->grouped;
 
 	/*
 	 * Paths are parameterized by top-level parents, so run parameterization
@@ -422,10 +438,61 @@ try_nestloop_path(PlannerInfo *root,
 	initial_cost_nestloop(root, &workspace, jointype,
 						  outer_path, inner_path, extra);
 
-	if (add_path_precheck(joinrel,
-						  workspace.startup_cost, workspace.total_cost,
-						  pathkeys, required_outer))
+	/*
+	 * If the join output should be (partially) aggregated, the precheck
+	 * includes the aggregation and is postponed to create_grouped_path().
+	 */
+	if ((!do_aggregate &&
+		 add_path_precheck(joinrel,
+						   workspace.startup_cost, workspace.total_cost,
+						   pathkeys, required_outer)) ||
+		do_aggregate)
 	{
+		PathTarget *target;
+		List	   *uniquekeys = NIL;
+		Path	   *path;
+
+		/*
+		 * If the join output is subject to partial aggregation, the path must
+		 * generate aggregation input.
+		 */
+		if (!do_aggregate)
+		{
+			target = joinrel->reltarget;
+
+			/*
+			 * 1-stage aggregation can only be used if the join produces
+			 * unique grouping keys, so we have to check that.
+			 */
+			if (grouped)
+			{
+				bool		keys_ok;
+
+				/*
+				 * We're not going to produce AggPath, so the grouping keys
+				 * are not guaranteed to be unique across the output set. This
+				 * function returns NULL if no appropriate uniquekeys could be
+				 * generated.
+				 */
+				uniquekeys = make_uniquekeys_for_join(root, outer_path,
+													  inner_path,
+													  target, &keys_ok);
+
+				/*
+				 * Do not create the join path if it would duplicate the
+				 * grouping keys and if the upper paths do not expect those
+				 * duplicities.
+				 */
+				if (!keys_ok)
+					return;
+			}
+		}
+		else
+		{
+			Assert(joinrel->agg_info != NULL);
+			target = joinrel->agg_info->input;
+		}
+
 		/*
 		 * If the inner path is parameterized, it is parameterized by the
 		 * topmost parent of the outer rel, not the outer rel itself.  Fix
@@ -447,21 +514,72 @@ try_nestloop_path(PlannerInfo *root,
 			}
 		}
 
-		add_path(joinrel, (Path *)
-				 create_nestloop_path(root,
-									  joinrel,
-									  jointype,
-									  &workspace,
-									  extra,
-									  outer_path,
-									  inner_path,
-									  extra->restrictlist,
-									  pathkeys,
-									  required_outer));
+		path = (Path *) create_nestloop_path(root,
+											 joinrel_plain,
+											 target,
+											 jointype,
+											 &workspace,
+											 extra,
+											 outer_path,
+											 inner_path,
+											 extra->restrictlist,
+											 pathkeys,
+											 required_outer);
+
+		/*
+		 * TODO joinrel_plain had to be passed above because of row estimates.
+		 * Pass "grouped" in addition so that we do not need the following
+		 * hack?
+		 */
+		path->parent = joinrel;
+
+		/*
+		 * If uniquekeys is NIL, we should not need it. Either because
+		 * grouped==false (obviously no aggregation push-down), or the input
+		 * path(s) do not have unique keys or we're going to apply AggPath to
+		 * the join.
+		 */
+		path->uniquekeys = uniquekeys;
+
+		if (!do_aggregate)
+		{
+			add_path(joinrel, path);
+			success = true;
+		}
+		else
+		{
+			/*
+			 * Try both AGG_HASHED and AGG_SORTED partial aggregation.
+			 *
+			 * AGG_HASHED should not be parameterized because we don't want to
+			 * create the hashtable again for each set of parameters.
+			 */
+			if (required_outer == NULL)
+				success = create_grouped_path(root,
+											  joinrel,
+											  path,
+											  true,
+											  false,
+											  AGG_HASHED);
+
+			/*
+			 * Don't try AGG_SORTED if create_grouped_path() would reject it
+			 * anyway.
+			 */
+			if (pathkeys != NIL)
+				success = success ||
+					create_grouped_path(root,
+										joinrel,
+										path,
+										true,
+										false,
+										AGG_SORTED);
+		}
 	}
-	else
+
+	if (!success)
 	{
-		/* Waste no memory when we reject a path here */
+		/* Waste no memory when we reject path(s) here */
 		bms_free(required_outer);
 	}
 }
@@ -538,6 +656,7 @@ try_partial_nestloop_path(PlannerInfo *root,
 	add_partial_path(joinrel, (Path *)
 					 create_nestloop_path(root,
 										  joinrel,
+										  joinrel->reltarget,
 										  jointype,
 										  &workspace,
 										  extra,
@@ -564,15 +683,22 @@ try_mergejoin_path(PlannerInfo *root,
 				   List *innersortkeys,
 				   JoinType jointype,
 				   JoinPathExtraData *extra,
-				   bool is_partial)
+				   bool is_partial,
+				   bool grouped,
+				   bool do_aggregate)
 {
 	Relids		required_outer;
 	JoinCostWorkspace workspace;
+	bool		success = false;
+	RelOptInfo *joinrel_plain = joinrel;
+
+	if (grouped)
+		joinrel = joinrel->grouped;
 
-	if (is_partial)
+	if (!grouped && is_partial)
 	{
 		try_partial_mergejoin_path(root,
-								   joinrel,
+								   joinrel_plain,
 								   outer_path,
 								   inner_path,
 								   pathkeys,
@@ -617,26 +743,90 @@ try_mergejoin_path(PlannerInfo *root,
 						   outersortkeys, innersortkeys,
 						   extra);
 
-	if (add_path_precheck(joinrel,
-						  workspace.startup_cost, workspace.total_cost,
-						  pathkeys, required_outer))
+	/*
+	 * See comments in try_nestloop_path().
+	 */
+	if ((!do_aggregate &&
+		 add_path_precheck(joinrel,
+						   workspace.startup_cost, workspace.total_cost,
+						   pathkeys, required_outer)) ||
+		do_aggregate)
 	{
-		add_path(joinrel, (Path *)
-				 create_mergejoin_path(root,
-									   joinrel,
-									   jointype,
-									   &workspace,
-									   extra,
-									   outer_path,
-									   inner_path,
-									   extra->restrictlist,
-									   pathkeys,
-									   required_outer,
-									   mergeclauses,
-									   outersortkeys,
-									   innersortkeys));
+		PathTarget *target;
+		List	   *uniquekeys = NIL;
+		Path	   *path;
+
+		if (!do_aggregate)
+		{
+			target = joinrel->reltarget;
+
+			if (grouped)
+			{
+				bool		keys_ok;
+
+				uniquekeys = make_uniquekeys_for_join(root, outer_path,
+													  inner_path,
+													  target,
+													  &keys_ok);
+				if (!keys_ok)
+					return;
+			}
+		}
+		else
+		{
+			Assert(joinrel->agg_info != NULL);
+			target = joinrel->agg_info->input;
+		}
+
+		path = (Path *) create_mergejoin_path(root,
+											  joinrel_plain,
+											  target,
+											  jointype,
+											  &workspace,
+											  extra,
+											  outer_path,
+											  inner_path,
+											  extra->restrictlist,
+											  pathkeys,
+											  required_outer,
+											  mergeclauses,
+											  outersortkeys,
+											  innersortkeys);
+		/* See try_nestloop_path() */
+		path->parent = joinrel;
+
+		/*
+		 * See comments in try_nestloop_path().
+		 */
+		path->uniquekeys = uniquekeys;
+
+		if (!do_aggregate)
+		{
+			add_path(joinrel, path);
+			success = true;
+		}
+		else
+		{
+			if (required_outer == NULL)
+				success = create_grouped_path(root,
+											  joinrel,
+											  path,
+											  true,
+											  false,
+											  AGG_HASHED);
+
+			if (pathkeys != NIL)
+				success = success ||
+					create_grouped_path(root,
+										joinrel,
+										path,
+										true,
+										false,
+										AGG_SORTED);
+		}
 	}
-	else
+
+	if (!success)
 	{
 		/* Waste no memory when we reject a path here */
 		bms_free(required_outer);
@@ -700,6 +890,7 @@ try_partial_mergejoin_path(PlannerInfo *root,
 	add_partial_path(joinrel, (Path *)
 					 create_mergejoin_path(root,
 										   joinrel,
+										   joinrel->reltarget,
 										   jointype,
 										   &workspace,
 										   extra,
@@ -725,10 +916,17 @@ try_hashjoin_path(PlannerInfo *root,
 				  Path *inner_path,
 				  List *hashclauses,
 				  JoinType jointype,
-				  JoinPathExtraData *extra)
+				  JoinPathExtraData *extra,
+				  bool grouped,
+				  bool do_aggregate)
 {
 	Relids		required_outer;
 	JoinCostWorkspace workspace;
+	bool		success = false;
+	RelOptInfo *joinrel_plain = joinrel;	/* Non-grouped joinrel. */
+
+	if (grouped)
+		joinrel = joinrel->grouped;
 
 	/*
 	 * Check to see if proposed path is still parameterized, and reject if the
@@ -745,30 +943,98 @@ try_hashjoin_path(PlannerInfo *root,
 	}
 
 	/*
+	 * Parameterized execution of grouped path would mean repeated hashing of
+	 * the output of the hashjoin output, so forget about AGG_HASHED if there
+	 * are any parameters. And AGG_SORTED makes no sense because the hash join
+	 * output is not sorted.
+	 */
+	if (required_outer && joinrel->agg_info)
+		return;
+
+	/*
 	 * See comments in try_nestloop_path().  Also note that hashjoin paths
 	 * never have any output pathkeys, per comments in create_hashjoin_path.
 	 */
 	initial_cost_hashjoin(root, &workspace, jointype, hashclauses,
 						  outer_path, inner_path, extra, false);
 
-	if (add_path_precheck(joinrel,
-						  workspace.startup_cost, workspace.total_cost,
-						  NIL, required_outer))
+
+	/*
+	 * See comments try_nestloop_path().
+	 */
+	if ((!do_aggregate &&
+		 add_path_precheck(joinrel,
+						   workspace.startup_cost, workspace.total_cost,
+						   NIL, required_outer)) ||
+		do_aggregate)
 	{
-		add_path(joinrel, (Path *)
-				 create_hashjoin_path(root,
-									  joinrel,
-									  jointype,
-									  &workspace,
-									  extra,
-									  outer_path,
-									  inner_path,
-									  false,	/* parallel_hash */
-									  extra->restrictlist,
-									  required_outer,
-									  hashclauses));
+		PathTarget *target;
+		List	   *uniquekeys = NIL;
+		Path	   *path = NULL;
+
+		if (!do_aggregate)
+		{
+			target = joinrel->reltarget;
+
+			if (grouped)
+			{
+				bool		keys_ok;
+
+				uniquekeys = make_uniquekeys_for_join(root, outer_path,
+													  inner_path,
+													  target,
+													  &keys_ok);
+				if (!keys_ok)
+					return;
+			}
+		}
+		else
+		{
+			Assert(joinrel->agg_info != NULL);
+			target = joinrel->agg_info->input;
+		}
+
+		path = (Path *) create_hashjoin_path(root,
+											 joinrel_plain,
+											 target,
+											 jointype,
+											 &workspace,
+											 extra,
+											 outer_path,
+											 inner_path,
+											 false, /* parallel_hash */
+											 extra->restrictlist,
+											 required_outer,
+											 hashclauses);
+		/* See try_nestloop_path() */
+		path->parent = joinrel;
+
+		/*
+		 * See comments in try_nestloop_path().
+		 */
+		path->uniquekeys = uniquekeys;
+
+		if (!do_aggregate)
+		{
+			add_path(joinrel, path);
+			success = true;
+		}
+		else
+		{
+			/*
+			 * As the hashjoin path is not sorted, only try AGG_HASHED.
+			 */
+			if (create_grouped_path(root,
+									joinrel,
+									path,
+									true,
+									false,
+									AGG_HASHED))
+				success = true;
+		}
 	}
-	else
+
+	if (!success)
 	{
 		/* Waste no memory when we reject a path here */
 		bms_free(required_outer);
@@ -824,6 +1090,7 @@ try_partial_hashjoin_path(PlannerInfo *root,
 	add_partial_path(joinrel, (Path *)
 					 create_hashjoin_path(root,
 										  joinrel,
+										  joinrel->reltarget,
 										  jointype,
 										  &workspace,
 										  extra,
@@ -876,6 +1143,7 @@ clause_sides_match_join(RestrictInfo *rinfo, RelOptInfo *outerrel,
  * 'innerrel' is the inner join relation
  * 'jointype' is the type of join to do
  * 'extra' contains additional input values
+ * 'agg_info' tells if/how to apply partial aggregation to the output.
  */
 static void
 sort_inner_and_outer(PlannerInfo *root,
@@ -883,7 +1151,9 @@ sort_inner_and_outer(PlannerInfo *root,
 					 RelOptInfo *outerrel,
 					 RelOptInfo *innerrel,
 					 JoinType jointype,
-					 JoinPathExtraData *extra)
+					 JoinPathExtraData *extra,
+					 bool grouped,
+					 bool do_aggregate)
 {
 	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
@@ -1045,13 +1315,15 @@ sort_inner_and_outer(PlannerInfo *root,
 						   innerkeys,
 						   jointype,
 						   extra,
-						   false);
+						   false,
+						   grouped,
+						   do_aggregate);
 
 		/*
 		 * If we have partial outer and parallel safe inner path then try
 		 * partial mergejoin path.
 		 */
-		if (cheapest_partial_outer && cheapest_safe_inner)
+		if (!grouped && cheapest_partial_outer && cheapest_safe_inner)
 			try_partial_mergejoin_path(root,
 									   joinrel,
 									   cheapest_partial_outer,
@@ -1089,7 +1361,9 @@ generate_mergejoin_paths(PlannerInfo *root,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
 						 List *merge_pathkeys,
-						 bool is_partial)
+						 bool is_partial,
+						 bool grouped,
+						 bool do_aggregate)
 {
 	List	   *mergeclauses;
 	List	   *innersortkeys;
@@ -1150,7 +1424,9 @@ generate_mergejoin_paths(PlannerInfo *root,
 					   innersortkeys,
 					   jointype,
 					   extra,
-					   is_partial);
+					   is_partial,
+					   grouped,
+					   do_aggregate);
 
 	/* Can't do anything else if inner path needs to be unique'd */
 	if (save_jointype == JOIN_UNIQUE_INNER)
@@ -1247,7 +1523,9 @@ generate_mergejoin_paths(PlannerInfo *root,
 							   NIL,
 							   jointype,
 							   extra,
-							   is_partial);
+							   is_partial,
+							   grouped,
+							   do_aggregate);
 			cheapest_total_inner = innerpath;
 		}
 		/* Same on the basis of cheapest startup cost ... */
@@ -1291,7 +1569,9 @@ generate_mergejoin_paths(PlannerInfo *root,
 								   NIL,
 								   jointype,
 								   extra,
-								   is_partial);
+								   is_partial,
+								   grouped,
+								   do_aggregate);
 			}
 			cheapest_startup_inner = innerpath;
 		}
@@ -1333,7 +1613,9 @@ match_unsorted_outer(PlannerInfo *root,
 					 RelOptInfo *outerrel,
 					 RelOptInfo *innerrel,
 					 JoinType jointype,
-					 JoinPathExtraData *extra)
+					 JoinPathExtraData *extra,
+					 bool grouped,
+					 bool do_aggregate)
 {
 	JoinType	save_jointype = jointype;
 	bool		nestjoinOK;
@@ -1456,7 +1738,9 @@ match_unsorted_outer(PlannerInfo *root,
 							  inner_cheapest_total,
 							  merge_pathkeys,
 							  jointype,
-							  extra);
+							  extra,
+							  grouped,
+							  do_aggregate);
 		}
 		else if (nestjoinOK)
 		{
@@ -1478,7 +1762,9 @@ match_unsorted_outer(PlannerInfo *root,
 								  innerpath,
 								  merge_pathkeys,
 								  jointype,
-								  extra);
+								  extra,
+								  grouped,
+								  do_aggregate);
 			}
 
 			/* Also consider materialized form of the cheapest inner path */
@@ -1489,7 +1775,9 @@ match_unsorted_outer(PlannerInfo *root,
 								  matpath,
 								  merge_pathkeys,
 								  jointype,
-								  extra);
+								  extra,
+								  grouped,
+								  do_aggregate);
 		}
 
 		/* Can't do anything else if outer path needs to be unique'd */
@@ -1504,7 +1792,7 @@ match_unsorted_outer(PlannerInfo *root,
 		generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
 								 save_jointype, extra, useallclauses,
 								 inner_cheapest_total, merge_pathkeys,
-								 false);
+								 false, grouped, do_aggregate);
 	}
 
 	/*
@@ -1516,7 +1804,8 @@ match_unsorted_outer(PlannerInfo *root,
 	 * parameterized. Similarly, we can't handle JOIN_FULL and JOIN_RIGHT,
 	 * because they can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel &&
+	if (!grouped &&
+		joinrel->consider_parallel &&
 		save_jointype != JOIN_UNIQUE_OUTER &&
 		save_jointype != JOIN_FULL &&
 		save_jointype != JOIN_RIGHT &&
@@ -1545,7 +1834,9 @@ match_unsorted_outer(PlannerInfo *root,
 		if (inner_cheapest_total)
 			consider_parallel_mergejoin(root, joinrel, outerrel, innerrel,
 										save_jointype, extra,
-										inner_cheapest_total);
+										inner_cheapest_total,
+										grouped,
+										do_aggregate);
 	}
 }
 
@@ -1568,7 +1859,9 @@ consider_parallel_mergejoin(PlannerInfo *root,
 							RelOptInfo *innerrel,
 							JoinType jointype,
 							JoinPathExtraData *extra,
-							Path *inner_cheapest_total)
+							Path *inner_cheapest_total,
+							bool grouped,
+							bool do_aggregate)
 {
 	ListCell   *lc1;
 
@@ -1586,7 +1879,8 @@ consider_parallel_mergejoin(PlannerInfo *root,
 
 		generate_mergejoin_paths(root, joinrel, innerrel, outerpath, jointype,
 								 extra, false, inner_cheapest_total,
-								 merge_pathkeys, true);
+								 merge_pathkeys, true, grouped,
+								 do_aggregate);
 	}
 }
 
@@ -1679,7 +1973,9 @@ hash_inner_and_outer(PlannerInfo *root,
 					 RelOptInfo *outerrel,
 					 RelOptInfo *innerrel,
 					 JoinType jointype,
-					 JoinPathExtraData *extra)
+					 JoinPathExtraData *extra,
+					 bool grouped,
+					 bool do_aggregate)
 {
 	JoinType	save_jointype = jointype;
 	bool		isouterjoin = IS_OUTER_JOIN(jointype);
@@ -1754,7 +2050,9 @@ hash_inner_and_outer(PlannerInfo *root,
 							  cheapest_total_inner,
 							  hashclauses,
 							  jointype,
-							  extra);
+							  extra,
+							  grouped,
+							  do_aggregate);
 			/* no possibility of cheap startup here */
 		}
 		else if (jointype == JOIN_UNIQUE_INNER)
@@ -1770,7 +2068,9 @@ hash_inner_and_outer(PlannerInfo *root,
 							  cheapest_total_inner,
 							  hashclauses,
 							  jointype,
-							  extra);
+							  extra,
+							  grouped,
+							  do_aggregate);
 			if (cheapest_startup_outer != NULL &&
 				cheapest_startup_outer != cheapest_total_outer)
 				try_hashjoin_path(root,
@@ -1779,7 +2079,9 @@ hash_inner_and_outer(PlannerInfo *root,
 								  cheapest_total_inner,
 								  hashclauses,
 								  jointype,
-								  extra);
+								  extra,
+								  grouped,
+								  do_aggregate);
 		}
 		else
 		{
@@ -1800,7 +2102,9 @@ hash_inner_and_outer(PlannerInfo *root,
 								  cheapest_total_inner,
 								  hashclauses,
 								  jointype,
-								  extra);
+								  extra,
+								  grouped,
+								  do_aggregate);
 
 			foreach(lc1, outerrel->cheapest_parameterized_paths)
 			{
@@ -1834,7 +2138,9 @@ hash_inner_and_outer(PlannerInfo *root,
 									  innerpath,
 									  hashclauses,
 									  jointype,
-									  extra);
+									  extra,
+									  grouped,
+									  do_aggregate);
 				}
 			}
 		}
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 7008e1318e..3370a217f3 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -16,13 +16,16 @@
 
 #include "miscadmin.h"
 #include "optimizer/clauses.h"
+#include "optimizer/cost.h"
 #include "optimizer/joininfo.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 #include "optimizer/prep.h"
+#include "optimizer/tlist.h"
 #include "partitioning/partbounds.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/selfuncs.h"
 
 
 static void make_rels_by_clause_joins(PlannerInfo *root,
@@ -31,23 +34,35 @@ static void make_rels_by_clause_joins(PlannerInfo *root,
 static void make_rels_by_clauseless_joins(PlannerInfo *root,
 							  RelOptInfo *old_rel,
 							  ListCell *other_rels);
+static void set_grouped_joinrel_target(PlannerInfo *root, RelOptInfo *joinrel,
+						   RelOptInfo *rel1, RelOptInfo *rel2,
+						   SpecialJoinInfo *sjinfo, List *restrictlist,
+						   RelAggInfo *agg_info);
 static bool has_join_restriction(PlannerInfo *root, RelOptInfo *rel);
 static bool has_legal_joinclause(PlannerInfo *root, RelOptInfo *rel);
 static bool is_dummy_rel(RelOptInfo *rel);
 static bool restriction_is_constant_false(List *restrictlist,
 							  RelOptInfo *joinrel,
 							  bool only_pushed_down);
+static RelOptInfo *make_join_rel_common(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
+					 RelAggInfo *agg_info, bool grouped,
+					 bool do_aggregate);
+static void make_join_rel_common_grouped(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
+							 RelAggInfo *agg_info, bool do_aggregate);
 static void populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 							RelOptInfo *rel2, RelOptInfo *joinrel,
-							SpecialJoinInfo *sjinfo, List *restrictlist);
-static void try_partitionwise_join(PlannerInfo *root, RelOptInfo *rel1,
-					   RelOptInfo *rel2, RelOptInfo *joinrel,
-					   SpecialJoinInfo *parent_sjinfo,
-					   List *parent_restrictlist);
+							SpecialJoinInfo *sjinfo, List *restrictlist,
+							bool grouped,
+							bool do_aggregate);
+static void try_partition_wise_join(PlannerInfo *root, RelOptInfo *rel1,
+						RelOptInfo *rel2, RelOptInfo *joinrel,
+						SpecialJoinInfo *parent_sjinfo,
+						List *parent_restrictlist,
+						bool grouped,
+						bool do_aggregate);
 static int match_expr_to_partition_keys(Expr *expr, RelOptInfo *rel,
 							 bool strict_op);
 
-
 /*
  * join_search_one_level
  *	  Consider ways to produce join relations containing exactly 'level'
@@ -322,6 +337,58 @@ make_rels_by_clauseless_joins(PlannerInfo *root,
 	}
 }
 
+/*
+ * Set joinrel's reltarget according to agg_info and estimate the number of
+ * rows.
+ */
+static void
+set_grouped_joinrel_target(PlannerInfo *root, RelOptInfo *joinrel,
+						   RelOptInfo *rel1, RelOptInfo *rel2,
+						   SpecialJoinInfo *sjinfo, List *restrictlist,
+						   RelAggInfo *agg_info)
+{
+	PathTarget *target = NULL;
+
+	Assert(agg_info != NULL);
+
+	/*
+	 * build_join_rel() / build_child_join_rel() does not create the target
+	 * for grouped relation.
+	 */
+	Assert(joinrel->reltarget == NULL);
+	Assert(joinrel->agg_info == NULL);
+
+	target = agg_info->target;
+
+	/*
+	 * The output will actually be grouped, i.e. partially aggregated. No
+	 * additional processing needed.
+	 */
+	joinrel->reltarget = copy_pathtarget(target);
+
+	/*
+	 * The rest of agg_info will be needed at aggregation time.
+	 */
+	joinrel->agg_info = agg_info;
+
+	/*
+	 * Now that we have the target, compute the estimates.
+	 */
+	set_joinrel_size_estimates(root, joinrel, rel1, rel2, sjinfo,
+							   restrictlist);
+
+	/*
+	 * Grouping essentially changes the number of rows.
+	 *
+	 * XXX We do not distinguish whether two plain rels are joined and the
+	 * result is partially aggregated, or the partial aggregation has been
+	 * already applied to one of the input rels. Is this worth extra effort,
+	 * e.g. maintaining a separate RelOptInfo for each case (one difficulty
+	 * that would introduce is construction of AppendPath)?
+	 */
+	joinrel->rows = estimate_num_groups(root, joinrel->agg_info->group_exprs,
+										joinrel->rows, NULL);
+}
 
 /*
  * join_is_legal
@@ -651,32 +718,45 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 	return true;
 }
 
-
 /*
- * make_join_rel
+ * make_join_rel_common
  *	   Find or create a join RelOptInfo that represents the join of
  *	   the two given rels, and add to it path information for paths
  *	   created with the two rels as outer and inner rel.
  *	   (The join rel may already contain paths generated from other
  *	   pairs of rels that add up to the same set of base rels.)
  *
- * NB: will return NULL if attempted join is not valid.  This can happen
- * when working with outer joins, or with IN or EXISTS clauses that have been
- * turned into joins.
+ *	   'agg_info' contains the reltarget of grouped relation and everything we
+ *	   need to aggregate the join result. If NULL, then the join relation
+ *	   should not be grouped.
+ *
+ *	   'do_aggregate' tells that two non-grouped rels should be grouped and
+ *	   partial aggregation should be applied to all their paths.
+ *
+ * NB: will return NULL if attempted join is not valid.  This can happen when
+ * working with outer joins, or with IN or EXISTS clauses that have been
+ * turned into joins. NULL is also returned if caller is interested in a
+ * grouped relation but there's no useful grouped input relation.
  */
-RelOptInfo *
-make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
+static RelOptInfo *
+make_join_rel_common(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
+					 RelAggInfo *agg_info, bool grouped,
+					 bool do_aggregate)
 {
 	Relids		joinrelids;
 	SpecialJoinInfo *sjinfo;
 	bool		reversed;
 	SpecialJoinInfo sjinfo_data;
-	RelOptInfo *joinrel;
+	RelOptInfo *joinrel,
+			   *joinrel_plain;
 	List	   *restrictlist;
 
 	/* We should never try to join two overlapping sets of rels. */
 	Assert(!bms_overlap(rel1->relids, rel2->relids));
 
+	/* do_aggregate implies the output to be grouped. */
+	Assert(!do_aggregate || grouped);
+
 	/* Construct Relids set that identifies the joinrel. */
 	joinrelids = bms_union(rel1->relids, rel2->relids);
 
@@ -725,8 +805,38 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 	 * Find or build the join RelOptInfo, and compute the restrictlist that
 	 * goes with this particular joining.
 	 */
-	joinrel = build_join_rel(root, joinrelids, rel1, rel2, sjinfo,
-							 &restrictlist);
+	joinrel = joinrel_plain = build_join_rel(root, joinrelids, rel1, rel2, sjinfo,
+											 &restrictlist, false);
+
+	if (grouped)
+	{
+		/*
+		 * Make sure there's a grouped join relation.
+		 */
+		if (joinrel->grouped == NULL)
+			joinrel->grouped = build_join_rel(root,
+											  joinrelids,
+											  rel1,
+											  rel2,
+											  sjinfo,
+											  &restrictlist,
+											  true);
+
+		/*
+		 * The grouped join is what we need to return.
+		 */
+		joinrel = joinrel->grouped;
+
+
+		/*
+		 * Make sure the grouped joinrel has reltarget initialized. Caller
+		 * should supply the target for group relation, so build_join_rel()
+		 * should have omitted its creation.
+		 */
+		if (joinrel->reltarget == NULL)
+			set_grouped_joinrel_target(root, joinrel, rel1, rel2, sjinfo,
+									   restrictlist, agg_info);
+	}
 
 	/*
 	 * If we've already proven this join is empty, we needn't consider any
@@ -738,15 +848,182 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 		return joinrel;
 	}
 
-	/* Add paths to the join relation. */
-	populate_joinrel_with_paths(root, rel1, rel2, joinrel, sjinfo,
-								restrictlist);
+	/*
+	 * Add paths to the join relation.
+	 *
+	 * Pass joinrel_plain and agg_kind instead of joinrel, since the function
+	 * needs agg_kind anyway.
+	 */
+	populate_joinrel_with_paths(root, rel1, rel2, joinrel_plain, sjinfo,
+								restrictlist, grouped, do_aggregate);
 
 	bms_free(joinrelids);
 
 	return joinrel;
 }
 
+static void
+make_join_rel_common_grouped(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
+							 RelAggInfo *agg_info, bool do_aggregate)
+{
+	RelOptInfo *rel1_grouped = NULL;
+	RelOptInfo *rel2_grouped = NULL;
+	bool		rel1_grouped_useful = false;
+	bool		rel2_grouped_useful = false;
+
+	/*
+	 * Retrieve the grouped relations.
+	 *
+	 * Dummy rel indicates join relation able to generate grouped paths as
+	 * such (i.e. it has valid agg_info), but for which the path actually
+	 * could not be created (e.g. only AGG_HASHED strategy was possible but
+	 * work_mem was not sufficient for hash table).
+	 */
+	if (rel1->grouped)
+		rel1_grouped = rel1->grouped;
+	if (rel2->grouped)
+		rel2_grouped = rel2->grouped;
+
+	rel1_grouped_useful = rel1_grouped != NULL && !IS_DUMMY_REL(rel1_grouped);
+	rel2_grouped_useful = rel2_grouped != NULL && !IS_DUMMY_REL(rel2_grouped);
+
+	/*
+	 * Nothing else to do?
+	 */
+	if (!rel1_grouped_useful && !rel2_grouped_useful)
+		return;
+
+	/*
+	 * At maximum one input rel can be grouped (here we don't care if any rel
+	 * is eventually dummy, the existence of grouped rel indicates that
+	 * aggregates can be pushed down to it). If both were grouped, then
+	 * grouping of one side would change the occurrence of the other side's
+	 * aggregate transient states on the input of the final aggregation. This
+	 * can be handled by adjusting the transient states, but it's not worth
+	 * the effort because it's hard to find a use case for this kind of join.
+	 *
+	 * XXX If the join of two grouped rels is implemented someday, note that
+	 * both rels can have aggregates, so it'd be hard to join grouped rel to
+	 * non-grouped here: 1) such a "mixed join" would require a special
+	 * target, 2) both AGGSPLIT_FINAL_DESERIAL and AGGSPLIT_SIMPLE aggregates
+	 * could appear in the target of the final aggregation node, originating
+	 * from the grouped and the non-grouped input rel respectively.
+	 */
+	if (rel1_grouped && rel2_grouped)
+		return;
+
+	if (rel1_grouped_useful)
+	{
+		if (rel1_grouped->agg_info->target)
+			make_join_rel_common(root, rel1_grouped, rel2, agg_info, true,
+								 do_aggregate);
+	}
+	else if (rel2_grouped_useful)
+	{
+		if (rel2_grouped->agg_info->target)
+			make_join_rel_common(root, rel1, rel2_grouped, agg_info, true,
+								 do_aggregate);
+	}
+}
+
+/*
+ * Front-end to make_join_rel_common(). Generates plain (non-grouped) join and
+ * then uses all the possible strategies to generate the grouped one.
+ */
+RelOptInfo *
+make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
+{
+	Relids		joinrelids;
+	RelAggInfo *agg_info;
+	RelOptInfo *joinrel;
+	double		nrows_plain;
+	RelOptInfo *result;
+
+	/* 1) form the plain join. */
+	result = make_join_rel_common(root, rel1, rel2, NULL, false,
+								  false);
+
+	if (result == NULL)
+		return result;
+
+	nrows_plain = result->rows;
+
+	/*
+	 * We're done if there are no grouping expressions nor aggregates.
+	 */
+	if (root->grouped_var_list == NIL)
+		return result;
+
+	/*
+	 * If the same joinrel was already formed, just with the base rels divided
+	 * between rel1 and rel2 in a different way, we might already have the
+	 * matching agg_info.
+	 */
+	joinrelids = bms_union(rel1->relids, rel2->relids);
+	joinrel = find_join_rel(root, joinrelids);
+
+	/*
+	 * At the moment we know that non-grouped join exists, so it should have
+	 * been fetched.
+	 */
+	Assert(joinrel != NULL);
+
+	if (joinrel->grouped != NULL)
+	{
+		Assert(joinrel->grouped->agg_info != NULL);
+
+		agg_info = joinrel->grouped->agg_info;
+	}
+	else
+	{
+		double		nrows;
+
+		/*
+		 * agg_info must be created from scratch.
+		 */
+		agg_info = create_rel_agg_info(root, result);
+
+		/*
+		 * Grouping essentially changes the number of rows.
+		 */
+		if (agg_info != NULL)
+		{
+			nrows = estimate_num_groups(root,
+										agg_info->group_exprs,
+										nrows_plain,
+										NULL);
+			agg_info->rows = clamp_row_est(nrows);
+		}
+	}
+
+	/*
+	 * Cannot we build grouped join?
+	 */
+	if (agg_info == NULL)
+		return result;
+
+	/*
+	 * 2) join two plain rels and aggregate the join paths.
+	 */
+	result->grouped = make_join_rel_common(root, rel1, rel2,
+										   agg_info,
+										   true,
+										   true);
+
+	/*
+	 * If the non-grouped join relation could be built, its aggregated form
+	 * should exist too.
+	 */
+	Assert(result->grouped != NULL);
+
+	/*
+	 * 3) combine plain and grouped relations.
+	 */
+	make_join_rel_common_grouped(root, rel1, rel2, agg_info, false);
+
+	return result;
+}
+
 /*
  * populate_joinrel_with_paths
  *	  Add paths to the given joinrel for given pair of joining relations. The
@@ -757,8 +1034,24 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2)
 static void
 populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 							RelOptInfo *rel2, RelOptInfo *joinrel,
-							SpecialJoinInfo *sjinfo, List *restrictlist)
+							SpecialJoinInfo *sjinfo, List *restrictlist,
+							bool grouped, bool do_aggregate)
 {
+	RelOptInfo *joinrel_plain;
+
+	/*
+	 * joinrel_plain and agg_kind is passed to add_paths_to_joinrel() since it
+	 * needs agg_kind anyway.
+	 *
+	 * TODO As for the other uses, find out where joinrel can be used safely
+	 * instead of joinrel_plain, i.e. check that even grouped joinrel has all
+	 * the information needed.
+	 */
+	joinrel_plain = joinrel;
+
+	if (grouped)
+		joinrel = joinrel->grouped;
+
 	/*
 	 * Consider paths using each rel as both outer and inner.  Depending on
 	 * the join type, a provably empty outer or inner rel might mean the join
@@ -781,17 +1074,17 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 	{
 		case JOIN_INNER:
 			if (is_dummy_rel(rel1) || is_dummy_rel(rel2) ||
-				restriction_is_constant_false(restrictlist, joinrel, false))
+				restriction_is_constant_false(restrictlist, joinrel_plain, false))
 			{
 				mark_dummy_rel(joinrel);
 				break;
 			}
-			add_paths_to_joinrel(root, joinrel, rel1, rel2,
+			add_paths_to_joinrel(root, joinrel_plain, rel1, rel2,
 								 JOIN_INNER, sjinfo,
-								 restrictlist);
-			add_paths_to_joinrel(root, joinrel, rel2, rel1,
+								 restrictlist, grouped, do_aggregate);
+			add_paths_to_joinrel(root, joinrel_plain, rel2, rel1,
 								 JOIN_INNER, sjinfo,
-								 restrictlist);
+								 restrictlist, grouped, do_aggregate);
 			break;
 		case JOIN_LEFT:
 			if (is_dummy_rel(rel1) ||
@@ -800,29 +1093,29 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 				mark_dummy_rel(joinrel);
 				break;
 			}
-			if (restriction_is_constant_false(restrictlist, joinrel, false) &&
+			if (restriction_is_constant_false(restrictlist, joinrel_plain, false) &&
 				bms_is_subset(rel2->relids, sjinfo->syn_righthand))
 				mark_dummy_rel(rel2);
-			add_paths_to_joinrel(root, joinrel, rel1, rel2,
+			add_paths_to_joinrel(root, joinrel_plain, rel1, rel2,
 								 JOIN_LEFT, sjinfo,
-								 restrictlist);
-			add_paths_to_joinrel(root, joinrel, rel2, rel1,
+								 restrictlist, grouped, do_aggregate);
+			add_paths_to_joinrel(root, joinrel_plain, rel2, rel1,
 								 JOIN_RIGHT, sjinfo,
-								 restrictlist);
+								 restrictlist, grouped, do_aggregate);
 			break;
 		case JOIN_FULL:
 			if ((is_dummy_rel(rel1) && is_dummy_rel(rel2)) ||
-				restriction_is_constant_false(restrictlist, joinrel, true))
+				restriction_is_constant_false(restrictlist, joinrel_plain, true))
 			{
 				mark_dummy_rel(joinrel);
 				break;
 			}
-			add_paths_to_joinrel(root, joinrel, rel1, rel2,
+			add_paths_to_joinrel(root, joinrel_plain, rel1, rel2,
 								 JOIN_FULL, sjinfo,
-								 restrictlist);
-			add_paths_to_joinrel(root, joinrel, rel2, rel1,
+								 restrictlist, grouped, do_aggregate);
+			add_paths_to_joinrel(root, joinrel_plain, rel2, rel1,
 								 JOIN_FULL, sjinfo,
-								 restrictlist);
+								 restrictlist, grouped, do_aggregate);
 
 			/*
 			 * If there are join quals that aren't mergeable or hashable, we
@@ -848,14 +1141,14 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 				bms_is_subset(sjinfo->min_righthand, rel2->relids))
 			{
 				if (is_dummy_rel(rel1) || is_dummy_rel(rel2) ||
-					restriction_is_constant_false(restrictlist, joinrel, false))
+					restriction_is_constant_false(restrictlist, joinrel_plain, false))
 				{
 					mark_dummy_rel(joinrel);
 					break;
 				}
-				add_paths_to_joinrel(root, joinrel, rel1, rel2,
+				add_paths_to_joinrel(root, joinrel_plain, rel1, rel2,
 									 JOIN_SEMI, sjinfo,
-									 restrictlist);
+									 restrictlist, grouped, do_aggregate);
 			}
 
 			/*
@@ -871,32 +1164,32 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 								   sjinfo) != NULL)
 			{
 				if (is_dummy_rel(rel1) || is_dummy_rel(rel2) ||
-					restriction_is_constant_false(restrictlist, joinrel, false))
+					restriction_is_constant_false(restrictlist, joinrel_plain, false))
 				{
 					mark_dummy_rel(joinrel);
 					break;
 				}
-				add_paths_to_joinrel(root, joinrel, rel1, rel2,
+				add_paths_to_joinrel(root, joinrel_plain, rel1, rel2,
 									 JOIN_UNIQUE_INNER, sjinfo,
-									 restrictlist);
-				add_paths_to_joinrel(root, joinrel, rel2, rel1,
+									 restrictlist, grouped, do_aggregate);
+				add_paths_to_joinrel(root, joinrel_plain, rel2, rel1,
 									 JOIN_UNIQUE_OUTER, sjinfo,
-									 restrictlist);
+									 restrictlist, grouped, do_aggregate);
 			}
 			break;
 		case JOIN_ANTI:
 			if (is_dummy_rel(rel1) ||
-				restriction_is_constant_false(restrictlist, joinrel, true))
+				restriction_is_constant_false(restrictlist, joinrel_plain, true))
 			{
 				mark_dummy_rel(joinrel);
 				break;
 			}
-			if (restriction_is_constant_false(restrictlist, joinrel, false) &&
+			if (restriction_is_constant_false(restrictlist, joinrel_plain, false) &&
 				bms_is_subset(rel2->relids, sjinfo->syn_righthand))
 				mark_dummy_rel(rel2);
-			add_paths_to_joinrel(root, joinrel, rel1, rel2,
+			add_paths_to_joinrel(root, joinrel_plain, rel1, rel2,
 								 JOIN_ANTI, sjinfo,
-								 restrictlist);
+								 restrictlist, grouped, do_aggregate);
 			break;
 		default:
 			/* other values not expected here */
@@ -904,8 +1197,16 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 			break;
 	}
 
-	/* Apply partitionwise join technique, if possible. */
-	try_partitionwise_join(root, rel1, rel2, joinrel, sjinfo, restrictlist);
+	/*
+	 * TODO Only allow per-child AGGSPLIT_SIMPLE if the partitioning allows
+	 * it, i.e. each partition generates distinct set of grouping keys.
+	 */
+	if (grouped)
+		return;
+
+	/* Apply partition-wise join technique, if possible. */
+	try_partition_wise_join(root, rel1, rel2, joinrel_plain, sjinfo, restrictlist,
+							grouped, do_aggregate);
 }
 
 
@@ -1308,16 +1609,16 @@ restriction_is_constant_false(List *restrictlist,
  * obtained by translating the respective parent join structures.
  */
 static void
-try_partitionwise_join(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
-					   RelOptInfo *joinrel, SpecialJoinInfo *parent_sjinfo,
-					   List *parent_restrictlist)
+try_partition_wise_join(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
+						RelOptInfo *joinrel, SpecialJoinInfo *parent_sjinfo,
+						List *parent_restrictlist, bool grouped,
+						bool do_aggregate)
 {
 	int			nparts;
 	int			cnt_parts;
 
 	/* Guard against stack overflow due to overly deep partition hierarchy. */
 	check_stack_depth();
-
 	/* Nothing to do, if the join relation is not partitioned. */
 	if (!IS_PARTITIONED_REL(joinrel))
 		return;
@@ -1390,23 +1691,91 @@ try_partitionwise_join(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 			(List *) adjust_appendrel_attrs(root,
 											(Node *) parent_restrictlist,
 											nappinfos, appinfos);
-		pfree(appinfos);
 
 		child_joinrel = joinrel->part_rels[cnt_parts];
 		if (!child_joinrel)
 		{
-			child_joinrel = build_child_join_rel(root, child_rel1, child_rel2,
-												 joinrel, child_restrictlist,
-												 child_sjinfo,
-												 child_sjinfo->jointype);
-			joinrel->part_rels[cnt_parts] = child_joinrel;
+			if (!grouped)
+				child_joinrel = build_child_join_rel(root, child_rel1, child_rel2,
+													 joinrel,
+													 child_restrictlist,
+													 child_sjinfo,
+													 child_sjinfo->jointype,
+													 false);
+			else
+			{
+				/*
+				 * The join should have been created when we were called with
+				 * !grouped.
+				 */
+				child_joinrel = find_join_rel(root, bms_union(child_rel1->relids,
+															  child_rel2->relids));
+				Assert(child_joinrel);
+			}
 		}
 
+		if (grouped)
+		{
+			RelOptInfo *joinrel_grouped,
+					   *child_joinrel_grouped;
+			RelAggInfo *child_agg_info;
+
+			joinrel_grouped = joinrel->grouped;
+
+			if (child_joinrel->grouped == NULL)
+				child_joinrel->grouped =
+					build_child_join_rel(root, child_rel1, child_rel2,
+										 joinrel_grouped,
+										 child_restrictlist,
+										 child_sjinfo,
+										 child_sjinfo->jointype,
+										 true);
+
+			/*
+			 * The grouped join is what we need till the end of the function.
+			 */
+			child_joinrel_grouped = child_joinrel->grouped;
+
+			/*
+			 * Make sure the child_joinrel has reltarget initialized.
+			 *
+			 * Although build_child_join_rel() creates reltarget for each
+			 * child join from scratch as opposed to translating the parent
+			 * reltarget (XXX set_append_rel_size() uses the translation ---
+			 * is this inconsistency justified?), we just translate the parent
+			 * reltarget here. Per-child call of create_rel_agg_info() would
+			 * introduce too much duplicate work because it needs the *parent*
+			 * target as a source and that one is identical for all the child
+			 * joins
+			 */
+			child_agg_info = translate_rel_agg_info(root,
+													joinrel_grouped->agg_info,
+													appinfos, nappinfos);
+
+			/*
+			 * Make sure the child joinrel has reltarget initialized.
+			 */
+			if (child_joinrel_grouped->reltarget == NULL)
+			{
+				set_grouped_joinrel_target(root, child_joinrel_grouped, rel1, rel2,
+										   child_sjinfo, child_restrictlist,
+										   child_agg_info);
+			}
+
+			joinrel_grouped->part_rels[cnt_parts] = child_joinrel_grouped;
+		}
+		else
+			joinrel->part_rels[cnt_parts] = child_joinrel;
+
+		pfree(appinfos);
+
 		Assert(bms_equal(child_joinrel->relids, child_joinrelids));
 
 		populate_joinrel_with_paths(root, child_rel1, child_rel2,
 									child_joinrel, child_sjinfo,
-									child_restrictlist);
+									child_restrictlist,
+									grouped,
+									do_aggregate);
 	}
 }
 
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index ec66cb9c3c..52e76530f3 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -1664,3 +1664,165 @@ has_useful_pathkeys(PlannerInfo *root, RelOptInfo *rel)
 		return true;			/* might be able to use them for ordering */
 	return false;				/* definitely useless */
 }
+
+/*
+ * Add a new set of unique keys to a list of unique key sets, to which keys_p
+ * points. If an identical set is already there, free new_set instead of
+ * adding it.
+ */
+void
+add_uniquekeys(List **keys_p, Bitmapset *new_set)
+{
+	ListCell   *lc;
+
+	foreach(lc, *keys_p)
+	{
+		Bitmapset  *set = (Bitmapset *) lfirst(lc);
+
+		if (bms_equal(new_set, set))
+			break;
+	}
+	if (lc == NULL)
+		*keys_p = lappend(*keys_p, new_set);
+	else
+		bms_free(new_set);
+}
+
+/*
+ * Return true the output of a path having given uniquekeys and target
+ * contains only distinct values of root->group_pathkeys.
+ */
+bool
+match_uniquekeys_to_group_pathkeys(PlannerInfo *root,
+								   List *uniquekeys,
+								   PathTarget *target)
+{
+	Bitmapset  *uniquekeys_all = NULL;
+	ListCell   *l1;
+	int			i;
+	bool	   *is_group_expr;
+
+	/*
+	 * group_pathkeys are essential for this function.
+	 */
+	if (root->group_pathkeys == NIL)
+		return false;
+
+	/*
+	 * The path is not aware of being unique.
+	 */
+	if (uniquekeys == NIL)
+		return false;
+
+	/*
+	 * There can be multiple known unique key sets. Gather pathkeys of all the
+	 * unique expressions the sets may reference.
+	 */
+	foreach(l1, uniquekeys)
+	{
+		Bitmapset  *set = (Bitmapset *) lfirst(l1);
+
+		uniquekeys_all = bms_union(uniquekeys_all, set);
+	}
+
+	/*
+	 * Find pathkeys for the expressions.
+	 */
+	is_group_expr = (bool *)
+		palloc0(list_length(target->exprs) * sizeof(bool));
+
+	i = 0;
+	foreach(l1, target->exprs)
+	{
+		Expr	   *expr = (Expr *) lfirst(l1);
+
+		if (bms_is_member(i, uniquekeys_all))
+		{
+			ListCell   *l2;
+			bool		found = false;
+
+			/*
+			 * This is an unique expression, so find its pathkey.
+			 */
+			foreach(l2, root->group_pathkeys)
+			{
+				PathKey    *pk = lfirst_node(PathKey, l2);
+				EquivalenceClass *ec = pk->pk_eclass;
+				ListCell   *l3;
+				EquivalenceMember *em = NULL;
+
+				if (ec->ec_below_outer_join)
+					continue;
+				if (ec->ec_has_volatile)
+					continue;
+
+				foreach(l3, ec->ec_members)
+				{
+					em = lfirst_node(EquivalenceMember, l3);
+
+					if (em->em_nullable_relids)
+						continue;
+
+					if (equal(em->em_expr, expr))
+					{
+						found = true;
+						break;
+					}
+				}
+				if (found)
+					break;
+
+			}
+			is_group_expr[i] = found;
+		}
+
+		i++;
+	}
+
+	/*
+	 * Now check the unique key sets and see if any one matches all items of
+	 * group_pathkeys.
+	 */
+	foreach(l1, uniquekeys)
+	{
+		Bitmapset  *set = (Bitmapset *) lfirst(l1);
+		bool		found = false;
+
+		/*
+		 * Check unique keys associated with this set.
+		 */
+		for (i = 0; i < list_length(target->exprs); i++)
+		{
+			/*
+			 * Is this expression an unique key?
+			 */
+			if (bms_is_member(i, set))
+			{
+				/*
+				 * If the set misses a single grouping pathkey, at least one
+				 * expression of the unique key is outside the grouping
+				 * expressions, and thus the path can generate multiple rows
+				 * with the same grouping expressions.
+				 */
+				if (!is_group_expr[i])
+				{
+					found = true;
+					break;
+				}
+			}
+		}
+
+		/*
+		 * No problem with this set. No need to check the other ones.
+		 */
+		if (!found)
+		{
+			pfree(is_group_expr);
+			return true;
+		}
+	}
+
+	/* No match found. */
+	pfree(is_group_expr);
+	return false;
+}
diff --git a/src/backend/optimizer/path/tidpath.c b/src/backend/optimizer/path/tidpath.c
index 3bb5b8def6..bb0f8142a6 100644
--- a/src/backend/optimizer/path/tidpath.c
+++ b/src/backend/optimizer/path/tidpath.c
@@ -250,10 +250,11 @@ TidQualFromBaseRestrictinfo(RelOptInfo *rel)
  *	  Candidate paths are added to the rel's pathlist (using add_path).
  */
 void
-create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel)
+create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel, bool grouped)
 {
 	Relids		required_outer;
 	List	   *tidquals;
+	Path	   *tidpath;
 
 	/*
 	 * We don't support pushing join clauses into the quals of a tidscan, but
@@ -263,8 +264,20 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel)
 	required_outer = rel->lateral_relids;
 
 	tidquals = TidQualFromBaseRestrictinfo(rel);
+	if (!tidquals)
+		return;
 
-	if (tidquals)
-		add_path(rel, (Path *) create_tidscan_path(root, rel, tidquals,
-												   required_outer));
+	tidpath = (Path *) create_tidscan_path(root, rel, tidquals,
+										   required_outer);
+
+	if (!grouped)
+		add_path(rel, tidpath);
+	else if (required_outer == NULL)
+	{
+		/*
+		 * Only AGG_HASHED is suitable here as it does not expect the input
+		 * set to be sorted.
+		 */
+		create_grouped_path(root, rel, tidpath, false, false, AGG_HASHED);
+	}
 }
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ae41c9efa0..f9dde17ce8 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -824,6 +824,12 @@ use_physical_tlist(PlannerInfo *root, Path *path, int flags)
 		return false;
 
 	/*
+	 * Grouped relation's target list contains GroupedVars.
+	 */
+	if (rel->agg_info != NULL)
+		return false;
+
+	/*
 	 * If a bitmap scan's tlist is empty, keep it as-is.  This may allow the
 	 * executor to skip heap page fetches, and in any case, the benefit of
 	 * using a physical tlist instead would be minimal.
@@ -1667,7 +1673,8 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags)
 	 * therefore can't predict whether it will require an exact tlist. For
 	 * both of these reasons, we have to recheck here.
 	 */
-	if (use_physical_tlist(root, &best_path->path, flags))
+	if (!best_path->force_result &&
+		use_physical_tlist(root, &best_path->path, flags))
 	{
 		/*
 		 * Our caller doesn't really care what tlist we return, so we don't
@@ -1680,7 +1687,8 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags)
 			apply_pathtarget_labeling_to_tlist(tlist,
 											   best_path->path.pathtarget);
 	}
-	else if (is_projection_capable_path(best_path->subpath))
+	else if (!best_path->force_result &&
+			 is_projection_capable_path(best_path->subpath))
 	{
 		/*
 		 * Our caller requires that we return the exact tlist, but no separate
@@ -5881,6 +5889,21 @@ find_ec_member_for_tle(EquivalenceClass *ec,
 	while (tlexpr && IsA(tlexpr, RelabelType))
 		tlexpr = ((RelabelType *) tlexpr)->arg;
 
+	/*
+	 * GroupedVar can contain either non-Var grouping expression or aggregate.
+	 * The grouping expression might be useful for sorting, however aggregates
+	 * shouldn't currently appear among pathkeys.
+	 */
+	if (IsA(tlexpr, GroupedVar))
+	{
+		GroupedVar *gvar = castNode(GroupedVar, tlexpr);
+
+		if (!IsA(gvar->gvexpr, Aggref))
+			tlexpr = gvar->gvexpr;
+		else
+			return NULL;
+	}
+
 	foreach(lc, ec->ec_members)
 	{
 		EquivalenceMember *em = (EquivalenceMember *) lfirst(lc);
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 01335db511..0dca87a589 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -14,6 +14,7 @@
  */
 #include "postgres.h"
 
+#include "access/sysattr.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_class.h"
 #include "nodes/nodeFuncs.h"
@@ -27,6 +28,7 @@
 #include "optimizer/planner.h"
 #include "optimizer/prep.h"
 #include "optimizer/restrictinfo.h"
+#include "optimizer/tlist.h"
 #include "optimizer/var.h"
 #include "parser/analyze.h"
 #include "rewrite/rewriteManip.h"
@@ -46,6 +48,9 @@ typedef struct PostponedQual
 } PostponedQual;
 
 
+static void create_aggregate_grouped_var_infos(PlannerInfo *root);
+static void create_grouping_expr_grouped_var_infos(PlannerInfo *root);
+static RelOptInfo *copy_simple_rel(PlannerInfo *root, RelOptInfo *rel);
 static void extract_lateral_references(PlannerInfo *root, RelOptInfo *brel,
 						   Index rtindex);
 static List *deconstruct_recurse(PlannerInfo *root, Node *jtnode,
@@ -96,10 +101,9 @@ static void check_hashjoinable(RestrictInfo *restrictinfo);
  * jtnode.  Internally, the function recurses through the jointree.
  *
  * At the end of this process, there should be one baserel RelOptInfo for
- * every non-join RTE that is used in the query.  Therefore, this routine
- * is the only place that should call build_simple_rel with reloptkind
- * RELOPT_BASEREL.  (Note: build_simple_rel recurses internally to build
- * "other rel" RelOptInfos for the members of any appendrels we find here.)
+ * every non-grouped non-join RTE that is used in the query. (Note:
+ * build_simple_rel recurses internally to build "other rel" RelOptInfos for
+ * the members of any appendrels we find here.)
  */
 void
 add_base_rels_to_query(PlannerInfo *root, Node *jtnode)
@@ -241,6 +245,415 @@ add_vars_to_targetlist(PlannerInfo *root, List *vars,
 	}
 }
 
+/*
+ * Add GroupedVarInfo to grouped_var_list for each aggregate as well as for
+ * each possible grouping expression and setup RelOptInfo for each base or
+ * 'other' relation that can product grouped paths.
+ *
+ * Note that targets of the 'other' relations are not set here ---
+ * set_append_rel_size() will create them by translating the targets of the
+ * base rel.
+ *
+ * root->group_pathkeys must be setup before this function is called.
+ */
+extern void
+add_grouped_base_rels_to_query(PlannerInfo *root)
+{
+	int			i;
+
+	/*
+	 * Isn't user interested in the aggregate push-down feature?
+	 */
+	if (!enable_agg_pushdown)
+		return;
+
+	/* No grouping in the query? */
+	if (!root->parse->groupClause)
+		return;
+
+	/*
+	 * Grouping sets require multiple different groupings but the base
+	 * relation can only generate one.
+	 */
+	if (root->parse->groupingSets)
+		return;
+
+	/*
+	 * SRF is not allowed in the aggregate argument and we don't even want it
+	 * in the GROUP BY clause, so forbid it in general. It needs to be
+	 * analyzed if evaluation of a GROUP BY clause containing SRF below the
+	 * query targetlist would be correct. Currently it does not seem to be an
+	 * important use case.
+	 */
+	if (root->parse->hasTargetSRFs)
+		return;
+
+	/*
+	 * TODO Consider if this is a real limitation.
+	 */
+	if (root->parse->hasWindowFuncs)
+		return;
+
+	/* Create GroupedVarInfo per (distinct) aggregate. */
+	create_aggregate_grouped_var_infos(root);
+
+	/* Isn't there any aggregate to be pushed down? */
+	if (root->grouped_var_list == NIL)
+		return;
+
+	/* Create GroupedVarInfo per grouping expression. */
+	create_grouping_expr_grouped_var_infos(root);
+
+	/*
+	 * Are all the aggregates AGGSPLIT_SIMPLE?
+	 */
+	if (root->grouped_var_list == NIL)
+		return;
+
+	/* Process the individual base relations. */
+	for (i = 1; i < root->simple_rel_array_size; i++)
+	{
+		RelOptInfo *rel = root->simple_rel_array[i];
+		RangeTblEntry *rte;
+		RelAggInfo *agg_info;
+
+		/* NULL should mean a join relation. */
+		if (rel == NULL)
+			continue;
+
+		/*
+		 * Not all RTE kinds are supported when grouping is considered.
+		 *
+		 * TODO Consider relaxing some of these restrictions.
+		 */
+		rte = root->simple_rte_array[rel->relid];
+		if (rte->rtekind != RTE_RELATION ||
+			rte->relkind == RELKIND_FOREIGN_TABLE ||
+			rte->tablesample != NULL)
+			return;
+
+		/*
+		 * Grouped "other member rels" should not be created until we know
+		 * whether the parent can be grouped, i.e. until the parent has
+		 * rel->agg_info initialized.
+		 */
+		if (rel->reloptkind != RELOPT_BASEREL)
+			continue;
+
+		/*
+		 * Retrieve the information we need for aggregation of the rel
+		 * contents.
+		 */
+		Assert(rel->agg_info == NULL);
+		agg_info = create_rel_agg_info(root, rel);
+		if (agg_info == NULL)
+			continue;
+
+		/*
+		 * Create the grouped counterpart of "rel". This may includes the
+		 * "other member rels" rejected above, if they're children of this
+		 * rel. (The child rels will have their ->target and ->agg_info
+		 * initialized later by set_append_rel_size()).
+		 */
+		Assert(rel->agg_info == NULL);
+		Assert(rel->grouped == NULL);
+		rel->grouped = copy_simple_rel(root, rel);
+
+		/*
+		 * Assign it the aggregation-specific info.
+		 *
+		 * The aggregation paths will get their input target from agg_info, so
+		 * store it too.
+		 */
+		rel->grouped->reltarget = agg_info->target;
+		rel->grouped->agg_info = agg_info;
+	}
+}
+
+/*
+ * Create GroupedVarInfo for each distinct aggregate.
+ *
+ * If any aggregate is not suitable, set root->grouped_var_list to NIL and
+ * return.
+ */
+static void
+create_aggregate_grouped_var_infos(PlannerInfo *root)
+{
+	List	   *tlist_exprs;
+	ListCell   *lc;
+
+	Assert(root->grouped_var_list == NIL);
+
+	tlist_exprs = pull_var_clause((Node *) root->processed_tlist,
+								  PVC_INCLUDE_AGGREGATES);
+
+	/*
+	 * Although GroupingFunc is related to root->parse->groupingSets, this
+	 * field does not necessarily reflect its presence.
+	 */
+	foreach(lc, tlist_exprs)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+
+		if (IsA(expr, GroupingFunc))
+			return;
+	}
+
+	/*
+	 * Aggregates within the HAVING clause need to be processed in the same
+	 * way as those in the main targetlist.
+	 */
+	if (root->parse->havingQual != NULL)
+	{
+		List	   *having_exprs;
+
+		having_exprs = pull_var_clause((Node *) root->parse->havingQual,
+									   PVC_INCLUDE_AGGREGATES);
+		if (having_exprs != NIL)
+			tlist_exprs = list_concat(tlist_exprs, having_exprs);
+	}
+
+	if (tlist_exprs == NIL)
+		return;
+
+	/* tlist_exprs may also contain Vars, but we only need Aggrefs. */
+	foreach(lc, tlist_exprs)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+		Aggref	   *aggref;
+		ListCell   *lc2;
+		GroupedVarInfo *gvi;
+		bool		exists;
+
+		if (IsA(expr, Var))
+			continue;
+
+		aggref = castNode(Aggref, expr);
+
+		/* TODO Think if (some of) these can be handled. */
+		if (aggref->aggvariadic ||
+			aggref->aggdirectargs || aggref->aggorder ||
+			aggref->aggdistinct || aggref->aggfilter)
+		{
+			/*
+			 * Partial aggregation is not useful if at least one aggregate
+			 * cannot be evaluated below the top-level join.
+			 *
+			 * XXX Is it worth freeing the GroupedVarInfos and their subtrees?
+			 */
+			root->grouped_var_list = NIL;
+			break;
+		}
+
+		/*
+		 * Aggregation push-down does not work w/o aggcombinefn. This field is
+		 * not mandatory, so check if this particular aggregate can handle
+		 * partial aggregation.
+		 */
+		if (!OidIsValid(aggref->aggcombinefn))
+		{
+			root->grouped_var_list = NIL;
+			break;
+		}
+
+		/* Does GroupedVarInfo for this aggregate already exist? */
+		exists = false;
+		foreach(lc2, root->grouped_var_list)
+		{
+			gvi = lfirst_node(GroupedVarInfo, lc2);
+
+			if (equal(expr, gvi->gvexpr))
+			{
+				exists = true;
+				break;
+			}
+		}
+
+		/* Construct a new GroupedVarInfo if does not exist yet. */
+		if (!exists)
+		{
+			Relids		relids;
+
+			gvi = makeNode(GroupedVarInfo);
+			gvi->gvid = list_length(root->grouped_var_list);
+			gvi->gvexpr = (Expr *) copyObject(aggref);
+
+			/* Find out where the aggregate should be evaluated. */
+			relids = pull_varnos((Node *) aggref);
+			if (!bms_is_empty(relids))
+				gvi->gv_eval_at = relids;
+			else
+				gvi->gv_eval_at = NULL;
+
+			root->grouped_var_list = lappend(root->grouped_var_list, gvi);
+		}
+	}
+
+	list_free(tlist_exprs);
+}
+
+/*
+ * Create GroupedVarInfo for each expression usable as grouping key.
+ *
+ * In addition to the expressions of the query targetlist, group_pathkeys is
+ * also considered the source of grouping expressions. That increases the
+ * chance to get the relation output grouped.
+ */
+static void
+create_grouping_expr_grouped_var_infos(PlannerInfo *root)
+{
+	ListCell   *l1,
+			   *l2;
+	List	   *exprs = NIL;
+	List	   *sortgrouprefs = NIL;
+
+	/*
+	 * Make sure GroupedVarInfo exists for each expression usable as grouping
+	 * key.
+	 */
+	foreach(l1, root->parse->groupClause)
+	{
+		SortGroupClause *sgClause;
+		TargetEntry *te;
+		Index		sortgroupref;
+
+		sgClause = lfirst_node(SortGroupClause, l1);
+		te = get_sortgroupclause_tle(sgClause, root->processed_tlist);
+		sortgroupref = te->ressortgroupref;
+
+		if (sortgroupref == 0)
+			continue;
+
+		/*
+		 * Non-zero sortgroupref does not necessarily imply grouping
+		 * expression: data can also be sorted by aggregate.
+		 */
+		if (IsA(te->expr, Aggref))
+			continue;
+
+		exprs = lappend(exprs, te->expr);
+		sortgrouprefs = lappend_int(sortgrouprefs, sortgroupref);
+	}
+
+	/*
+	 * Construct GroupedVarInfo for each expression.
+	 */
+	forboth(l1, exprs, l2, sortgrouprefs)
+	{
+		Expr	   *expr = (Expr *) lfirst(l1);
+		int			sortgroupref = lfirst_int(l2);
+		GroupedVarInfo *gvi = makeNode(GroupedVarInfo);
+
+		gvi->gvid = list_length(root->grouped_var_list);
+		gvi->gvexpr = (Expr *) copyObject(expr);
+		gvi->sortgroupref = sortgroupref;
+
+		/* Find out where the expression should be evaluated. */
+		gvi->gv_eval_at = pull_varnos((Node *) expr);
+
+		root->grouped_var_list = lappend(root->grouped_var_list, gvi);
+	}
+}
+
+/*
+ * Take a flat copy of already initialized RelOptInfo and process child rels
+ * recursively.
+ *
+ * Flat copy ensures that we do not miss any information that the non-grouped
+ * rel already contains. XXX Do we need to copy any Node field?
+ *
+ * TODO The function only produces grouped rels, the name should reflect it
+ * (create_grouped_rel() ?).
+ */
+static RelOptInfo *
+copy_simple_rel(PlannerInfo *root, RelOptInfo *rel)
+{
+	Index		relid = rel->relid;
+	RangeTblEntry *rte;
+	ListCell   *l;
+	List	   *indexlist = NIL;
+	RelOptInfo *result;
+
+	result = makeNode(RelOptInfo);
+	memcpy(result, rel, sizeof(RelOptInfo));
+
+	/*
+	 * The new relation is grouped itself.
+	 */
+	result->grouped = NULL;
+
+	/*
+	 * The target to generate aggregation input will be initialized later.
+	 */
+	result->reltarget = NULL;
+
+	/*
+	 * Make sure that index paths have access to the parent rel's agg_info,
+	 * which is used to indicate that the rel should produce grouped paths.
+	 */
+	foreach(l, result->indexlist)
+	{
+		IndexOptInfo *src,
+				   *dst;
+
+		src = lfirst_node(IndexOptInfo, l);
+		dst = makeNode(IndexOptInfo);
+		memcpy(dst, src, sizeof(IndexOptInfo));
+
+		dst->rel = result;
+		indexlist = lappend(indexlist, dst);
+	}
+	result->indexlist = indexlist;
+
+	/*
+	 * This is very similar to child rel processing in build_simple_rel().
+	 */
+	rte = root->simple_rte_array[relid];
+	if (rte->inh)
+	{
+		int			nparts = rel->nparts;
+		int			cnt_parts = 0;
+
+		if (nparts > 0)
+			result->part_rels = (RelOptInfo **)
+				palloc(sizeof(RelOptInfo *) * nparts);
+
+		foreach(l, root->append_rel_list)
+		{
+			AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(l);
+			RelOptInfo *childrel;
+
+			/* append_rel_list contains all append rels; ignore others */
+			if (appinfo->parent_relid != relid)
+				continue;
+
+			/*
+			 * The non-grouped child rel must already exist.
+			 */
+			childrel = root->simple_rel_array[appinfo->child_relid];
+			Assert(childrel != NULL);
+
+			/*
+			 * Create the copy.
+			 */
+			Assert(childrel->agg_info == NULL);
+			childrel->grouped = copy_simple_rel(root, childrel);
+
+			/* Nothing more to do for an unpartitioned table. */
+			if (!rel->part_scheme)
+				continue;
+
+			Assert(cnt_parts < nparts);
+			result->part_rels[cnt_parts] = childrel;
+			cnt_parts++;
+		}
+
+		/* We should have seen all the child partitions. */
+		Assert(cnt_parts == nparts);
+	}
+
+	return result;
+}
 
 /*****************************************************************************
  *
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index b05adc70c4..0ca5d6ea0b 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -43,6 +43,8 @@
  *		(this is NOT necessarily root->parse->targetList!)
  * qp_callback is a function to compute query_pathkeys once it's safe to do so
  * qp_extra is optional extra data to pass to qp_callback
+ * *partially_grouped may receive relation that contains partial aggregate
+ *  anywhere in the join tree.
  *
  * Note: the PlannerInfo node also includes a query_pathkeys field, which
  * tells query_planner the sort order that is desired in the final output
@@ -66,6 +68,8 @@ query_planner(PlannerInfo *root, List *tlist,
 	 */
 	if (parse->jointree->fromlist == NIL)
 	{
+		RelOptInfo *final_rel;
+
 		/* We need a dummy joinrel to describe the empty set of baserels */
 		final_rel = build_empty_join_rel(root);
 
@@ -114,6 +118,7 @@ query_planner(PlannerInfo *root, List *tlist,
 	root->full_join_clauses = NIL;
 	root->join_info_list = NIL;
 	root->placeholder_list = NIL;
+	root->grouped_var_list = NIL;
 	root->fkey_list = NIL;
 	root->initial_rels = NIL;
 
@@ -232,6 +237,16 @@ query_planner(PlannerInfo *root, List *tlist,
 	extract_restriction_or_clauses(root);
 
 	/*
+	 * If the query result can be grouped, check if any grouping can be
+	 * performed below the top-level join. If so, setup root->grouped_var_list
+	 * and create RelOptInfo for base relations capable to do the grouping.
+	 *
+	 * The base relations should be fully initialized now, so that we have
+	 * enough info to decide whether grouping is possible.
+	 */
+	add_grouped_base_rels_to_query(root);
+
+	/*
 	 * We should now have size estimates for every actual table involved in
 	 * the query, and we also know which if any have been deleted from the
 	 * query by join removal; so we can compute total_table_pages.
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index fd06da98b9..ffef925f5f 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -133,9 +133,6 @@ static double get_number_of_groups(PlannerInfo *root,
 					 double path_rows,
 					 grouping_sets_data *gd,
 					 List *target_list);
-static Size estimate_hashagg_tablesize(Path *path,
-						   const AggClauseCosts *agg_costs,
-						   double dNumGroups);
 static RelOptInfo *create_grouping_paths(PlannerInfo *root,
 					  RelOptInfo *input_rel,
 					  PathTarget *target,
@@ -2044,6 +2041,7 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 												grouping_target_parallel_safe,
 												&agg_costs,
 												gset_data);
+
 			/* Fix things up if grouping_target contains SRFs */
 			if (parse->hasTargetSRFs)
 				adjust_paths_for_srfs(root, current_rel,
@@ -3640,40 +3638,6 @@ get_number_of_groups(PlannerInfo *root,
 }
 
 /*
- * estimate_hashagg_tablesize
- *	  estimate the number of bytes that a hash aggregate hashtable will
- *	  require based on the agg_costs, path width and dNumGroups.
- *
- * XXX this may be over-estimating the size now that hashagg knows to omit
- * unneeded columns from the hashtable. Also for mixed-mode grouping sets,
- * grouping columns not in the hashed set are counted here even though hashagg
- * won't store them. Is this a problem?
- */
-static Size
-estimate_hashagg_tablesize(Path *path, const AggClauseCosts *agg_costs,
-						   double dNumGroups)
-{
-	Size		hashentrysize;
-
-	/* Estimate per-hash-entry space at tuple width... */
-	hashentrysize = MAXALIGN(path->pathtarget->width) +
-		MAXALIGN(SizeofMinimalTupleHeader);
-
-	/* plus space for pass-by-ref transition values... */
-	hashentrysize += agg_costs->transitionSpace;
-	/* plus the per-hash-entry overhead */
-	hashentrysize += hash_agg_entry_size(agg_costs->numAggs);
-
-	/*
-	 * Note that this disregards the effect of fill-factor and growth policy
-	 * of the hash-table. That's probably ok, given default the default
-	 * fill-factor is relatively high. It'd be hard to meaningfully factor in
-	 * "double-in-size" growth policies here.
-	 */
-	return hashentrysize * dNumGroups;
-}
-
-/*
  * create_grouping_paths
  *
  * Build a new upperrel containing Paths for grouping and/or aggregation.
@@ -3720,6 +3684,7 @@ create_grouping_paths(PlannerInfo *root,
 	{
 		int			flags = 0;
 		GroupPathExtraData extra;
+		List	   *agg_pushdown_paths = NIL;
 
 		/*
 		 * Determine whether it's possible to perform sort-based
@@ -3787,6 +3752,38 @@ create_grouping_paths(PlannerInfo *root,
 		create_ordinary_grouping_paths(root, input_rel, grouped_rel,
 									   agg_costs, gd, &extra,
 									   &partially_grouped_rel);
+
+		/*
+		 * Process paths generated by aggregation push-down feature.
+		 */
+		if (input_rel->grouped && input_rel->grouped)
+		{
+			RelOptInfo *agg_pushdown_rel;
+			ListCell   *lc;
+
+			agg_pushdown_rel = input_rel->grouped;
+			agg_pushdown_paths = agg_pushdown_rel->pathlist;
+
+			/*
+			 * See create_grouped_path().
+			 */
+			Assert(agg_pushdown_rel->partial_pathlist == NIL);
+
+			foreach(lc, agg_pushdown_paths)
+			{
+				Path	   *path = (Path *) lfirst(lc);
+
+				/*
+				 * The aggregate push-down feature currently turns append rel
+				 * into a dummy rel, see comment in set_append_rel_pathlist().
+				 * XXX Can we eliminate this situation earlier?
+				 */
+				if (IS_DUMMY_PATH(path))
+					continue;
+
+				add_path(grouped_rel, path);
+			}
+		}
 	}
 
 	set_cheapest(grouped_rel);
@@ -3992,11 +3989,11 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 		bool		force_rel_creation;
 
 		/*
-		 * If we're doing partitionwise aggregation at this level, force
-		 * creation of a partially_grouped_rel so we can add partitionwise
-		 * paths to it.
+		 * If we're doing partitionwise aggregation at this level or if
+		 * aggregation push-down took place, force creation of a
+		 * partially_grouped_rel so we can add the related paths to it.
 		 */
-		force_rel_creation = (patype == PARTITIONWISE_AGGREGATE_PARTIAL);
+		force_rel_creation = patype == PARTITIONWISE_AGGREGATE_PARTIAL;
 
 		partially_grouped_rel =
 			create_partial_grouping_paths(root,
@@ -4029,10 +4026,14 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 
 	/* Gather any partially grouped partial paths. */
 	if (partially_grouped_rel && partially_grouped_rel->partial_pathlist)
-	{
 		gather_grouping_paths(root, partially_grouped_rel);
+
+	/*
+	 * The non-partial paths can come either from the Gather above or from
+	 * aggregate push-down.
+	 */
+	if (partially_grouped_rel && partially_grouped_rel->pathlist)
 		set_cheapest(partially_grouped_rel);
-	}
 
 	/*
 	 * Estimate number of groups.
@@ -7117,6 +7118,7 @@ create_partitionwise_grouping_paths(PlannerInfo *root,
 	if (partially_grouped_rel && partial_grouping_valid)
 	{
 		Assert(partially_grouped_live_children != NIL);
+		Assert(partially_grouped_rel->agg_info == NULL);
 
 		add_paths_to_append_rel(root, partially_grouped_rel,
 								partially_grouped_live_children);
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 69dd327f0c..f03d979f15 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -40,6 +40,7 @@ typedef struct
 	List	   *tlist;			/* underlying target list */
 	int			num_vars;		/* number of plain Var tlist entries */
 	bool		has_ph_vars;	/* are there PlaceHolderVar entries? */
+	bool		has_grp_vars;	/* are there GroupedVar entries? */
 	bool		has_non_vars;	/* are there other entries? */
 	bool		has_conv_whole_rows;	/* are there ConvertRowtypeExpr
 										 * entries encapsulating a whole-row
@@ -1988,6 +1989,7 @@ build_tlist_index(List *tlist)
 	indexed_tlist *itlist;
 	tlist_vinfo *vinfo;
 	ListCell   *l;
+	List	   *tlist_gvars = NIL;
 
 	/* Create data structure with enough slots for all tlist entries */
 	itlist = (indexed_tlist *)
@@ -1996,6 +1998,7 @@ build_tlist_index(List *tlist)
 
 	itlist->tlist = tlist;
 	itlist->has_ph_vars = false;
+	itlist->has_grp_vars = false;
 	itlist->has_non_vars = false;
 	itlist->has_conv_whole_rows = false;
 
@@ -2016,6 +2019,8 @@ build_tlist_index(List *tlist)
 		}
 		else if (tle->expr && IsA(tle->expr, PlaceHolderVar))
 			itlist->has_ph_vars = true;
+		else if (tle->expr && IsA(tle->expr, GroupedVar))
+			tlist_gvars = lappend(tlist_gvars, tle);
 		else if (is_converted_whole_row_reference((Node *) tle->expr))
 			itlist->has_conv_whole_rows = true;
 		else
@@ -2024,6 +2029,42 @@ build_tlist_index(List *tlist)
 
 	itlist->num_vars = (vinfo - itlist->vars);
 
+	/*
+	 * If the targetlist contains GroupedVars, we may need to match them to
+	 * Aggrefs in the upper plan. Thus the upper planner can always put
+	 * Aggrefs into the targetlists, regardless the subplan(s) contain the
+	 * original Aggrefs or GroupedVar substitutions.
+	 */
+	if (list_length(tlist_gvars) > 0)
+	{
+		List	   *tlist_new;
+
+		/*
+		 * Copy the source list because caller does not expect to see the
+		 * items we're going to add.
+		 */
+		tlist_new = list_copy(itlist->tlist);
+
+		foreach(l, tlist_gvars)
+		{
+			TargetEntry *tle = lfirst_node(TargetEntry, l);
+			TargetEntry *tle_new;
+			GroupedVar *gvar = castNode(GroupedVar, tle->expr);
+
+			/*
+			 * Add the entry to match the Aggref.
+			 */
+			tle_new = flatCopyTargetEntry(tle);
+			tle_new->expr = gvar->gvexpr;
+			tlist_new = lappend(tlist_new, tle_new);
+		}
+
+		itlist->tlist = tlist_new;
+		itlist->has_grp_vars = true;
+
+		list_free(tlist_gvars);
+	}
+
 	return itlist;
 }
 
@@ -2299,6 +2340,48 @@ fix_join_expr_mutator(Node *node, fix_join_expr_context *context)
 		/* No referent found for Var */
 		elog(ERROR, "variable not found in subplan target lists");
 	}
+	if (IsA(node, GroupedVar) ||IsA(node, Aggref))
+	{
+		bool		try = true;
+
+		/*
+		 * The upper plan targetlist can contain Aggref whose value has
+		 * already been evaluated by the subplan and is being delivered via
+		 * GroupedVar. However this is true only for specific kinds of Aggref.
+		 */
+		if (IsA(node, Aggref))
+		{
+			Aggref	   *aggref = castNode(Aggref, node);
+
+			if (aggref->aggsplit != AGGSPLIT_SIMPLE &&
+				aggref->aggsplit != AGGSPLIT_INITIAL_SERIAL)
+				try = false;
+		}
+
+		if (try)
+		{
+			/* See if the GroupedVar has bubbled up from a lower plan node */
+			if (context->outer_itlist && context->outer_itlist->has_grp_vars)
+			{
+				newvar = search_indexed_tlist_for_non_var((Expr *) node,
+														  context->outer_itlist,
+														  OUTER_VAR);
+				if (newvar)
+					return (Node *) newvar;
+			}
+			if (context->inner_itlist && context->inner_itlist->has_grp_vars)
+			{
+				newvar = search_indexed_tlist_for_non_var((Expr *) node,
+														  context->inner_itlist,
+														  INNER_VAR);
+				if (newvar)
+					return (Node *) newvar;
+			}
+		}
+
+		/* No referent found for GroupedVar */
+		elog(ERROR, "grouped variable not found in subplan target lists");
+	}
 	if (IsA(node, PlaceHolderVar))
 	{
 		PlaceHolderVar *phv = (PlaceHolderVar *) node;
@@ -2461,7 +2544,8 @@ fix_upper_expr_mutator(Node *node, fix_upper_expr_context *context)
 		/* If no match, just fall through to process it normally */
 	}
 	/* Try matching more complex expressions too, if tlist has any */
-	if (context->subplan_itlist->has_non_vars ||
+	if (context->subplan_itlist->has_grp_vars ||
+		context->subplan_itlist->has_non_vars ||
 		(context->subplan_itlist->has_conv_whole_rows &&
 		 is_converted_whole_row_reference(node)))
 	{
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index c5aaaf5c22..793d44e6d3 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -27,6 +27,7 @@
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
 #include "optimizer/restrictinfo.h"
+/* TODO Remove this if create_grouped_path ends up in another module. */
 #include "optimizer/tlist.h"
 #include "optimizer/var.h"
 #include "parser/parsetree.h"
@@ -56,7 +57,12 @@ static int	append_startup_cost_compare(const void *a, const void *b);
 static List *reparameterize_pathlist_by_child(PlannerInfo *root,
 								 List *pathlist,
 								 RelOptInfo *child_rel);
-
+static Bitmapset *combine_uniquekeys(Path *outerpath, Bitmapset *outerset,
+				   Path *innerpath,
+				   Bitmapset *innerset,
+				   PathTarget *target);
+static void make_uniquekeys_for_unique_index(PathTarget *reltarget,
+								 IndexOptInfo *index, Path *path);
 
 /*****************************************************************************
  *		MISC. PATH UTILITIES
@@ -955,10 +961,15 @@ create_seqscan_path(PlannerInfo *root, RelOptInfo *rel,
 					Relids required_outer, int parallel_workers)
 {
 	Path	   *pathnode = makeNode(Path);
+	bool		grouped = rel->agg_info != NULL;
 
 	pathnode->pathtype = T_SeqScan;
 	pathnode->parent = rel;
-	pathnode->pathtarget = rel->reltarget;
+	/* For grouped relation only generate the aggregation input. */
+	if (!grouped)
+		pathnode->pathtarget = rel->reltarget;
+	else
+		pathnode->pathtarget = rel->agg_info->input;
 	pathnode->param_info = get_baserel_parampathinfo(root, rel,
 													 required_outer);
 	pathnode->parallel_aware = parallel_workers > 0 ? true : false;
@@ -1038,10 +1049,15 @@ create_index_path(PlannerInfo *root,
 	RelOptInfo *rel = index->rel;
 	List	   *indexquals,
 			   *indexqualcols;
+	bool		grouped = rel->agg_info != NULL;
 
 	pathnode->path.pathtype = indexonly ? T_IndexOnlyScan : T_IndexScan;
 	pathnode->path.parent = rel;
-	pathnode->path.pathtarget = rel->reltarget;
+	/* For grouped relation only generate the aggregation input. */
+	if (!grouped)
+		pathnode->path.pathtarget = rel->reltarget;
+	else
+		pathnode->path.pathtarget = rel->agg_info->input;
 	pathnode->path.param_info = get_baserel_parampathinfo(root, rel,
 														  required_outer);
 	pathnode->path.parallel_aware = false;
@@ -1189,10 +1205,15 @@ create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, List *tidquals,
 					Relids required_outer)
 {
 	TidPath    *pathnode = makeNode(TidPath);
+	bool		grouped = rel->agg_info != NULL;
 
 	pathnode->path.pathtype = T_TidScan;
 	pathnode->path.parent = rel;
-	pathnode->path.pathtarget = rel->reltarget;
+	/* For grouped relation only generate the aggregation input. */
+	if (!grouped)
+		pathnode->path.pathtarget = rel->reltarget;
+	else
+		pathnode->path.pathtarget = rel->agg_info->input;
 	pathnode->path.param_info = get_baserel_parampathinfo(root, rel,
 														  required_outer);
 	pathnode->path.parallel_aware = false;
@@ -1229,9 +1250,11 @@ create_append_path(PlannerInfo *root,
 	Assert(!parallel_aware || parallel_workers > 0);
 
 	pathnode->path.pathtype = T_Append;
-	pathnode->path.parent = rel;
+
 	pathnode->path.pathtarget = rel->reltarget;
 
+	pathnode->path.parent = rel;
+
 	/*
 	 * When generating an Append path for a partitioned table, there may be
 	 * parameters that are useful so we can eliminate certain partitions
@@ -1341,11 +1364,13 @@ append_startup_cost_compare(const void *a, const void *b)
 /*
  * create_merge_append_path
  *	  Creates a path corresponding to a MergeAppend plan, returning the
- *	  pathnode.
+ *	  pathnode. target can be supplied by caller. If NULL is passed, the field
+ *	  is set to rel->reltarget.
  */
 MergeAppendPath *
 create_merge_append_path(PlannerInfo *root,
 						 RelOptInfo *rel,
+						 PathTarget *target,
 						 List *subpaths,
 						 List *pathkeys,
 						 Relids required_outer,
@@ -1358,7 +1383,7 @@ create_merge_append_path(PlannerInfo *root,
 
 	pathnode->path.pathtype = T_MergeAppend;
 	pathnode->path.parent = rel;
-	pathnode->path.pathtarget = rel->reltarget;
+	pathnode->path.pathtarget = target ? target : rel->reltarget;
 	pathnode->path.param_info = get_appendrel_parampathinfo(rel,
 															required_outer);
 	pathnode->path.parallel_aware = false;
@@ -1495,6 +1520,7 @@ create_material_path(RelOptInfo *rel, Path *subpath)
 		subpath->parallel_safe;
 	pathnode->path.parallel_workers = subpath->parallel_workers;
 	pathnode->path.pathkeys = subpath->pathkeys;
+	pathnode->path.uniquekeys = subpath->uniquekeys;
 
 	pathnode->subpath = subpath;
 
@@ -1528,7 +1554,9 @@ create_unique_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
 	MemoryContext oldcontext;
 	int			numCols;
 
-	/* Caller made a mistake if subpath isn't cheapest_total ... */
+	/*
+	 * Caller made a mistake if subpath isn't cheapest_total.
+	 */
 	Assert(subpath == rel->cheapest_total_path);
 	Assert(subpath->parent == rel);
 	/* ... or if SpecialJoinInfo is the wrong one */
@@ -2149,6 +2177,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
  *	  relations.
  *
  * 'joinrel' is the join relation.
+ * 'target' is the join path target
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_nestloop
  * 'extra' contains various information about the join
@@ -2163,6 +2192,7 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path)
 NestPath *
 create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
+					 PathTarget *target,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
 					 JoinPathExtraData *extra,
@@ -2203,7 +2233,7 @@ create_nestloop_path(PlannerInfo *root,
 
 	pathnode->path.pathtype = T_NestLoop;
 	pathnode->path.parent = joinrel;
-	pathnode->path.pathtarget = joinrel->reltarget;
+	pathnode->path.pathtarget = target;
 	pathnode->path.param_info =
 		get_joinrel_parampathinfo(root,
 								  joinrel,
@@ -2235,6 +2265,7 @@ create_nestloop_path(PlannerInfo *root,
  *	  two relations
  *
  * 'joinrel' is the join relation
+ * 'target' is the join path target
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_mergejoin
  * 'extra' contains various information about the join
@@ -2251,6 +2282,7 @@ create_nestloop_path(PlannerInfo *root,
 MergePath *
 create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
+					  PathTarget *target,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
 					  JoinPathExtraData *extra,
@@ -2267,7 +2299,7 @@ create_mergejoin_path(PlannerInfo *root,
 
 	pathnode->jpath.path.pathtype = T_MergeJoin;
 	pathnode->jpath.path.parent = joinrel;
-	pathnode->jpath.path.pathtarget = joinrel->reltarget;
+	pathnode->jpath.path.pathtarget = target;
 	pathnode->jpath.path.param_info =
 		get_joinrel_parampathinfo(root,
 								  joinrel,
@@ -2303,6 +2335,7 @@ create_mergejoin_path(PlannerInfo *root,
  *	  Creates a pathnode corresponding to a hash join between two relations.
  *
  * 'joinrel' is the join relation
+ * 'target' is the join path target
  * 'jointype' is the type of join required
  * 'workspace' is the result from initial_cost_hashjoin
  * 'extra' contains various information about the join
@@ -2317,6 +2350,7 @@ create_mergejoin_path(PlannerInfo *root,
 HashPath *
 create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
+					 PathTarget *target,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
 					 JoinPathExtraData *extra,
@@ -2331,7 +2365,7 @@ create_hashjoin_path(PlannerInfo *root,
 
 	pathnode->jpath.path.pathtype = T_HashJoin;
 	pathnode->jpath.path.parent = joinrel;
-	pathnode->jpath.path.pathtarget = joinrel->reltarget;
+	pathnode->jpath.path.pathtarget = target;
 	pathnode->jpath.path.param_info =
 		get_joinrel_parampathinfo(root,
 								  joinrel,
@@ -2413,8 +2447,8 @@ create_projection_path(PlannerInfo *root,
 	 * Note: in the latter case, create_projection_plan has to recheck our
 	 * conclusion; see comments therein.
 	 */
-	if (is_projection_capable_path(subpath) ||
-		equal(oldtarget->exprs, target->exprs))
+	if ((is_projection_capable_path(subpath) ||
+		 equal(oldtarget->exprs, target->exprs)))
 	{
 		/* No separate Result node needed */
 		pathnode->dummypp = true;
@@ -2647,6 +2681,7 @@ create_sort_path(PlannerInfo *root,
 		subpath->parallel_safe;
 	pathnode->path.parallel_workers = subpath->parallel_workers;
 	pathnode->path.pathkeys = pathkeys;
+	pathnode->path.uniquekeys = subpath->uniquekeys;
 
 	pathnode->subpath = subpath;
 
@@ -2799,8 +2834,7 @@ create_agg_path(PlannerInfo *root,
 	pathnode->path.pathtype = T_Agg;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = target;
-	/* For now, assume we are above any joins, so no parameterization */
-	pathnode->path.param_info = NULL;
+	pathnode->path.param_info = subpath->param_info;
 	pathnode->path.parallel_aware = false;
 	pathnode->path.parallel_safe = rel->consider_parallel &&
 		subpath->parallel_safe;
@@ -2833,6 +2867,179 @@ create_agg_path(PlannerInfo *root,
 }
 
 /*
+ * Apply AGG_SORTED aggregation path to subpath if it's suitably sorted.
+ *
+ * check_pathkeys can be passed FALSE if the function was already called for
+ * given index --- since the target should not change, we can skip the check
+ * of sorting during subsequent calls.
+ *
+ * NULL is returned if sorting of subpath output is not suitable.
+ */
+AggPath *
+create_agg_sorted_path(PlannerInfo *root, Path *subpath,
+					   bool check_pathkeys, double input_rows)
+{
+	RelOptInfo *rel;
+	Node	   *agg_exprs;
+	AggSplit	aggsplit;
+	AggClauseCosts agg_costs;
+	PathTarget *target;
+	double		dNumGroups;
+	Node	   *qual = NULL;
+	AggPath    *result = NULL;
+	RelAggInfo *agg_info;
+
+	rel = subpath->parent;
+	agg_info = rel->agg_info;
+	Assert(agg_info != NULL);
+
+	aggsplit = AGGSPLIT_SIMPLE;
+	agg_exprs = (Node *) agg_info->agg_exprs;
+	target = agg_info->target;
+
+	if (subpath->pathkeys == NIL)
+		return NULL;
+
+	if (!grouping_is_sortable(root->parse->groupClause))
+		return NULL;
+
+	if (check_pathkeys)
+	{
+		ListCell   *lc1;
+		List	   *key_subset = NIL;
+
+		/*
+		 * Find all query pathkeys that our relation does affect.
+		 */
+		foreach(lc1, root->group_pathkeys)
+		{
+			PathKey    *gkey = castNode(PathKey, lfirst(lc1));
+			ListCell   *lc2;
+
+			foreach(lc2, subpath->pathkeys)
+			{
+				PathKey    *skey = castNode(PathKey, lfirst(lc2));
+
+				if (skey == gkey)
+				{
+					key_subset = lappend(key_subset, gkey);
+					break;
+				}
+			}
+		}
+
+		if (key_subset == NIL)
+			return NULL;
+
+		/* Check if AGG_SORTED is useful for the whole query.  */
+		if (!pathkeys_contained_in(key_subset, subpath->pathkeys))
+			return NULL;
+	}
+
+	MemSet(&agg_costs, 0, sizeof(AggClauseCosts));
+	get_agg_clause_costs(root, (Node *) agg_exprs, aggsplit, &agg_costs);
+
+	if (root->parse->havingQual)
+	{
+		qual = root->parse->havingQual;
+		get_agg_clause_costs(root, agg_exprs, aggsplit, &agg_costs);
+	}
+
+	Assert(agg_info->group_exprs != NIL);
+	dNumGroups = estimate_num_groups(root, agg_info->group_exprs,
+									 input_rows, NULL);
+
+	Assert(agg_info->group_clauses != NIL);
+	result = create_agg_path(root, rel, subpath, target,
+							 AGG_SORTED, aggsplit,
+							 agg_info->group_clauses,
+							 (List *) qual, &agg_costs,
+							 dNumGroups);
+
+	return result;
+}
+
+/*
+ * Apply AGG_HASHED aggregation to subpath.
+ *
+ * Arguments have the same meaning as those of create_agg_sorted_path.
+ */
+AggPath *
+create_agg_hashed_path(PlannerInfo *root, Path *subpath,
+					   double input_rows)
+{
+	RelOptInfo *rel;
+	bool		can_hash;
+	Node	   *agg_exprs;
+	AggSplit	aggsplit;
+	AggClauseCosts agg_costs;
+	PathTarget *target;
+	double		dNumGroups;
+	Size		hashaggtablesize;
+	Query	   *parse = root->parse;
+	Node	   *qual = NULL;
+	AggPath    *result = NULL;
+	RelAggInfo *agg_info;
+
+	rel = subpath->parent;
+	agg_info = rel->agg_info;
+	Assert(agg_info != NULL);
+
+	aggsplit = AGGSPLIT_SIMPLE;
+	agg_exprs = (Node *) agg_info->agg_exprs;
+	target = agg_info->target;
+
+	MemSet(&agg_costs, 0, sizeof(AggClauseCosts));
+	get_agg_clause_costs(root, agg_exprs, aggsplit, &agg_costs);
+
+	if (parse->havingQual)
+	{
+		qual = parse->havingQual;
+		get_agg_clause_costs(root, agg_exprs, aggsplit, &agg_costs);
+	}
+
+	can_hash = (parse->groupClause != NIL &&
+				parse->groupingSets == NIL &&
+				agg_costs.numOrderedAggs == 0 &&
+				grouping_is_hashable(parse->groupClause));
+
+	if (can_hash)
+	{
+		Assert(agg_info->group_exprs != NIL);
+		dNumGroups = estimate_num_groups(root, agg_info->group_exprs,
+										 input_rows, NULL);
+
+		hashaggtablesize = estimate_hashagg_tablesize(subpath, &agg_costs,
+													  dNumGroups);
+
+		if (hashaggtablesize < work_mem * 1024L)
+		{
+			/*
+			 * Create the partial aggregation path.
+			 */
+			Assert(agg_info->group_clauses != NIL);
+
+			result = create_agg_path(root, rel, subpath,
+									 target,
+									 AGG_HASHED,
+									 aggsplit,
+									 agg_info->group_clauses,
+									 (List *) qual,
+									 &agg_costs,
+									 dNumGroups);
+
+			/*
+			 * The agg path should require no fewer parameters than the plain
+			 * one.
+			 */
+			result->path.param_info = subpath->param_info;
+		}
+	}
+
+	return result;
+}
+
+/*
  * create_groupingsets_path
  *	  Creates a pathnode that represents performing GROUPING SETS aggregation
  *
@@ -3956,3 +4163,402 @@ reparameterize_pathlist_by_child(PlannerInfo *root,
 
 	return result;
 }
+
+/*
+ * Find out if path produces an unique set of expressions and set uniquekeys
+ * accordingly.
+ *
+ * TODO Check if any expression of any unique key isn't nullable, whether in
+ * the table / index or by an outer join.
+ */
+void
+make_uniquekeys(PlannerInfo *root, Path *path)
+{
+	RelOptInfo *rel;
+
+	/*
+	 * The unique keys are not interesting if there's no chance to push
+	 * aggregation down to base relations / joins.
+	 */
+	if (root->grouped_var_list == NIL)
+		return;
+
+	/*
+	 * Do not accept repeated calls of the function on the same path.
+	 */
+	if (path->uniquekeys != NIL)
+		return;
+
+	rel = path->parent;
+
+	/*
+	 * Base relations.
+	 */
+	if (IsA(path, IndexPath) ||
+		(IsA(path, Path) &&path->pathtype == T_SeqScan))
+	{
+		ListCell   *lc;
+
+		/*
+		 * Derive grouping keys from unique indexes.
+		 */
+		if (IsA(path, IndexPath) ||IsA(path, Path))
+		{
+			foreach(lc, rel->indexlist)
+			{
+				IndexOptInfo *index = lfirst_node(IndexOptInfo, lc);
+
+				make_uniquekeys_for_unique_index(rel->reltarget, index, path);
+			}
+		}
+#ifdef USE_ASSERT_CHECKING
+		else
+			Assert(false);
+#endif
+		return;
+	}
+
+	if (IsA(path, AggPath))
+	{
+		/*
+		 * The immediate output of aggregation essentially produces an unique
+		 * set of grouping keys.
+		 */
+		make_uniquekeys_for_agg_path(path);
+		return;
+	}
+#ifdef USE_ASSERT_CHECKING
+
+	/*
+	 * TODO Consider other ones, e.g. UniquePath.
+	 */
+	Assert(false);
+#endif
+}
+
+/*
+ * Create uniquekeys for a path that has Aggrefs in its target.
+ * set.
+ *
+ * Besides AggPath, ForeignPath is a known use case for this function.
+ */
+void
+make_uniquekeys_for_agg_path(Path *path)
+{
+	PathTarget *target;
+	ListCell   *lc;
+	Bitmapset  *keyset = NULL;
+	int			i = 0;
+
+	target = path->pathtarget;
+	Assert(target->sortgrouprefs != NULL);
+
+	foreach(lc, target->exprs)
+	{
+		Expr	   *expr = (Expr *) lfirst(lc);
+
+		if (IsA(expr, GroupedVar))
+		{
+			GroupedVar *gvar = castNode(GroupedVar, expr);
+
+			if (!IsA(gvar->gvexpr, Aggref))
+			{
+				/*
+				 * Generic grouping expression.
+				 */
+				keyset = bms_add_member(keyset, i);
+			}
+		}
+		else
+		{
+			Assert(IsA(expr, Var));
+
+			if (target->sortgrouprefs[i] > 0)
+			{
+				/*
+				 * Plain Var grouping expression.
+				 */
+				keyset = bms_add_member(keyset, i);
+			}
+			else
+			{
+				/*
+				 * A column functionally dependent on the GROUP BY clause?
+				 */
+			}
+		}
+
+		i++;
+	}
+
+	add_uniquekeys(&path->uniquekeys, keyset);
+}
+
+/*
+ * Unlike other kinds of path, creation of join path might be rejected due to
+ * inappropriate uniquekeys. Therefore this function only derives uniquekeys
+ * for a join and checks if the join would produce unique grouping
+ * keys. Caller is responsible for adding them to the path if it's eventually
+ * created.
+ */
+List *
+make_uniquekeys_for_join(PlannerInfo *root, Path *outerpath, Path *innerpath,
+						 PathTarget *target, bool *keys_ok)
+{
+	ListCell   *l1;
+	List	   *result = NIL;
+
+	*keys_ok = true;
+
+	/*
+	 * Find out if the join produces unique keys for various combinations of
+	 * input sets of unique keys.
+	 *
+	 * TODO Implement heuristic that picks a few most useful sets on each
+	 * side, to avoid exponential growth of the uniquekeys list as we proceed
+	 * from lower to higher joins. Maybe also discard the resulting sets
+	 * containing unique expressions which are not grouping expressions (and
+	 * of course which are not aggregates) of this join's target.
+	 */
+	foreach(l1, outerpath->uniquekeys)
+	{
+		ListCell   *l2;
+		Bitmapset  *outerset = (Bitmapset *) lfirst(l1);
+
+		foreach(l2, innerpath->uniquekeys)
+		{
+			Bitmapset  *innerset = (Bitmapset *) lfirst(l2);
+			Bitmapset  *joinset;
+
+			joinset = combine_uniquekeys(outerpath, outerset, innerpath,
+										 innerset, target);
+			if (joinset != NULL)
+			{
+				/* Add the set to the path. */
+				add_uniquekeys(&result, joinset);
+			}
+		}
+	}
+
+	if (!match_uniquekeys_to_group_pathkeys(root, result, target))
+		*keys_ok = false;
+
+	return result;
+}
+
+/*
+ * Create join uniquekeys out of the uniquekeys of input paths.
+ */
+static Bitmapset *
+combine_uniquekeys(Path *outerpath, Bitmapset *outerset, Path *innerpath,
+				   Bitmapset *innerset, PathTarget *target)
+{
+	ListCell   *l1;
+	PathTarget *unique_exprs;
+	Expr	   *expr;
+	Index		sortgroupref;
+	int			i;
+	Bitmapset  *result = NULL;
+
+	/*
+	 * TODO sortgroupref is used to improve matching of the input and output
+	 * path. Better solution might be to store the uniquekeys as a list of EC
+	 * pointers, which is how PathKey is implemented.
+	 */
+
+	/*
+	 * Use PathTarget so that we can store both expression and its
+	 * sortgroupref.
+	 */
+	unique_exprs = create_empty_pathtarget();
+
+	/*
+	 * First, collect the expressions corresponding to the uniquekeys of each
+	 * input target.
+	 */
+	i = 0;
+	foreach(l1, outerpath->pathtarget->exprs)
+	{
+		expr = (Expr *) lfirst(l1);
+
+		sortgroupref = 0;
+		if (outerpath->pathtarget->sortgrouprefs)
+			sortgroupref = outerpath->pathtarget->sortgrouprefs[i];
+
+		if (bms_is_member(i, outerset))
+			add_column_to_pathtarget(unique_exprs, expr, sortgroupref);
+
+		i++;
+	}
+	i = 0;
+	foreach(l1, innerpath->pathtarget->exprs)
+	{
+		expr = (Expr *) lfirst(l1);
+
+		sortgroupref = 0;
+		if (innerpath->pathtarget->sortgrouprefs)
+			sortgroupref = innerpath->pathtarget->sortgrouprefs[i];
+
+		if (bms_is_member(i, innerset))
+			add_column_to_pathtarget(unique_exprs, expr, sortgroupref);
+
+		i++;
+	}
+	if (unique_exprs->exprs == NIL)
+		return NULL;
+
+	/*
+	 * Now find each expression in the join target and add the position to the
+	 * output uniquekeys.
+	 */
+	i = 0;
+	foreach(l1, unique_exprs->exprs)
+	{
+		Expr	   *unique_expr = (Expr *) lfirst(l1);
+		Index		sortgroupref = 0;
+		ListCell   *l2;
+		int			j;
+		bool		match = false;
+
+		if (unique_exprs->sortgrouprefs)
+			sortgroupref = unique_exprs->sortgrouprefs[i];
+
+		/*
+		 * As some expressions of the input uniquekeys do not necessarily
+		 * appear in the output target (they could have been there just
+		 * because of the join clause of the current join), we first try to
+		 * find match using sortgroupref. If one expression is gone but other
+		 * one with the same sortgroupref still exists (i.e. one was derived
+		 * from the other), it should always have the same value.
+		 */
+		j = 0;
+		if (sortgroupref > 0)
+		{
+			foreach(l2, target->exprs)
+			{
+				Index		sortgroupref_target = 0;
+
+				if (target->sortgrouprefs)
+					sortgroupref_target = target->sortgrouprefs[j];
+
+				if (sortgroupref_target == sortgroupref)
+				{
+					result = bms_add_member(result, i);
+					match = true;
+					break;
+				}
+			}
+		}
+
+		if (match)
+		{
+			i++;
+			continue;
+		}
+
+		/*
+		 * If sortgroupref didn't help, we need to find the exact expression.
+		 */
+		j = 0;
+		foreach(l2, target->exprs)
+		{
+			Expr	   *expr = (Expr *) lfirst(l2);
+
+			if (equal(expr, unique_expr))
+			{
+				result = bms_add_member(result, i);
+				match = true;
+				break;
+			}
+
+			j++;
+		}
+
+		/*
+		 * We can't construct uniquekeys for the join.
+		 */
+		if (!match)
+			return NULL;
+
+		i++;
+	}
+
+	return result;
+}
+
+void
+free_uniquekeys(List *uniquekeys)
+{
+	ListCell   *lc;
+
+	foreach(lc, uniquekeys)
+		bms_free((Bitmapset *) lc);
+	list_free(uniquekeys);
+}
+
+/*
+ * Create a set of positions of expressions in reltarget if the index is
+ * unique and if reltarget contains all the index columns. Add the set to
+ * uniquekeys if identical one is not already there.
+ */
+static void
+make_uniquekeys_for_unique_index(PathTarget *reltarget, IndexOptInfo *index,
+								 Path *path)
+{
+	int			i;
+	Bitmapset  *new_set = NULL;
+
+	/*
+	 * Give up if the index does not guarantee uniqueness.
+	 */
+	if (!index->unique || !index->immediate ||
+		(index->indpred != NIL && !index->predOK))
+		return;
+
+	/*
+	 * For the index path to be acceptable, reltarget must contain all the
+	 * index columns.
+	 *
+	 * reltarget is not supposed to contain non-var expressions, so the index
+	 * should neither.
+	 */
+	if (index->indexprs != NULL)
+		return;
+
+	for (i = 0; i < index->ncolumns; i++)
+	{
+		int			indkey = index->indexkeys[i];
+		ListCell   *lc;
+		bool		found = false;
+		int			j = 0;
+
+		foreach(lc, reltarget->exprs)
+		{
+			Var		   *var = lfirst_node(Var, lc);
+
+			if (var->varno == index->rel->relid && var->varattno == indkey)
+			{
+				new_set = bms_add_member(new_set, j);
+				found = true;
+				break;
+			}
+
+			j++;
+		}
+
+		/*
+		 * If rel needs less than the whole index key then the values of the
+		 * columns matched so far can be duplicate.
+		 */
+		if (!found)
+		{
+			bms_free(new_set);
+			return;
+		}
+	}
+
+	/*
+	 * Add the set to the path, unless it's already there.
+	 */
+	add_uniquekeys(&path->uniquekeys, new_set);
+}
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index c69740eda6..120dd4ab49 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -17,6 +17,7 @@
 #include <limits.h>
 
 #include "miscadmin.h"
+#include "catalog/pg_constraint.h"
 #include "optimizer/clauses.h"
 #include "optimizer/cost.h"
 #include "optimizer/pathnode.h"
@@ -26,6 +27,8 @@
 #include "optimizer/prep.h"
 #include "optimizer/restrictinfo.h"
 #include "optimizer/tlist.h"
+#include "optimizer/var.h"
+#include "parser/parse_oper.h"
 #include "partitioning/partbounds.h"
 #include "utils/hsearch.h"
 
@@ -57,6 +60,9 @@ static void add_join_rel(PlannerInfo *root, RelOptInfo *joinrel);
 static void build_joinrel_partition_info(RelOptInfo *joinrel,
 							 RelOptInfo *outer_rel, RelOptInfo *inner_rel,
 							 List *restrictlist, JoinType jointype);
+static bool init_grouping_targets(PlannerInfo *root, RelOptInfo *rel,
+					  PathTarget *target, PathTarget *agg_input,
+					  List *gvis);
 
 
 /*
@@ -72,7 +78,10 @@ setup_simple_rel_arrays(PlannerInfo *root)
 	/* Arrays are accessed using RT indexes (1..N) */
 	root->simple_rel_array_size = list_length(root->parse->rtable) + 1;
 
-	/* simple_rel_array is initialized to all NULLs */
+	/*
+	 * simple_rel_array / simple_grouped_rel_array are both initialized to all
+	 * NULLs
+	 */
 	root->simple_rel_array = (RelOptInfo **)
 		palloc0(root->simple_rel_array_size * sizeof(RelOptInfo *));
 
@@ -148,7 +157,14 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 	rel->reloptkind = parent ? RELOPT_OTHER_MEMBER_REL : RELOPT_BASEREL;
 	rel->relids = bms_make_singleton(relid);
 	rel->rows = 0;
-	/* cheap startup cost is interesting iff not all tuples to be retrieved */
+
+	/*
+	 * Cheap startup cost is interesting iff not all tuples to be retrieved.
+	 * XXX As for grouped relation, the startup cost might be interesting for
+	 * AGG_SORTED (if it can produce the ordering that matches
+	 * root->query_pathkeys) but not in general (other kinds of aggregation
+	 * need the whole relation). Yet it seems worth trying.
+	 */
 	rel->consider_startup = (root->tuple_fraction > 0);
 	rel->consider_param_startup = false;	/* might get changed later */
 	rel->consider_parallel = false; /* might get changed later */
@@ -162,6 +178,8 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 	rel->cheapest_parameterized_paths = NIL;
 	rel->direct_lateral_relids = NULL;
 	rel->lateral_relids = NULL;
+	rel->agg_info = NULL;
+	rel->grouped = NULL;
 	rel->relid = relid;
 	rel->rtekind = rte->rtekind;
 	/* min_attr, max_attr, attr_needed, attr_widths are set below */
@@ -380,13 +398,23 @@ build_join_rel_hash(PlannerInfo *root)
 RelOptInfo *
 find_join_rel(PlannerInfo *root, Relids relids)
 {
+	HTAB	   *join_rel_hash;
+	List	   *join_rel_list;
+
+	join_rel_hash = root->join_rel_hash;
+	join_rel_list = root->join_rel_list;
+
 	/*
 	 * Switch to using hash lookup when list grows "too long".  The threshold
 	 * is arbitrary and is known only here.
 	 */
-	if (!root->join_rel_hash && list_length(root->join_rel_list) > 32)
+	if (!join_rel_hash && list_length(join_rel_list) > 32)
+	{
 		build_join_rel_hash(root);
 
+		join_rel_hash = root->join_rel_hash;
+	}
+
 	/*
 	 * Use either hashtable lookup or linear search, as appropriate.
 	 *
@@ -395,12 +423,12 @@ find_join_rel(PlannerInfo *root, Relids relids)
 	 * so would force relids out of a register and thus probably slow down the
 	 * list-search case.
 	 */
-	if (root->join_rel_hash)
+	if (join_rel_hash)
 	{
 		Relids		hashkey = relids;
 		JoinHashEntry *hentry;
 
-		hentry = (JoinHashEntry *) hash_search(root->join_rel_hash,
+		hentry = (JoinHashEntry *) hash_search(join_rel_hash,
 											   &hashkey,
 											   HASH_FIND,
 											   NULL);
@@ -411,7 +439,7 @@ find_join_rel(PlannerInfo *root, Relids relids)
 	{
 		ListCell   *l;
 
-		foreach(l, root->join_rel_list)
+		foreach(l, join_rel_list)
 		{
 			RelOptInfo *rel = (RelOptInfo *) lfirst(l);
 
@@ -481,7 +509,9 @@ set_foreign_rel_properties(RelOptInfo *joinrel, RelOptInfo *outer_rel,
 static void
 add_join_rel(PlannerInfo *root, RelOptInfo *joinrel)
 {
-	/* GEQO requires us to append the new joinrel to the end of the list! */
+	/*
+	 * GEQO requires us to append the new joinrel to the end of the list!
+	 */
 	root->join_rel_list = lappend(root->join_rel_list, joinrel);
 
 	/* store it into the auxiliary hashtable if there is one. */
@@ -511,6 +541,9 @@ add_join_rel(PlannerInfo *root, RelOptInfo *joinrel)
  * 'restrictlist_ptr': result variable.  If not NULL, *restrictlist_ptr
  *		receives the list of RestrictInfo nodes that apply to this
  *		particular pair of joinable relations.
+ * 'grouped' forces creation of a "standalone" object, i.e.  w/o search in the
+ *		join list and without adding the result to the list. Caller is
+ *		responsible for setup of reltarget in such a case.
  *
  * restrictlist_ptr makes the routine's API a little grotty, but it saves
  * duplicated calculation of the restrictlist...
@@ -521,10 +554,12 @@ build_join_rel(PlannerInfo *root,
 			   RelOptInfo *outer_rel,
 			   RelOptInfo *inner_rel,
 			   SpecialJoinInfo *sjinfo,
-			   List **restrictlist_ptr)
+			   List **restrictlist_ptr,
+			   bool grouped)
 {
-	RelOptInfo *joinrel;
+	RelOptInfo *joinrel = NULL;
 	List	   *restrictlist;
+	bool		create_target = !grouped;
 
 	/* This function should be used only for join between parents. */
 	Assert(!IS_OTHER_REL(outer_rel) && !IS_OTHER_REL(inner_rel));
@@ -532,7 +567,8 @@ build_join_rel(PlannerInfo *root,
 	/*
 	 * See if we already have a joinrel for this set of base rels.
 	 */
-	joinrel = find_join_rel(root, joinrelids);
+	if (!grouped)
+		joinrel = find_join_rel(root, joinrelids);
 
 	if (joinrel)
 	{
@@ -555,11 +591,11 @@ build_join_rel(PlannerInfo *root,
 	joinrel->reloptkind = RELOPT_JOINREL;
 	joinrel->relids = bms_copy(joinrelids);
 	joinrel->rows = 0;
-	/* cheap startup cost is interesting iff not all tuples to be retrieved */
+	/* See the comment in build_simple_rel(). */
 	joinrel->consider_startup = (root->tuple_fraction > 0);
 	joinrel->consider_param_startup = false;
 	joinrel->consider_parallel = false;
-	joinrel->reltarget = create_empty_pathtarget();
+	joinrel->reltarget = NULL;
 	joinrel->pathlist = NIL;
 	joinrel->ppilist = NIL;
 	joinrel->partial_pathlist = NIL;
@@ -573,6 +609,8 @@ build_join_rel(PlannerInfo *root,
 				  inner_rel->direct_lateral_relids);
 	joinrel->lateral_relids = min_join_parameterization(root, joinrel->relids,
 														outer_rel, inner_rel);
+	joinrel->agg_info = NULL;
+	joinrel->grouped = NULL;
 	joinrel->relid = 0;			/* indicates not a baserel */
 	joinrel->rtekind = RTE_JOIN;
 	joinrel->min_attr = 0;
@@ -623,9 +661,13 @@ build_join_rel(PlannerInfo *root,
 	 * and inner rels we first try to build it from.  But the contents should
 	 * be the same regardless.
 	 */
-	build_joinrel_tlist(root, joinrel, outer_rel);
-	build_joinrel_tlist(root, joinrel, inner_rel);
-	add_placeholders_to_joinrel(root, joinrel, outer_rel, inner_rel);
+	if (create_target)
+	{
+		joinrel->reltarget = create_empty_pathtarget();
+		build_joinrel_tlist(root, joinrel, outer_rel);
+		build_joinrel_tlist(root, joinrel, inner_rel);
+		add_placeholders_to_joinrel(root, joinrel, outer_rel, inner_rel);
+	}
 
 	/*
 	 * add_placeholders_to_joinrel also took care of adding the ph_lateral
@@ -662,31 +704,39 @@ build_join_rel(PlannerInfo *root,
 
 	/*
 	 * Set estimates of the joinrel's size.
-	 */
-	set_joinrel_size_estimates(root, joinrel, outer_rel, inner_rel,
-							   sjinfo, restrictlist);
-
-	/*
-	 * Set the consider_parallel flag if this joinrel could potentially be
-	 * scanned within a parallel worker.  If this flag is false for either
-	 * inner_rel or outer_rel, then it must be false for the joinrel also.
-	 * Even if both are true, there might be parallel-restricted expressions
-	 * in the targetlist or quals.
 	 *
-	 * Note that if there are more than two rels in this relation, they could
-	 * be divided between inner_rel and outer_rel in any arbitrary way.  We
-	 * assume this doesn't matter, because we should hit all the same baserels
-	 * and joinclauses while building up to this joinrel no matter which we
-	 * take; therefore, we should make the same decision here however we get
-	 * here.
+	 * XXX The function claims to need reltarget but it does not seem to
+	 * actually use it. Should we call it unconditionally so that callers of
+	 * build_join_rel() do not have to care?
 	 */
-	if (inner_rel->consider_parallel && outer_rel->consider_parallel &&
-		is_parallel_safe(root, (Node *) restrictlist) &&
-		is_parallel_safe(root, (Node *) joinrel->reltarget->exprs))
-		joinrel->consider_parallel = true;
+	if (create_target)
+	{
+		set_joinrel_size_estimates(root, joinrel, outer_rel, inner_rel,
+								   sjinfo, restrictlist);
+
+		/*
+		 * Set the consider_parallel flag if this joinrel could potentially be
+		 * scanned within a parallel worker.  If this flag is false for either
+		 * inner_rel or outer_rel, then it must be false for the joinrel also.
+		 * Even if both are true, there might be parallel-restricted
+		 * expressions in the targetlist or quals.
+		 *
+		 * Note that if there are more than two rels in this relation, they
+		 * could be divided between inner_rel and outer_rel in any arbitrary
+		 * way.  We assume this doesn't matter, because we should hit all the
+		 * same baserels and joinclauses while building up to this joinrel no
+		 * matter which we take; therefore, we should make the same decision
+		 * here however we get here.
+		 */
+		if (inner_rel->consider_parallel && outer_rel->consider_parallel &&
+			is_parallel_safe(root, (Node *) restrictlist) &&
+			is_parallel_safe(root, (Node *) joinrel->reltarget->exprs))
+			joinrel->consider_parallel = true;
+	}
 
 	/* Add the joinrel to the PlannerInfo. */
-	add_join_rel(root, joinrel);
+	if (!grouped)
+		add_join_rel(root, joinrel);
 
 	/*
 	 * Also, if dynamic-programming join search is active, add the new joinrel
@@ -694,7 +744,7 @@ build_join_rel(PlannerInfo *root,
 	 * of members should be for equality, but some of the level 1 rels might
 	 * have been joinrels already, so we can only assert <=.
 	 */
-	if (root->join_rel_level)
+	if (root->join_rel_level && !grouped)
 	{
 		Assert(root->join_cur_level > 0);
 		Assert(root->join_cur_level <= bms_num_members(joinrel->relids));
@@ -718,16 +768,19 @@ build_join_rel(PlannerInfo *root,
  * 'restrictlist': list of RestrictInfo nodes that apply to this particular
  *		pair of joinable relations
  * 'jointype' is the join type (inner, left, full, etc)
+ * 'grouped': does the join contain partial aggregate? (If it does, then
+ * caller is responsible for setup of reltarget.)
  */
 RelOptInfo *
 build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 					 RelOptInfo *inner_rel, RelOptInfo *parent_joinrel,
 					 List *restrictlist, SpecialJoinInfo *sjinfo,
-					 JoinType jointype)
+					 JoinType jointype, bool grouped)
 {
 	RelOptInfo *joinrel = makeNode(RelOptInfo);
 	AppendRelInfo **appinfos;
 	int			nappinfos;
+	bool		create_target = !grouped;
 
 	/* Only joins between "other" relations land here. */
 	Assert(IS_OTHER_REL(outer_rel) && IS_OTHER_REL(inner_rel));
@@ -735,11 +788,11 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 	joinrel->reloptkind = RELOPT_OTHER_JOINREL;
 	joinrel->relids = bms_union(outer_rel->relids, inner_rel->relids);
 	joinrel->rows = 0;
-	/* cheap startup cost is interesting iff not all tuples to be retrieved */
+	/* See the comment in build_simple_rel(). */
 	joinrel->consider_startup = (root->tuple_fraction > 0);
 	joinrel->consider_param_startup = false;
 	joinrel->consider_parallel = false;
-	joinrel->reltarget = create_empty_pathtarget();
+	joinrel->reltarget = NULL;
 	joinrel->pathlist = NIL;
 	joinrel->ppilist = NIL;
 	joinrel->partial_pathlist = NIL;
@@ -749,6 +802,8 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 	joinrel->cheapest_parameterized_paths = NIL;
 	joinrel->direct_lateral_relids = NULL;
 	joinrel->lateral_relids = NULL;
+	joinrel->agg_info = NULL;
+	joinrel->grouped = NULL;
 	joinrel->relid = 0;			/* indicates not a baserel */
 	joinrel->rtekind = RTE_JOIN;
 	joinrel->min_attr = 0;
@@ -789,11 +844,15 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 	/* Compute information relevant to foreign relations. */
 	set_foreign_rel_properties(joinrel, outer_rel, inner_rel);
 
-	/* Build targetlist */
-	build_joinrel_tlist(root, joinrel, outer_rel);
-	build_joinrel_tlist(root, joinrel, inner_rel);
-	/* Add placeholder variables. */
-	add_placeholders_to_child_joinrel(root, joinrel, parent_joinrel);
+	if (create_target)
+	{
+		/* Build targetlist */
+		joinrel->reltarget = create_empty_pathtarget();
+		build_joinrel_tlist(root, joinrel, outer_rel);
+		build_joinrel_tlist(root, joinrel, inner_rel);
+		/* Add placeholder variables. */
+		add_placeholders_to_child_joinrel(root, joinrel, parent_joinrel);
+	}
 
 	/* Construct joininfo list. */
 	appinfos = find_appinfos_by_relids(root, joinrel->relids, &nappinfos);
@@ -801,7 +860,6 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 														(Node *) parent_joinrel->joininfo,
 														nappinfos,
 														appinfos);
-	pfree(appinfos);
 
 	/*
 	 * Lateral relids referred in child join will be same as that referred in
@@ -828,14 +886,22 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 
 
 	/* Set estimates of the child-joinrel's size. */
-	set_joinrel_size_estimates(root, joinrel, outer_rel, inner_rel,
-							   sjinfo, restrictlist);
+	/* XXX See the corresponding comment in build_join_rel(). */
+	if (create_target)
+		set_joinrel_size_estimates(root, joinrel, outer_rel, inner_rel,
+								   sjinfo, restrictlist);
 
-	/* We build the join only once. */
-	Assert(!find_join_rel(root, joinrel->relids));
+	/*
+	 * We build the join only once. (Grouped joins should not exist in the
+	 * list.)
+	 */
+	Assert(!find_join_rel(root, joinrel->relids) || grouped);
 
 	/* Add the relation to the PlannerInfo. */
-	add_join_rel(root, joinrel);
+	if (!grouped)
+		add_join_rel(root, joinrel);
+
+	pfree(appinfos);
 
 	return joinrel;
 }
@@ -1768,3 +1834,728 @@ build_joinrel_partition_info(RelOptInfo *joinrel, RelOptInfo *outer_rel,
 		joinrel->nullable_partexprs[cnt] = nullable_partexpr;
 	}
 }
+
+/*
+ * Check if the relation can produce grouped paths and return the information
+ * it'll need for it. The passed relation is the non-grouped one which has the
+ * reltarget already constructed.
+ */
+RelAggInfo *
+create_rel_agg_info(PlannerInfo *root, RelOptInfo *rel)
+{
+	List	   *gvis;
+	List	   *aggregates = NIL;
+	List	   *grp_exprs = NIL;
+	bool		found_other_rel_agg;
+	ListCell   *lc;
+	RelAggInfo *result;
+	PathTarget *agg_input;
+	PathTarget *target = NULL;
+	int			i;
+	Bitmapset  *sgr_set = NULL;
+	Bitmapset  *sgr_query_set = NULL;
+
+	/*
+	 * The function shouldn't have been called if there's no opportunity for
+	 * aggregation push-down.
+	 */
+	Assert(root->grouped_var_list != NIL);
+
+	/*
+	 * The source relation has nothing to do with grouping.
+	 */
+	Assert(rel->agg_info == NULL);
+
+	/*
+	 * The current implementation of aggregation push-down cannot handle
+	 * PlaceHolderVar (PHV).
+	 *
+	 * If we knew that the PHV should be evaluated in this target (and of
+	 * course, if its expression matched some grouping expression or Aggref
+	 * argument), we'd just let init_grouping_targets create GroupedVar for
+	 * the corresponding expression (phexpr). On the other hand, if we knew
+	 * that the PHV is evaluated below the current rel, we'd ignore it because
+	 * the referencing GroupedVar would take care of propagation of the value
+	 * to upper joins. (PHV whose ph_eval_at is above the current rel make the
+	 * aggregation push-down impossible in any case because the partial
+	 * aggregation would receive wrong input if we ignored the ph_eval_at.)
+	 *
+	 * The problem is that the same PHV can be evaluated in the target of the
+	 * current rel or in that of lower rel --- depending on the input paths.
+	 * For example, consider rel->relids = {A, B, C} and if ph_eval_at = {B,
+	 * C}. Path "A JOIN (B JOIN C)" implies that the PHV is evaluated by the
+	 * "(B JOIN C)", while path "(A JOIN B) JOIN C" evaluates the PHV itself.
+	 */
+	foreach(lc, rel->reltarget->exprs)
+	{
+		Expr	   *expr = lfirst(lc);
+
+		if (IsA(expr, PlaceHolderVar))
+			return NULL;
+	}
+
+	if (IS_SIMPLE_REL(rel))
+	{
+		RangeTblEntry *rte = root->simple_rte_array[rel->relid];;
+
+		/*
+		 * rtekind != RTE_RELATION case is not supported yet.
+		 */
+		if (rte->rtekind != RTE_RELATION)
+			return NULL;
+	}
+
+	/* Caller should only pass base relations or joins. */
+	Assert(rel->reloptkind == RELOPT_BASEREL ||
+		   rel->reloptkind == RELOPT_JOINREL);
+
+	/*
+	 * If any outer join can set the attribute value to NULL, the Agg plan
+	 * would receive different input at the base rel level.
+	 *
+	 * XXX For RELOPT_JOINREL, do not return if all the joins that can set any
+	 * entry of the grouped target (do we need to postpone this check until
+	 * the grouped target is available, and init_grouping_targets take care?)
+	 * of this rel to NULL are provably below rel. (It's ok if rel is one of
+	 * these joins.)
+	 */
+	if (bms_overlap(rel->relids, root->nullable_baserels))
+		return NULL;
+
+	/*
+	 * Use equivalence classes to generate additional grouping expressions for
+	 * the current rel. Without these we might not be able to apply
+	 * aggregation to the relation result set.
+	 *
+	 * It's important that create_grouping_expr_grouped_var_infos has
+	 * processed the explicit grouping columns by now. If the grouping clause
+	 * contains multiple expressions belonging to the same EC, the original
+	 * (i.e. not derived) one should be preferred when we build grouping
+	 * target for a relation. Otherwise we have a problem when trying to match
+	 * target entries to grouping clauses during plan creation, see
+	 * get_grouping_expression().
+	 */
+	gvis = list_copy(root->grouped_var_list);
+	foreach(lc, root->grouped_var_list)
+	{
+		GroupedVarInfo *gvi = lfirst_node(GroupedVarInfo, lc);
+		int			relid = -1;
+
+		/* Only interested in grouping expressions. */
+		if (IsA(gvi->gvexpr, Aggref))
+			continue;
+
+		while ((relid = bms_next_member(rel->relids, relid)) >= 0)
+		{
+			GroupedVarInfo *gvi_trans;
+
+			gvi_trans = translate_expression_to_rels(root, gvi, relid);
+			if (gvi_trans != NULL)
+				gvis = lappend(gvis, gvi_trans);
+		}
+	}
+
+	/*
+	 * Check if some aggregates or grouping expressions can be evaluated in
+	 * this relation's target, and collect all vars referenced by these
+	 * aggregates / grouping expressions;
+	 */
+	found_other_rel_agg = false;
+	foreach(lc, gvis)
+	{
+		GroupedVarInfo *gvi = lfirst_node(GroupedVarInfo, lc);
+
+		/*
+		 * The subset includes gv_eval_at uninitialized, which includes
+		 * Aggref.aggstar.
+		 */
+		if (bms_is_subset(gvi->gv_eval_at, rel->relids))
+		{
+			/*
+			 * init_grouping_targets will handle plain Var grouping
+			 * expressions because it needs to look them up in
+			 * grouped_var_list anyway.
+			 *
+			 * XXX A plain Var could actually be handled w/o GroupedVar, but
+			 * thus init_grouping_targets would have to spend extra effort
+			 * looking for the EC-related vars, instead of relying on
+			 * create_grouping_expr_grouped_var_infos. (Processing of
+			 * particular expression would look different, so we could hardly
+			 * reuse the same piece of code.)
+			 */
+			if (IsA(gvi->gvexpr, Var))
+				continue;
+
+			/*
+			 * The derived grouping expressions should not be referenced by
+			 * the query targetlist, so do not add them if we're at the top of
+			 * the join tree.
+			 */
+			if (gvi->derived && bms_equal(rel->relids, root->all_baserels))
+				continue;
+
+			/*
+			 * Accept the aggregate / grouping expression.
+			 *
+			 * (GroupedVarInfo is more convenient for the next processing than
+			 * Aggref, see add_aggregates_to_grouped_target.)
+			 */
+			if (IsA(gvi->gvexpr, Aggref))
+				aggregates = lappend(aggregates, gvi);
+			else
+				grp_exprs = lappend(grp_exprs, gvi);
+		}
+		else if (IsA(gvi->gvexpr, Aggref))
+		{
+			/*
+			 * Remember that there is at least one aggregate expression that
+			 * needs something else than this rel.
+			 */
+			found_other_rel_agg = true;
+
+			/*
+			 * This condition effectively terminates creation of the
+			 * RelAggInfo, so there's no reason to check the next
+			 * GroupedVarInfo.
+			 */
+			break;
+		}
+	}
+
+	/*
+	 * Grouping makes little sense w/o aggregate function and w/o grouping
+	 * expressions.
+	 *
+	 * In contrast, grp_exprs is only supposed to contain generic grouping
+	 * expression, so it can be NIL so far. If all the grouping keys are just
+	 * plain Vars, init_grouping_targets will take care of them.
+	 */
+	if (aggregates == NIL)
+	{
+		list_free(gvis);
+		return NULL;
+	}
+
+	/*
+	 * Give up if some other aggregate(s) need relations other than the
+	 * current one.
+	 *
+	 * If the aggregate needs the current rel plus anything else, then the
+	 * problem is that grouping of the current relation could make some input
+	 * variables unavailable for the "higher aggregate", and it'd also
+	 * decrease the number of input rows the "higher aggregate" receives.
+	 *
+	 * If the aggregate does not even need the current rel, then neither the
+	 * current rel nor anything else should be grouped because we do not
+	 * support join of two grouped relations.
+	 */
+	if (found_other_rel_agg)
+	{
+		list_free(gvis);
+		return NULL;
+	}
+
+	/*
+	 * Create target for grouped paths as well as one for the input paths of
+	 * the aggregation paths.
+	 */
+	target = create_empty_pathtarget();
+	agg_input = create_empty_pathtarget();
+
+	/*
+	 * Cannot suitable targets for the aggregation push-down be derived?
+	 */
+	if (!init_grouping_targets(root, rel, target, agg_input, gvis))
+	{
+		list_free(gvis);
+		return NULL;
+	}
+
+	list_free(gvis);
+
+	/*
+	 * Add (non-Var) grouping expressions (in the form of GroupedVar) to
+	 * target_agg.
+	 *
+	 * Follow the convention that the grouping expressions should precede
+	 * aggregates.
+	 */
+	add_grouped_vars_to_target(root, target, grp_exprs);
+
+	/*
+	 * Aggregation push-down makes no sense w/o grouping expressions.
+	 */
+	if (list_length(target->exprs) == 0)
+		return NULL;
+
+	/*
+	 * Add aggregates (in the form of GroupedVar) to the grouping target.
+	 */
+	add_grouped_vars_to_target(root, target, aggregates);
+
+	/*
+	 * Make sure that the paths generating input data for partial aggregation
+	 * include non-Var grouping expressions.
+	 *
+	 * TODO Shouldn't GroupedVar be added instead?
+	 */
+	foreach(lc, grp_exprs)
+	{
+		GroupedVarInfo *gvi;
+
+		gvi = lfirst_node(GroupedVarInfo, lc);
+		add_column_to_pathtarget(agg_input, gvi->gvexpr, gvi->sortgroupref);
+	}
+
+	/*
+	 * Since neither target nor agg_input is supposed to be identical to the
+	 * source reltarget, compute the width and cost again.
+	 */
+	set_pathtarget_cost_width(root, target);
+	set_pathtarget_cost_width(root, agg_input);
+
+	/*
+	 * Check if target for 1-stage aggregation can be setup.
+	 *
+	 * For this to work, the relation grouping target must contain all the
+	 * grouping expressions of the query. (Again, there's no final aggregation
+	 * that ensures utilization of all the grouping expressions.)
+	 *
+	 * TODO Take into account the fact that grouping expressions can be
+	 * derived using ECs, so a single expression of the target can correspond
+	 * to multiple expressions of the query target. (There's no reason to put
+	 * GROUP BY usually multiple EC members in the GROUP BY clause, but if
+	 * user does, it should not disable aggregation push-down.)
+	 *
+	 * First, collect sortgrouprefs of the relation target.
+	 */
+	i = 0;
+	foreach(lc, target->exprs)
+	{
+		Index		sortgroupref = 0;
+
+		Assert(target->sortgrouprefs != NULL);
+		sortgroupref = target->sortgrouprefs[i++];
+		if (sortgroupref > 0)
+			sgr_set = bms_add_member(sgr_set, sortgroupref);
+	}
+
+	/*
+	 * Collect those of the query.
+	 */
+	foreach(lc, root->processed_tlist)
+	{
+		TargetEntry *te = lfirst_node(TargetEntry, lc);
+		ListCell   *lc2;
+
+		if (te->ressortgroupref > 0)
+		{
+			bool		accept = false;
+
+			/*
+			 * Ignore target entries that contain aggregates.
+			 */
+			if (IsA(te->expr, Var))
+				accept = true;
+			else
+			{
+				List	   *l = pull_var_clause((Node *) te->expr,
+												PVC_INCLUDE_AGGREGATES);
+
+				foreach(lc2, l)
+				{
+					Expr	   *expr = lfirst(lc2);
+
+					if (IsA(expr, Aggref))
+						break;
+				}
+
+				/*
+				 * Accept the target entry if no Aggref was found.
+				 */
+				if (lc2 == NULL)
+					accept = true;
+				list_free(l);
+			}
+			if (accept)
+				sgr_query_set = bms_add_member(sgr_query_set,
+											   te->ressortgroupref);
+		}
+	}
+
+	/*
+	 * No grouping at this relation if the the check failed.
+	 */
+	if (!bms_equal(sgr_set, sgr_query_set))
+		return NULL;
+
+	result = makeNode(RelAggInfo);
+	result->target = target;
+	result->input = agg_input;
+
+	/*
+	 * Build a list of grouping expressions and a list of the corresponding
+	 * SortGroupClauses.
+	 */
+	i = 0;
+	foreach(lc, target->exprs)
+	{
+		Index		sortgroupref = 0;
+		SortGroupClause *cl;
+		Expr	   *texpr;
+
+		texpr = (Expr *) lfirst(lc);
+
+		if (IsA(texpr, GroupedVar) &&
+			IsA(((GroupedVar *) texpr)->gvexpr, Aggref))
+		{
+			/*
+			 * texpr should represent the first aggregate in the targetlist.
+			 */
+			break;
+		}
+
+		/*
+		 * Find the clause by sortgroupref.
+		 */
+		sortgroupref = target->sortgrouprefs[i++];
+
+		/*
+		 * Besides being an aggregate, the target expression should have no
+		 * other reason then being a column of a relation functionally
+		 * dependent on the GROUP BY clause. So it's not actually a grouping
+		 * column.
+		 */
+		if (sortgroupref == 0)
+			continue;
+
+		cl = get_sortgroupref_clause(sortgroupref,
+									 root->parse->groupClause);
+
+
+		result->group_clauses = list_append_unique(result->group_clauses,
+												   cl);
+
+		/*
+		 * Add only unique clauses because of joins (both sides of a join can
+		 * point at the same grouping clause). XXX Is it worth adding a bool
+		 * argument indicating that we're dealing with join right now?
+		 */
+		result->group_exprs = list_append_unique(result->group_exprs,
+												 texpr);
+	}
+
+	/* Finally collect the aggregates. */
+	while (lc != NULL)
+	{
+		GroupedVar *gvar = castNode(GroupedVar, lfirst(lc));
+
+		Assert(IsA(gvar->gvexpr, Aggref));
+		result->agg_exprs = lappend(result->agg_exprs,
+									gvar->gvexpr);
+
+		lc = lnext(lc);
+	}
+
+	return result;
+}
+
+/*
+ * Initialize target for grouped paths (target) as well as a target for paths
+ * that generate input for partial aggregation (agg_input).
+ *
+ * gvis a list of GroupedVarInfo's possibly useful for rel.
+ *
+ * Return true iff the targets could be initialized.
+ */
+static bool
+init_grouping_targets(PlannerInfo *root, RelOptInfo *rel,
+					  PathTarget *target, PathTarget *agg_input,
+					  List *gvis)
+{
+	ListCell   *lc;
+	List	   *unresolved = NIL;
+
+	foreach(lc, rel->reltarget->exprs)
+	{
+		Var		   *tvar;
+		GroupedVar *gvar;
+		Expr	   *expr_unresolved;
+		bool		derived = false;
+		ListCell   *lc2;
+		bool		needed_by_aggregate;
+
+		/*
+		 * Given that PlaceHolderVar currently prevents us from doing
+		 * aggregation push-down, the source target cannot contain anything
+		 * more complex than a Var. (As for generic grouping expressions,
+		 * add_grouped_vars_to_target will retrieve them from the query
+		 * targetlist and add them to "target" outside this function.)
+		 */
+		tvar = lfirst_node(Var, lc);
+
+		gvar = get_grouping_expression(gvis, (Expr *) tvar, &derived);
+
+		/*
+		 * Derived grouping expressions should not be referenced by the query
+		 * targetlist, so let them fall into vars_unresolved. It'll be checked
+		 * later if the current targetlist needs them.
+		 */
+		if (gvar != NULL && !derived)
+		{
+			/*
+			 * It's o.k. to use the target expression for grouping.
+			 *
+			 * The actual Var is added to the target. If we used the
+			 * containing GroupedVar, references from various clauses (e.g.
+			 * join quals) wouldn't work.
+			 */
+			add_column_to_pathtarget(target, gvar->gvexpr,
+									 gvar->sortgroupref);
+
+			/*
+			 * As for agg_input, add the original expression but set
+			 * sortgroupref in addition.
+			 */
+			add_column_to_pathtarget(agg_input, gvar->gvexpr,
+									 gvar->sortgroupref);
+
+			/* Process the next expression. */
+			continue;
+		}
+
+		/*
+		 * Is this Var needed in the query targetlist for anything else than
+		 * aggregate input?
+		 */
+		needed_by_aggregate = false;
+		foreach(lc2, root->grouped_var_list)
+		{
+			GroupedVarInfo *gvi = lfirst_node(GroupedVarInfo, lc2);
+			ListCell   *lc3;
+			List	   *vars;
+
+			if (!IsA(gvi->gvexpr, Aggref))
+				continue;
+
+			if (!bms_is_member(tvar->varno, gvi->gv_eval_at))
+				continue;
+
+			/*
+			 * XXX Consider some sort of caching.
+			 */
+			vars = pull_var_clause((Node *) gvi->gvexpr, PVC_RECURSE_AGGREGATES);
+			foreach(lc3, vars)
+			{
+				Var		   *var = lfirst_node(Var, lc3);
+
+				if (equal(var, tvar))
+				{
+					needed_by_aggregate = true;
+					break;
+				}
+			}
+			list_free(vars);
+			if (needed_by_aggregate)
+				break;
+		}
+
+		if (needed_by_aggregate)
+		{
+			bool		found = false;
+
+			foreach(lc2, root->processed_tlist)
+			{
+				TargetEntry *te = lfirst_node(TargetEntry, lc2);
+
+				if (IsA(te->expr, Aggref))
+					continue;
+
+				/*
+				 * Match tvar only to plain Vars in the targetlist.
+				 *
+				 * In contrast, occurrence of tvar in a generic (grouping)
+				 * expressions is not a reason to add tvar to vars_unresolved
+				 * and eventually to the grouping target because the generic
+				 * grouping expressions should already have their GroupedVars
+				 * created and those should be added to the grouping target
+				 * separate.
+				 */
+				if (equal(te->expr, tvar))
+				{
+					found = true;
+					break;
+				}
+			}
+
+			/*
+			 * If it's only Aggref input, add it to the aggregation input
+			 * target and that's it.
+			 */
+			if (!found)
+			{
+				add_new_column_to_pathtarget(agg_input, (Expr *) tvar);
+				continue;
+			}
+		}
+
+		if (gvar != NULL)
+		{
+			Assert(derived);
+
+			/*
+			 * Use the whole GroupedVar as it contains sortgroupref.
+			 */
+			expr_unresolved = (Expr *) gvar;
+		}
+		else
+			expr_unresolved = (Expr *) tvar;
+
+		/*
+		 * Further investigation involves dependency check, for which we need
+		 * to have all the plain-var grouping expressions gathered.
+		 */
+		unresolved = lappend(unresolved, expr_unresolved);
+	}
+
+	/*
+	 * Check for other possible reasons for the var to be in the plain target.
+	 */
+	foreach(lc, unresolved)
+	{
+		Expr	   *unresolved_expr = (Expr *) lfirst(lc);
+		Var		   *var;
+		GroupedVar *gvar = NULL;
+		Index		sortgroupref;
+		RangeTblEntry *rte;
+		List	   *deps = NIL;
+		Relids		relids_subtract;
+		int			ndx;
+		RelOptInfo *baserel;
+
+		if (IsA(unresolved_expr, Var))
+		{
+			var = castNode(Var, unresolved_expr);
+			sortgroupref = 0;
+		}
+		else
+		{
+			gvar = castNode(GroupedVar, unresolved_expr);
+			var = castNode(Var, gvar->gvexpr);
+			sortgroupref = gvar->sortgroupref;
+			Assert(sortgroupref > 0);
+		}
+
+		rte = root->simple_rte_array[var->varno];
+
+		/*
+		 * Check if the Var can be in the grouping key even though it's not
+		 * mentioned by the GROUP BY clause (and could not be derived using
+		 * ECs).
+		 */
+		if (sortgroupref == 0 &&
+			check_functional_grouping(rte->relid, var->varno,
+									  var->varlevelsup,
+									  target->exprs, &deps))
+		{
+			/*
+			 * The var shouldn't be actually used as a grouping key (instead,
+			 * the one this depends on will be), so sortgroupref should not be
+			 * important.
+			 */
+			add_new_column_to_pathtarget(target, (Expr *) var);
+			add_new_column_to_pathtarget(agg_input, (Expr *) var);
+
+			/*
+			 * The var may or may not be present in generic grouping
+			 * expression(s) in addition, but this is handled elsewhere.
+			 */
+			continue;
+		}
+
+		/*
+		 * Isn't the expression needed by joins above the current rel?
+		 *
+		 * The relids we're not interested in do include 0, which is the
+		 * top-level targetlist. The only reason for relids to contain 0
+		 * should be that arg_var is referenced either by aggregate or by
+		 * grouping expression, but right now we're interested in the *other*
+		 * reasons. (As soon as GroupedVars are installed, the top level
+		 * aggregates / grouping expressions no longer need direct reference
+		 * to arg_var anyway.)
+		 */
+		relids_subtract = bms_copy(rel->relids);
+		bms_add_member(relids_subtract, 0);
+
+		baserel = find_base_rel(root, var->varno);
+		ndx = var->varattno - baserel->min_attr;
+		if (bms_nonempty_difference(baserel->attr_needed[ndx],
+									relids_subtract))
+		{
+			/*
+			 * The variable is needed by upper join. This includes one that is
+			 * referenced by a generic grouping expression but couldn't be
+			 * recognized as grouping expression on its own at the top of the
+			 * loop.
+			 *
+			 * The only way to bring this var to the aggregation output is to
+			 * add it to the grouping expressions too, but we can't do this
+			 * unless aggregation push-down involves 2-stage aggregation. So
+			 * give up.
+			 */
+			return false;
+		}
+		else
+		{
+			/*
+			 * As long as the query is semantically correct, arriving here
+			 * means that the var is referenced by generic grouping
+			 * expression. "target" should not contain it, as it only provides
+			 * input for the final aggregation (it contans GroupedVar for the
+			 * whole grouping expression).
+			 */
+		}
+
+		add_column_to_pathtarget(agg_input, (Expr *) var, 0);
+	}
+
+	return true;
+}
+
+
+/*
+ * Translate RelAggInfo of parent relation so it matches given child relation.
+ */
+RelAggInfo *
+translate_rel_agg_info(PlannerInfo *root, RelAggInfo *parent,
+					   AppendRelInfo **appinfos, int nappinfos)
+{
+	RelAggInfo *result;
+
+	result = makeNode(RelAggInfo);
+
+	result->target = copy_pathtarget(parent->target);
+	result->target->exprs = (List *)
+		adjust_appendrel_attrs(root,
+							   (Node *) result->target->exprs,
+							   nappinfos, appinfos);
+
+	result->input = copy_pathtarget(parent->input);
+	result->input->exprs = (List *)
+		adjust_appendrel_attrs(root,
+							   (Node *) result->input->exprs,
+							   nappinfos, appinfos);
+
+	result->group_clauses = parent->group_clauses;
+
+	result->group_exprs = (List *)
+		adjust_appendrel_attrs(root,
+							   (Node *) parent->group_exprs,
+							   nappinfos, appinfos);
+
+	result->agg_exprs = (List *)
+		adjust_appendrel_attrs(root,
+							   (Node *) parent->agg_exprs,
+							   nappinfos, appinfos);
+	return result;
+}
diff --git a/src/backend/optimizer/util/tlist.c b/src/backend/optimizer/util/tlist.c
index 5500f33e63..fbc2851b39 100644
--- a/src/backend/optimizer/util/tlist.c
+++ b/src/backend/optimizer/util/tlist.c
@@ -105,6 +105,17 @@ tlist_member_ignore_relabel(Expr *node, List *targetlist)
 		while (tlexpr && IsA(tlexpr, RelabelType))
 			tlexpr = ((RelabelType *) tlexpr)->arg;
 
+		/*
+		 * The targetlist may contain GroupedVar where caller expects the
+		 * actual expression.
+		 *
+		 * XXX prepare_sort_from_pathkeys() needs this special case. Should
+		 * the same assignment be also added to the other tlist_member_...()
+		 * functions?
+		 */
+		if (IsA(tlexpr, GroupedVar))
+			tlexpr = ((GroupedVar *) tlexpr)->gvexpr;
+
 		if (equal(node, tlexpr))
 			return tlentry;
 	}
@@ -426,7 +437,6 @@ get_sortgrouplist_exprs(List *sgClauses, List *targetList)
 	return result;
 }
 
-
 /*****************************************************************************
  *		Functions to extract data from a list of SortGroupClauses
  *
@@ -801,6 +811,68 @@ apply_pathtarget_labeling_to_tlist(List *tlist, PathTarget *target)
 }
 
 /*
+ * For each aggregate add GroupedVar to the grouped target.
+ *
+ * Caller passes the aggregates in the form of GroupedVarInfos so that we
+ * don't have to look for gvid.
+ */
+void
+add_grouped_vars_to_target(PlannerInfo *root, PathTarget *target,
+						   List *expressions)
+{
+	ListCell   *lc;
+
+	/* Create the vars and add them to the target. */
+	foreach(lc, expressions)
+	{
+		GroupedVarInfo *gvi;
+		GroupedVar *gvar;
+
+		gvi = lfirst_node(GroupedVarInfo, lc);
+		gvar = makeNode(GroupedVar);
+		gvar->gvid = gvi->gvid;
+		gvar->gvexpr = gvi->gvexpr;
+		add_column_to_pathtarget(target, (Expr *) gvar, gvi->sortgroupref);
+	}
+}
+
+/*
+ * Return GroupedVar containing the passed-in expression if one exists, or
+ * NULL if the expression cannot be used as grouping key.
+ *
+ * is_derived reflects the ->derived field of the corresponding
+ * GroupedVarInfo.
+ */
+GroupedVar *
+get_grouping_expression(List *gvis, Expr *expr, bool *is_derived)
+{
+	ListCell   *lc;
+
+	foreach(lc, gvis)
+	{
+		GroupedVarInfo *gvi = lfirst_node(GroupedVarInfo, lc);
+
+		if (IsA(gvi->gvexpr, Aggref))
+			continue;
+
+		if (equal(gvi->gvexpr, expr))
+		{
+			GroupedVar *result = makeNode(GroupedVar);
+
+			Assert(gvi->sortgroupref > 0);
+			result->gvexpr = gvi->gvexpr;
+			result->gvid = gvi->gvid;
+			result->sortgroupref = gvi->sortgroupref;
+			*is_derived = gvi->derived;
+			return result;
+		}
+	}
+
+	/* The expression cannot be used as grouping key. */
+	return NULL;
+}
+
+/*
  * split_pathtarget_at_srfs
  *		Split given PathTarget into multiple levels to position SRFs safely
  *
diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c
index b16b1e4656..459dc3087c 100644
--- a/src/backend/optimizer/util/var.c
+++ b/src/backend/optimizer/util/var.c
@@ -840,3 +840,25 @@ alias_relid_set(PlannerInfo *root, Relids relids)
 	}
 	return result;
 }
+
+/*
+ * Return GroupedVarInfo for given GroupedVar.
+ *
+ * XXX Consider better location of this routine.
+ */
+GroupedVarInfo *
+find_grouped_var_info(PlannerInfo *root, GroupedVar *gvar)
+{
+	ListCell   *l;
+
+	foreach(l, root->grouped_var_list)
+	{
+		GroupedVarInfo *gvi = lfirst_node(GroupedVarInfo, l);
+
+		if (gvi->gvid == gvar->gvid)
+			return gvi;
+	}
+
+	elog(ERROR, "GroupedVarInfo not found");
+	return NULL;				/* keep compiler quiet */
+}
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 44257154b8..a47aa0e92c 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -104,6 +104,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	Oid			vatype;
 	FuncDetailCode fdresult;
 	char		aggkind = 0;
+	Oid			aggcombinefn = InvalidOid;
 	ParseCallbackState pcbstate;
 
 	/*
@@ -360,6 +361,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 			elog(ERROR, "cache lookup failed for aggregate %u", funcid);
 		classForm = (Form_pg_aggregate) GETSTRUCT(tup);
 		aggkind = classForm->aggkind;
+		aggcombinefn = classForm->aggcombinefn;
 		catDirectArgs = classForm->aggnumdirectargs;
 		ReleaseSysCache(tup);
 
@@ -759,6 +761,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 		aggref->aggstar = agg_star;
 		aggref->aggvariadic = func_variadic;
 		aggref->aggkind = aggkind;
+		aggref->aggcombinefn = aggcombinefn;
 		/* agglevelsup will be set by transformAggregateCall */
 		aggref->aggsplit = AGGSPLIT_SIMPLE; /* planner might change this */
 		aggref->location = location;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 03e9a28a63..5b1c86a4d0 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7757,6 +7757,35 @@ get_rule_expr(Node *node, deparse_context *context,
 			get_agg_expr((Aggref *) node, context, (Aggref *) node);
 			break;
 
+		case T_GroupedVar:
+			{
+				GroupedVar *gvar = castNode(GroupedVar, node);
+				Expr	   *expr = gvar->gvexpr;
+
+				/*
+				 * GroupedVar that setrefs.c leaves in the tree should only
+				 * exist in the Agg plan targetlist (while the GroupedVars in
+				 * upper plans should have been replaced with Vars). If
+				 * agg_partial is not initialized, the AGGSPLIT_SIMPLE
+				 * aggregate has been pushed down.
+				 */
+				if (IsA(expr, Aggref))
+				{
+					Aggref	   *aggref;
+
+					aggref = (Aggref *) gvar->gvexpr;
+					get_agg_expr(aggref, context, (Aggref *) gvar->gvexpr);
+				}
+				else if (IsA(expr, Var))
+					(void) get_variable((Var *) expr, 0, false, context);
+				else
+				{
+					Assert(IsA(gvar->gvexpr, OpExpr));
+					get_oper_expr((OpExpr *) expr, context);
+				}
+				break;
+			}
+
 		case T_GroupingFunc:
 			{
 				GroupingFunc *gexpr = (GroupingFunc *) node;
@@ -9242,10 +9271,18 @@ get_agg_combine_expr(Node *node, deparse_context *context, void *private)
 	Aggref	   *aggref;
 	Aggref	   *original_aggref = private;
 
-	if (!IsA(node, Aggref))
+	if (IsA(node, Aggref))
+		aggref = (Aggref *) node;
+	else if (IsA(node, GroupedVar))
+	{
+		GroupedVar *gvar = castNode(GroupedVar, node);
+
+		aggref = (Aggref *) gvar->gvexpr;
+		original_aggref = castNode(Aggref, gvar->gvexpr);
+	}
+	else
 		elog(ERROR, "combining Aggref does not point to an Aggref");
 
-	aggref = (Aggref *) node;
 	get_agg_expr(aggref, context, original_aggref);
 }
 
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index f1c78ffb65..bd4868cb56 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -113,6 +113,7 @@
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_type.h"
 #include "executor/executor.h"
+#include "executor/nodeAgg.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -3883,6 +3884,39 @@ estimate_hash_bucket_stats(PlannerInfo *root, Node *hashkey, double nbuckets,
 	ReleaseVariableStats(vardata);
 }
 
+/*
+ * estimate_hashagg_tablesize
+ *	  estimate the number of bytes that a hash aggregate hashtable will
+ *	  require based on the agg_costs, path width and dNumGroups.
+ *
+ * XXX this may be over-estimating the size now that hashagg knows to omit
+ * unneeded columns from the hashtable. Also for mixed-mode grouping sets,
+ * grouping columns not in the hashed set are counted here even though hashagg
+ * won't store them. Is this a problem?
+ */
+Size
+estimate_hashagg_tablesize(Path *path, const AggClauseCosts *agg_costs,
+						   double dNumGroups)
+{
+	Size		hashentrysize;
+
+	/* Estimate per-hash-entry space at tuple width... */
+	hashentrysize = MAXALIGN(path->pathtarget->width) +
+		MAXALIGN(SizeofMinimalTupleHeader);
+
+	/* plus space for pass-by-ref transition values... */
+	hashentrysize += agg_costs->transitionSpace;
+	/* plus the per-hash-entry overhead */
+	hashentrysize += hash_agg_entry_size(agg_costs->numAggs);
+
+	/*
+	 * Note that this disregards the effect of fill-factor and growth policy
+	 * of the hash-table. That's probably ok, given default the default
+	 * fill-factor is relatively high. It'd be hard to meaningfully factor in
+	 * "double-in-size" growth policies here.
+	 */
+	return hashentrysize * dNumGroups;
+}
 
 /*-------------------------------------------------------------------------
  *
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index c5ba149996..61388d1254 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -952,6 +952,15 @@ static struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 	{
+		{"enable_agg_pushdown", PGC_USERSET, QUERY_TUNING_METHOD,
+			gettext_noop("Enables aggregation push-down."),
+			NULL
+		},
+		&enable_agg_pushdown,
+		false,
+		NULL, NULL, NULL
+	},
+	{
 		{"enable_parallel_append", PGC_USERSET, QUERY_TUNING_METHOD,
 			gettext_noop("Enables the planner's use of parallel append plans."),
 			NULL
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 697d3d7a5f..1e422fc140 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -223,6 +223,7 @@ typedef enum NodeTag
 	T_IndexOptInfo,
 	T_ForeignKeyOptInfo,
 	T_ParamPathInfo,
+	T_RelAggInfo,
 	T_Path,
 	T_IndexPath,
 	T_BitmapHeapPath,
@@ -263,9 +264,11 @@ typedef enum NodeTag
 	T_PathTarget,
 	T_RestrictInfo,
 	T_PlaceHolderVar,
+	T_GroupedVar,
 	T_SpecialJoinInfo,
 	T_AppendRelInfo,
 	T_PlaceHolderInfo,
+	T_GroupedVarInfo,
 	T_MinMaxAggInfo,
 	T_PlannerParamItem,
 	T_RollupData,
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 1b4b0d75af..6af31f2722 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -296,6 +296,7 @@ typedef struct Aggref
 	Oid			aggcollid;		/* OID of collation of result */
 	Oid			inputcollid;	/* OID of collation that function should use */
 	Oid			aggtranstype;	/* type Oid of aggregate's transition value */
+	Oid			aggcombinefn;	/* combine function (see pg_aggregate.h) */
 	List	   *aggargtypes;	/* type Oids of direct and aggregated args */
 	List	   *aggdirectargs;	/* direct arguments, if an ordered-set agg */
 	List	   *args;			/* aggregated arguments and sort expressions */
@@ -306,6 +307,7 @@ typedef struct Aggref
 	bool		aggvariadic;	/* true if variadic arguments have been
 								 * combined into an array last argument */
 	char		aggkind;		/* aggregate kind (see pg_aggregate.h) */
+
 	Index		agglevelsup;	/* > 0 if agg belongs to outer query */
 	AggSplit	aggsplit;		/* expected agg-splitting mode of parent Agg */
 	int			location;		/* token location, or -1 if unknown */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 41caf873fb..3e0c4cb060 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -193,7 +193,8 @@ typedef struct PlannerInfo
 	 * unreferenced view RTE; or if the RelOptInfo hasn't been made yet.
 	 */
 	struct RelOptInfo **simple_rel_array;	/* All 1-rel RelOptInfos */
-	int			simple_rel_array_size;	/* allocated size of array */
+
+	int			simple_rel_array_size;	/* allocated size of the arrays above */
 
 	/*
 	 * simple_rte_array is the same length as simple_rel_array and holds
@@ -247,6 +248,7 @@ typedef struct PlannerInfo
 	 * join_rel_level is NULL if not in use.
 	 */
 	List	  **join_rel_level; /* lists of join-relation RelOptInfos */
+
 	int			join_cur_level; /* index of list being extended */
 
 	List	   *init_plans;		/* init SubPlans for query */
@@ -279,6 +281,8 @@ typedef struct PlannerInfo
 
 	List	   *placeholder_list;	/* list of PlaceHolderInfos */
 
+	List	   *grouped_var_list;	/* List of GroupedVarInfos. */
+
 	List	   *fkey_list;		/* list of ForeignKeyOptInfos */
 
 	List	   *query_pathkeys; /* desired pathkeys for query_planner() */
@@ -467,6 +471,8 @@ typedef struct PartitionSchemeData *PartitionScheme;
  *		direct_lateral_relids - rels this rel has direct LATERAL references to
  *		lateral_relids - required outer rels for LATERAL, as a Relids set
  *			(includes both direct and indirect lateral references)
+ *		gpi - GroupedPathInfo if the relation can produce grouped paths, NULL
+ *		otherwise.
  *
  * If the relation is a base relation it will have these fields set:
  *
@@ -646,6 +652,16 @@ typedef struct RelOptInfo
 	Relids		direct_lateral_relids;	/* rels directly laterally referenced */
 	Relids		lateral_relids; /* minimum parameterization of rel */
 
+	/* Information needed to apply partial aggregation to this rel's paths. */
+	struct RelAggInfo *agg_info;
+
+	/*
+	 * If the relation can produce grouped paths, store them here.
+	 *
+	 * If "grouped" is valid then "agg_info" must be NULL and vice versa.
+	 */
+	struct RelOptInfo *grouped;
+
 	/* information about a base rel (not set for join rels!) */
 	Index		relid;
 	Oid			reltablespace;	/* containing tablespace */
@@ -1049,6 +1065,64 @@ typedef struct ParamPathInfo
 	List	   *ppi_clauses;	/* join clauses available from outer rels */
 } ParamPathInfo;
 
+/*
+ * RelAggInfo
+ *
+ * RelOptInfo needs information contained here if its paths should be
+ * aggregated.
+ *
+ * "target" will be used as pathtarget for aggregation if "explicit
+ * aggregation" is applied to base relation or join. The same target will will
+ * also --- if the relation is a join --- be used to joinin grouped path to a
+ * non-grouped one.
+ *
+ * These targets contain plain-Var grouping expressions, generic grouping
+ * expressions wrapped in GroupedVar structure, or Aggrefs which are also
+ * wrapped in GroupedVar. Once GroupedVar is evaluated, its value is passed to
+ * the upper paths w/o being evaluated again. If final aggregation appears to
+ * be necessary above the final join, the contained Aggrefs are supposed to
+ * provide the final aggregation plan with input values, i.e. the aggregate
+ * transient state.
+ *
+ * Note: There's a convention that GroupedVars that contain Aggref expressions
+ * are supposed to follow the other expressions of the target. Iterations of
+ * ->exprs may rely on this arrangement.
+ *
+ * "input" contains Vars used either as grouping expressions or aggregate
+ * arguments, plus those used in grouping expressions which are not plain Vars
+ * themselves. Paths providing the aggregation plan with input data should use
+ * this target.
+ *
+ * "group_clauses" and "group_exprs" are lists of SortGroupClause and the
+ * corresponding grouping expressions respectively.
+ *
+ * "agg_exprs" is a list of Aggref nodes for the aggregation of the relation's
+ * paths.
+ *
+ * "rows" is the estimated number of result tuples produced by grouped
+ * paths.
+ */
+typedef struct RelAggInfo
+{
+	NodeTag		type;
+
+	PathTarget *target;			/* Target for grouped paths.. */
+
+	PathTarget *input;			/* pathtarget of paths that generate input for
+								 * aggregation paths. */
+
+	List	   *group_clauses;
+	List	   *group_exprs;
+
+	/*
+	 * TODO Consider removing this field and creating the Aggref, partial or
+	 * simple, when needed, but avoid creating it multiple times (e.g. once
+	 * for hash grouping, other times for sorted grouping).
+	 */
+	List	   *agg_exprs;		/* Aggref expressions. */
+
+	double		rows;
+} RelAggInfo;
 
 /*
  * Type "Path" is used as-is for sequential-scan paths, as well as some other
@@ -1078,6 +1152,10 @@ typedef struct ParamPathInfo
  *
  * "pathkeys" is a List of PathKey nodes (see above), describing the sort
  * ordering of the path's output rows.
+ *
+ * "uniquekeys" is a List of Bitmapset objects, each pointing at a set of
+ * expressions of "pathtarget" whose values within the path output are
+ * distinct.
  */
 typedef struct Path
 {
@@ -1101,6 +1179,10 @@ typedef struct Path
 
 	List	   *pathkeys;		/* sort ordering of path's output */
 	/* pathkeys is a List of PathKey nodes; see above */
+
+	List	   *uniquekeys;		/* list of bitmapsets where each set contains
+								 * positions of unique expressions within
+								 * pathtarget. */
 } Path;
 
 /* Macro for extracting a path's parameterization relids; beware double eval */
@@ -1526,12 +1608,16 @@ typedef struct HashPath
  * ProjectionPath node, which is marked dummy to indicate that we intend to
  * assign the work to the input plan node.  The estimated cost for the
  * ProjectionPath node will account for whether a Result will be used or not.
+ *
+ * force_result field tells that the Result node must be used for some reason
+ * even though the subpath could normally handle the projection.
  */
 typedef struct ProjectionPath
 {
 	Path		path;
 	Path	   *subpath;		/* path representing input source */
 	bool		dummypp;		/* true if no separate Result is needed */
+	bool		force_result;	/* Is Result node required? */
 } ProjectionPath;
 
 /*
@@ -2007,6 +2093,29 @@ typedef struct PlaceHolderVar
 	Index		phlevelsup;		/* > 0 if PHV belongs to outer query */
 } PlaceHolderVar;
 
+
+/*
+ * Similar to the concept of PlaceHolderVar, we treat aggregates and grouping
+ * columns as special variables if grouping is possible below the top-level
+ * join. Likewise, the variable is evaluated below the query targetlist (in
+ * particular, in the targetlist of AGGSPLIT_INITIAL_SERIAL aggregation node
+ * which has base relation or a join as the input) and bubbles up through the
+ * join tree until it reaches AGGSPLIT_FINAL_DESERIAL aggregation node.
+ *
+ * gvexpr is either Aggref or a generic (non-Var) grouping expression. (If a
+ * simple Var, we don't replace it with GroupedVar.)
+ */
+typedef struct GroupedVar
+{
+	Expr		xpr;
+	Expr	   *gvexpr;			/* the represented expression */
+
+	Index		sortgroupref;	/* SortGroupClause.tleSortGroupRef if gvexpr
+								 * is grouping expression. */
+	Index		gvid;			/* GroupedVarInfo */
+	int32		width;			/* Expression width. */
+} GroupedVar;
+
 /*
  * "Special join" info.
  *
@@ -2203,6 +2312,24 @@ typedef struct PlaceHolderInfo
 } PlaceHolderInfo;
 
 /*
+ * Likewise, GroupedVarInfo exists for each distinct GroupedVar.
+ */
+typedef struct GroupedVarInfo
+{
+	NodeTag		type;
+
+	Index		gvid;			/* GroupedVar.gvid */
+	Expr	   *gvexpr;			/* the represented expression. */
+	Index		sortgroupref;	/* If gvexpr is a grouping expression, this is
+								 * the tleSortGroupRef of the corresponding
+								 * SortGroupClause. */
+	Relids		gv_eval_at;		/* lowest level we can evaluate the expression
+								 * at or NULL if it can happen anywhere. */
+	bool		derived;		/* derived from another GroupedVarInfo using
+								 * equeivalence classes? */
+} GroupedVarInfo;
+
+/*
  * This struct describes one potentially index-optimizable MIN/MAX aggregate
  * function.  MinMaxAggPath contains a list of these, and if we accept that
  * path, the list is stored into root->minmax_aggs for use during setrefs.c.
diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h
index ed854fdd40..f9f3d14b0b 100644
--- a/src/include/optimizer/clauses.h
+++ b/src/include/optimizer/clauses.h
@@ -88,4 +88,6 @@ extern Query *inline_set_returning_function(PlannerInfo *root,
 extern List *expand_function_arguments(List *args, Oid result_type,
 						  HeapTuple func_tuple);
 
+extern GroupedVarInfo *translate_expression_to_rels(PlannerInfo *root,
+							 GroupedVarInfo *gvi, Index relid);
 #endif							/* CLAUSES_H */
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 77ca7ff837..bb6ec0f4e1 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -72,6 +72,7 @@ extern PGDLLIMPORT bool enable_partitionwise_aggregate;
 extern PGDLLIMPORT bool enable_parallel_append;
 extern PGDLLIMPORT bool enable_parallel_hash;
 extern PGDLLIMPORT bool enable_partition_pruning;
+extern PGDLLIMPORT bool enable_agg_pushdown;
 extern PGDLLIMPORT int constraint_exclusion;
 
 extern double clamp_row_est(double nrows);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 7c5ff22650..e7edd2f34e 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -71,6 +71,7 @@ extern AppendPath *create_append_path(PlannerInfo *root, RelOptInfo *rel,
 				   List *partitioned_rels, double rows);
 extern MergeAppendPath *create_merge_append_path(PlannerInfo *root,
 						 RelOptInfo *rel,
+						 PathTarget *target,
 						 List *subpaths,
 						 List *pathkeys,
 						 Relids required_outer,
@@ -123,6 +124,7 @@ extern Relids calc_non_nestloop_required_outer(Path *outer_path, Path *inner_pat
 
 extern NestPath *create_nestloop_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
+					 PathTarget *target,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
 					 JoinPathExtraData *extra,
@@ -134,6 +136,7 @@ extern NestPath *create_nestloop_path(PlannerInfo *root,
 
 extern MergePath *create_mergejoin_path(PlannerInfo *root,
 					  RelOptInfo *joinrel,
+					  PathTarget *target,
 					  JoinType jointype,
 					  JoinCostWorkspace *workspace,
 					  JoinPathExtraData *extra,
@@ -148,6 +151,7 @@ extern MergePath *create_mergejoin_path(PlannerInfo *root,
 
 extern HashPath *create_hashjoin_path(PlannerInfo *root,
 					 RelOptInfo *joinrel,
+					 PathTarget *target,
 					 JoinType jointype,
 					 JoinCostWorkspace *workspace,
 					 JoinPathExtraData *extra,
@@ -196,6 +200,13 @@ extern AggPath *create_agg_path(PlannerInfo *root,
 				List *qual,
 				const AggClauseCosts *aggcosts,
 				double numGroups);
+extern AggPath *create_agg_sorted_path(PlannerInfo *root,
+					   Path *subpath,
+					   bool check_pathkeys,
+					   double input_rows);
+extern AggPath *create_agg_hashed_path(PlannerInfo *root,
+					   Path *subpath,
+					   double input_rows);
 extern GroupingSetsPath *create_groupingsets_path(PlannerInfo *root,
 						 RelOptInfo *rel,
 						 Path *subpath,
@@ -255,6 +266,14 @@ extern Path *reparameterize_path(PlannerInfo *root, Path *path,
 					double loop_count);
 extern Path *reparameterize_path_by_child(PlannerInfo *root, Path *path,
 							 RelOptInfo *child_rel);
+extern void make_uniquekeys(PlannerInfo *root, Path *path);
+extern void make_uniquekeys_for_agg_path(Path *path);
+extern List *make_uniquekeys_for_join(PlannerInfo *root,
+						 Path *outerpath,
+						 Path *innerpath,
+						 PathTarget *target,
+						 bool *keys_ok);
+extern void free_uniquekeys(List *uniquekeys);
 
 /*
  * prototypes for relnode.c
@@ -270,7 +289,8 @@ extern RelOptInfo *build_join_rel(PlannerInfo *root,
 			   RelOptInfo *outer_rel,
 			   RelOptInfo *inner_rel,
 			   SpecialJoinInfo *sjinfo,
-			   List **restrictlist_ptr);
+			   List **restrictlist_ptr,
+			   bool grouped);
 extern Relids min_join_parameterization(PlannerInfo *root,
 						  Relids joinrelids,
 						  RelOptInfo *outer_rel,
@@ -296,6 +316,11 @@ extern ParamPathInfo *find_param_path_info(RelOptInfo *rel,
 extern RelOptInfo *build_child_join_rel(PlannerInfo *root,
 					 RelOptInfo *outer_rel, RelOptInfo *inner_rel,
 					 RelOptInfo *parent_joinrel, List *restrictlist,
-					 SpecialJoinInfo *sjinfo, JoinType jointype);
-
+					 SpecialJoinInfo *sjinfo, JoinType jointype,
+					 bool grouped);
+extern RelAggInfo *create_rel_agg_info(PlannerInfo *root, RelOptInfo *rel);
+extern RelAggInfo *translate_rel_agg_info(PlannerInfo *root,
+					   RelAggInfo *agg_info,
+					   AppendRelInfo **appinfos,
+					   int nappinfos);
 #endif							/* PATHNODE_H */
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index cafde307ad..ede0cf242d 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -21,6 +21,7 @@
  * allpaths.c
  */
 extern PGDLLIMPORT bool enable_geqo;
+extern PGDLLIMPORT bool enable_agg_pushdown;
 extern PGDLLIMPORT int geqo_threshold;
 extern PGDLLIMPORT int min_parallel_table_scan_size;
 extern PGDLLIMPORT int min_parallel_index_scan_size;
@@ -50,11 +51,16 @@ extern PGDLLIMPORT join_search_hook_type join_search_hook;
 
 extern RelOptInfo *make_one_rel(PlannerInfo *root, List *joinlist);
 extern void set_dummy_rel_pathlist(RelOptInfo *rel);
-extern RelOptInfo *standard_join_search(PlannerInfo *root, int levels_needed,
+extern RelOptInfo *standard_join_search(PlannerInfo *root,
+					 int levels_needed,
 					 List *initial_rels);
 
 extern void generate_gather_paths(PlannerInfo *root, RelOptInfo *rel,
 					  bool override_rows);
+
+extern bool create_grouped_path(PlannerInfo *root, RelOptInfo *rel,
+					Path *subpath, bool precheck,
+					bool parallel, AggStrategy aggstrategy);
 extern int compute_parallel_worker(RelOptInfo *rel, double heap_pages,
 						double index_pages, int max_workers);
 extern void create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel,
@@ -70,7 +76,8 @@ extern void debug_print_rel(PlannerInfo *root, RelOptInfo *rel);
  * indxpath.c
  *	  routines to generate index paths
  */
-extern void create_index_paths(PlannerInfo *root, RelOptInfo *rel);
+extern void create_index_paths(PlannerInfo *root, RelOptInfo *rel,
+				   bool grouped);
 extern bool relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel,
 							  List *restrictlist,
 							  List *exprlist, List *oprlist);
@@ -92,7 +99,8 @@ extern Expr *adjust_rowcompare_for_index(RowCompareExpr *clause,
  * tidpath.h
  *	  routines to generate tid paths
  */
-extern void create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel);
+extern void create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel,
+					 bool grouped);
 
 /*
  * joinpath.c
@@ -101,7 +109,8 @@ extern void create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel);
 extern void add_paths_to_joinrel(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, SpecialJoinInfo *sjinfo,
-					 List *restrictlist);
+					 List *restrictlist,
+					 bool grouped, bool do_aggregate);
 
 /*
  * joinrels.c
@@ -237,6 +246,10 @@ extern bool has_useful_pathkeys(PlannerInfo *root, RelOptInfo *rel);
 extern PathKey *make_canonical_pathkey(PlannerInfo *root,
 					   EquivalenceClass *eclass, Oid opfamily,
 					   int strategy, bool nulls_first);
+extern void add_uniquekeys(List **keys_p, Bitmapset *new_set);
+extern bool match_uniquekeys_to_group_pathkeys(PlannerInfo *root,
+								   List *uniquekeys,
+								   PathTarget *target);
 extern void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 						List *live_childrels);
 
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index c8ab0280d2..ac76375b31 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -76,6 +76,8 @@ extern void add_base_rels_to_query(PlannerInfo *root, Node *jtnode);
 extern void build_base_rel_tlists(PlannerInfo *root, List *final_tlist);
 extern void add_vars_to_targetlist(PlannerInfo *root, List *vars,
 					   Relids where_needed, bool create_new_ph);
+extern void add_grouped_base_rels_to_query(PlannerInfo *root);
+extern void add_grouped_vars_to_rels(PlannerInfo *root);
 extern void find_lateral_references(PlannerInfo *root);
 extern void create_lateral_join_info(PlannerInfo *root);
 extern List *deconstruct_jointree(PlannerInfo *root);
diff --git a/src/include/optimizer/tlist.h b/src/include/optimizer/tlist.h
index 9fa52e1278..6c7619ad31 100644
--- a/src/include/optimizer/tlist.h
+++ b/src/include/optimizer/tlist.h
@@ -16,7 +16,6 @@
 
 #include "nodes/relation.h"
 
-
 extern TargetEntry *tlist_member(Expr *node, List *targetlist);
 extern TargetEntry *tlist_member_ignore_relabel(Expr *node, List *targetlist);
 
@@ -41,7 +40,6 @@ extern Node *get_sortgroupclause_expr(SortGroupClause *sgClause,
 						 List *targetList);
 extern List *get_sortgrouplist_exprs(List *sgClauses,
 						List *targetList);
-
 extern SortGroupClause *get_sortgroupref_clause(Index sortref,
 						List *clauses);
 extern SortGroupClause *get_sortgroupref_clause_noerr(Index sortref,
@@ -65,6 +63,13 @@ extern void split_pathtarget_at_srfs(PlannerInfo *root,
 						 PathTarget *target, PathTarget *input_target,
 						 List **targets, List **targets_contain_srfs);
 
+/* TODO Find the best location (position and in some cases even file) for the
+ * following ones. */
+extern void add_grouped_vars_to_target(PlannerInfo *root, PathTarget *target,
+						   List *expressions);
+extern GroupedVar *get_grouping_expression(List *gvis, Expr *expr,
+						bool *is_derived);
+
 /* Convenience macro to get a PathTarget with valid cost/width fields */
 #define create_pathtarget(root, tlist) \
 	set_pathtarget_cost_width(root, make_pathtarget_from_tlist(tlist))
diff --git a/src/include/optimizer/var.h b/src/include/optimizer/var.h
index 43c53b5344..5a795c3231 100644
--- a/src/include/optimizer/var.h
+++ b/src/include/optimizer/var.h
@@ -36,5 +36,7 @@ extern bool contain_vars_of_level(Node *node, int levelsup);
 extern int	locate_var_of_level(Node *node, int levelsup);
 extern List *pull_var_clause(Node *node, int flags);
 extern Node *flatten_join_alias_vars(PlannerInfo *root, Node *node);
+extern GroupedVarInfo *find_grouped_var_info(PlannerInfo *root,
+					  GroupedVar *gvar);
 
 #endif							/* VAR_H */
diff --git a/src/include/utils/selfuncs.h b/src/include/utils/selfuncs.h
index 95e44280c4..3a14fc6036 100644
--- a/src/include/utils/selfuncs.h
+++ b/src/include/utils/selfuncs.h
@@ -213,6 +213,9 @@ extern void estimate_hash_bucket_stats(PlannerInfo *root,
 						   Node *hashkey, double nbuckets,
 						   Selectivity *mcv_freq,
 						   Selectivity *bucketsize_frac);
+extern Size estimate_hashagg_tablesize(Path *path,
+						   const AggClauseCosts *agg_costs,
+						   double dNumGroups);
 
 extern List *deconstruct_indexquals(IndexPath *path);
 extern void genericcostestimate(PlannerInfo *root, IndexPath *path,
