diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 6ef1e48..8388ea7 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -1064,6 +1064,7 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
 	/* For upper relations, scanrel must be either a joinrel or a baserel */
 	Assert(context->foreignrel->reloptkind != RELOPT_UPPER_REL ||
 		   IS_JOIN_REL(scanrel) ||
+		   scanrel->reloptkind == RELOPT_OTHER_MEMBER_REL ||
 		   scanrel->reloptkind == RELOPT_BASEREL);
 
 	/* Construct FROM clause */
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 059c5c3..d968832 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7181,3 +7181,115 @@ AND ftoptions @> array['fetch_size=60000'];
 (1 row)
 
 ROLLBACK;
+-- Partition-wise aggregates with FDW
+CREATE TABLE plt1 (a int, b int, c text) PARTITION BY RANGE(a);
+CREATE TABLE plt1_p1 (a int, b int, c text);
+CREATE TABLE plt1_p2 (a int, b int, c text);
+CREATE TABLE plt1_p3 (a int, b int, c text);
+INSERT INTO plt1_p1 SELECT i % 30, i % 50, to_char(i/30, 'FM0000') FROM generate_series(1, 3000) i WHERE (i % 30) < 10;
+INSERT INTO plt1_p2 SELECT i % 30, i % 50, to_char(i/30, 'FM0000') FROM generate_series(1, 3000) i WHERE (i % 30) < 20 and (i % 30) >= 10;
+INSERT INTO plt1_p3 SELECT i % 30, i % 50, to_char(i/30, 'FM0000') FROM generate_series(1, 3000) i WHERE (i % 30) < 30 and (i % 30) >= 20;
+-- Create foreign partitions
+CREATE FOREIGN TABLE fplt1_p1 PARTITION OF plt1 FOR VALUES FROM (0) TO (10) SERVER loopback OPTIONS (table_name 'plt1_p1');
+CREATE FOREIGN TABLE fplt1_p2 PARTITION OF plt1 FOR VALUES FROM (10) TO (20) SERVER loopback OPTIONS (table_name 'plt1_p2');;
+CREATE FOREIGN TABLE fplt1_p3 PARTITION OF plt1 FOR VALUES FROM (20) TO (30) SERVER loopback OPTIONS (table_name 'plt1_p3');;
+ANALYZE plt1;
+ANALYZE fplt1_p1;
+ANALYZE fplt1_p2;
+ANALYZE fplt1_p3;
+-- When GROUP BY clause matches with PARTITION KEY.
+-- Plan when partition-wise-agg is disabled
+SET enable_partition_wise_agg TO false;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT a, sum(b), min(b), count(*) FROM plt1 GROUP BY a ORDER BY 1;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Sort
+   Output: plt1.a, (sum(plt1.b)), (min(plt1.b)), (count(*))
+   Sort Key: plt1.a
+   ->  HashAggregate
+         Output: plt1.a, sum(plt1.b), min(plt1.b), count(*)
+         Group Key: plt1.a
+         ->  Append
+               ->  Seq Scan on public.plt1
+                     Output: plt1.a, plt1.b
+               ->  Foreign Scan on public.fplt1_p1
+                     Output: fplt1_p1.a, fplt1_p1.b
+                     Remote SQL: SELECT a, b FROM public.plt1_p1
+               ->  Foreign Scan on public.fplt1_p2
+                     Output: fplt1_p2.a, fplt1_p2.b
+                     Remote SQL: SELECT a, b FROM public.plt1_p2
+               ->  Foreign Scan on public.fplt1_p3
+                     Output: fplt1_p3.a, fplt1_p3.b
+                     Remote SQL: SELECT a, b FROM public.plt1_p3
+(18 rows)
+
+-- Plan when partition-wise-agg is enabled
+SET enable_partition_wise_agg TO true;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT a, sum(b), min(b), count(*) FROM plt1 GROUP BY a ORDER BY 1;
+NOTICE:  partition-wise grouping is possible.
+                                         QUERY PLAN                                          
+---------------------------------------------------------------------------------------------
+ Sort
+   Output: fplt1_p1.a, (sum(fplt1_p1.b)), (min(fplt1_p1.b)), (count(*))
+   Sort Key: fplt1_p1.a
+   ->  Append
+         ->  Foreign Scan
+               Output: fplt1_p1.a, (sum(fplt1_p1.b)), (min(fplt1_p1.b)), (count(*))
+               Relations: Aggregate on (public.fplt1_p1 plt1)
+               Remote SQL: SELECT a, sum(b), min(b), count(*) FROM public.plt1_p1 GROUP BY a
+         ->  Foreign Scan
+               Output: fplt1_p2.a, (sum(fplt1_p2.b)), (min(fplt1_p2.b)), (count(*))
+               Relations: Aggregate on (public.fplt1_p2 plt1)
+               Remote SQL: SELECT a, sum(b), min(b), count(*) FROM public.plt1_p2 GROUP BY a
+         ->  Foreign Scan
+               Output: fplt1_p3.a, (sum(fplt1_p3.b)), (min(fplt1_p3.b)), (count(*))
+               Relations: Aggregate on (public.fplt1_p3 plt1)
+               Remote SQL: SELECT a, sum(b), min(b), count(*) FROM public.plt1_p3 GROUP BY a
+(16 rows)
+
+SELECT a, sum(b), min(b), count(*) FROM plt1 GROUP BY a ORDER BY 1;
+NOTICE:  partition-wise grouping is possible.
+ a  | sum  | min | count 
+----+------+-----+-------
+  0 | 2000 |   0 |   100
+  1 | 2100 |   1 |   100
+  2 | 2200 |   2 |   100
+  3 | 2300 |   3 |   100
+  4 | 2400 |   4 |   100
+  5 | 2500 |   5 |   100
+  6 | 2600 |   6 |   100
+  7 | 2700 |   7 |   100
+  8 | 2800 |   8 |   100
+  9 | 2900 |   9 |   100
+ 10 | 2000 |   0 |   100
+ 11 | 2100 |   1 |   100
+ 12 | 2200 |   2 |   100
+ 13 | 2300 |   3 |   100
+ 14 | 2400 |   4 |   100
+ 15 | 2500 |   5 |   100
+ 16 | 2600 |   6 |   100
+ 17 | 2700 |   7 |   100
+ 18 | 2800 |   8 |   100
+ 19 | 2900 |   9 |   100
+ 20 | 2000 |   0 |   100
+ 21 | 2100 |   1 |   100
+ 22 | 2200 |   2 |   100
+ 23 | 2300 |   3 |   100
+ 24 | 2400 |   4 |   100
+ 25 | 2500 |   5 |   100
+ 26 | 2600 |   6 |   100
+ 27 | 2700 |   7 |   100
+ 28 | 2800 |   8 |   100
+ 29 | 2900 |   9 |   100
+(30 rows)
+
+-- Clean-up
+DROP FOREIGN TABLE fplt1_p3;
+DROP FOREIGN TABLE fplt1_p2;
+DROP FOREIGN TABLE fplt1_p1;
+DROP TABLE plt1_p3;
+DROP TABLE plt1_p2;
+DROP TABLE plt1_p1;
+DROP TABLE plt1;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 22acba8..630c374 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -349,7 +349,8 @@ static bool postgresRecheckForeignScan(ForeignScanState *node,
 static void postgresGetForeignUpperPaths(PlannerInfo *root,
 							 UpperRelationKind stage,
 							 RelOptInfo *input_rel,
-							 RelOptInfo *output_rel);
+							 RelOptInfo *output_rel,
+							 PathTarget *target);
 
 /*
  * Helper functions
@@ -415,7 +416,8 @@ static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
 								Path *epq_path);
 static void add_foreign_grouping_paths(PlannerInfo *root,
 						   RelOptInfo *input_rel,
-						   RelOptInfo *grouped_rel);
+						   RelOptInfo *grouped_rel,
+						   PathTarget *target);
 
 
 /*
@@ -2688,7 +2690,7 @@ estimate_path_cost_size(PlannerInfo *root,
 		else if (foreignrel->reloptkind == RELOPT_UPPER_REL)
 		{
 			PgFdwRelationInfo *ofpinfo;
-			PathTarget *ptarget = root->upper_targets[UPPERREL_GROUP_AGG];
+			PathTarget *ptarget = fpinfo->grouped_target;
 			AggClauseCosts aggcosts;
 			double		input_rows;
 			int			numGroupCols;
@@ -4536,7 +4538,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
 	 * different from those in the plan's targetlist. Use a copy of path
 	 * target to record the new sortgrouprefs.
 	 */
-	grouping_target = copy_pathtarget(root->upper_targets[UPPERREL_GROUP_AGG]);
+	grouping_target = copy_pathtarget(fpinfo->grouped_target);
 
 	/*
 	 * Evaluate grouping targets and check whether they are safe to push down
@@ -4715,7 +4717,8 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
  */
 static void
 postgresGetForeignUpperPaths(PlannerInfo *root, UpperRelationKind stage,
-							 RelOptInfo *input_rel, RelOptInfo *output_rel)
+							 RelOptInfo *input_rel, RelOptInfo *output_rel,
+							 PathTarget *target)
 {
 	PgFdwRelationInfo *fpinfo;
 
@@ -4735,7 +4738,7 @@ postgresGetForeignUpperPaths(PlannerInfo *root, UpperRelationKind stage,
 	fpinfo->pushdown_safe = false;
 	output_rel->fdw_private = fpinfo;
 
-	add_foreign_grouping_paths(root, input_rel, output_rel);
+	add_foreign_grouping_paths(root, input_rel, output_rel, target);
 }
 
 /*
@@ -4747,13 +4750,12 @@ postgresGetForeignUpperPaths(PlannerInfo *root, UpperRelationKind stage,
  */
 static void
 add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
-						   RelOptInfo *grouped_rel)
+						   RelOptInfo *grouped_rel, PathTarget *target)
 {
 	Query	   *parse = root->parse;
 	PgFdwRelationInfo *ifpinfo = input_rel->fdw_private;
 	PgFdwRelationInfo *fpinfo = grouped_rel->fdw_private;
 	ForeignPath *grouppath;
-	PathTarget *grouping_target;
 	double		rows;
 	int			width;
 	Cost		startup_cost;
@@ -4764,7 +4766,8 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 		!root->hasHavingQual)
 		return;
 
-	grouping_target = root->upper_targets[UPPERREL_GROUP_AGG];
+	/* Store passed-in target in fpinfo for later use */
+	fpinfo->grouped_target = target;
 
 	/* save the input_rel as outerrel in fpinfo */
 	fpinfo->outerrel = input_rel;
@@ -4795,7 +4798,7 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 	/* Create and add foreign path to the grouping relation. */
 	grouppath = create_foreignscan_path(root,
 										grouped_rel,
-										grouping_target,
+										target,
 										rows,
 										startup_cost,
 										total_cost,
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 57dbb79..99cecb7 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -95,6 +95,7 @@ typedef struct PgFdwRelationInfo
 
 	/* Grouping information */
 	List	   *grouped_tlist;
+	PathTarget *grouped_target;
 
 	/* Subquery information */
 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 8f3edc1..f02ec8a 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1706,3 +1706,47 @@ WHERE ftrelid = 'table30000'::regclass
 AND ftoptions @> array['fetch_size=60000'];
 
 ROLLBACK;
+
+
+-- Partition-wise aggregates with FDW
+CREATE TABLE plt1 (a int, b int, c text) PARTITION BY RANGE(a);
+
+CREATE TABLE plt1_p1 (a int, b int, c text);
+CREATE TABLE plt1_p2 (a int, b int, c text);
+CREATE TABLE plt1_p3 (a int, b int, c text);
+
+INSERT INTO plt1_p1 SELECT i % 30, i % 50, to_char(i/30, 'FM0000') FROM generate_series(1, 3000) i WHERE (i % 30) < 10;
+INSERT INTO plt1_p2 SELECT i % 30, i % 50, to_char(i/30, 'FM0000') FROM generate_series(1, 3000) i WHERE (i % 30) < 20 and (i % 30) >= 10;
+INSERT INTO plt1_p3 SELECT i % 30, i % 50, to_char(i/30, 'FM0000') FROM generate_series(1, 3000) i WHERE (i % 30) < 30 and (i % 30) >= 20;
+
+-- Create foreign partitions
+CREATE FOREIGN TABLE fplt1_p1 PARTITION OF plt1 FOR VALUES FROM (0) TO (10) SERVER loopback OPTIONS (table_name 'plt1_p1');
+CREATE FOREIGN TABLE fplt1_p2 PARTITION OF plt1 FOR VALUES FROM (10) TO (20) SERVER loopback OPTIONS (table_name 'plt1_p2');;
+CREATE FOREIGN TABLE fplt1_p3 PARTITION OF plt1 FOR VALUES FROM (20) TO (30) SERVER loopback OPTIONS (table_name 'plt1_p3');;
+
+ANALYZE plt1;
+ANALYZE fplt1_p1;
+ANALYZE fplt1_p2;
+ANALYZE fplt1_p3;
+
+-- When GROUP BY clause matches with PARTITION KEY.
+-- Plan when partition-wise-agg is disabled
+SET enable_partition_wise_agg TO false;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT a, sum(b), min(b), count(*) FROM plt1 GROUP BY a ORDER BY 1;
+
+-- Plan when partition-wise-agg is enabled
+SET enable_partition_wise_agg TO true;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT a, sum(b), min(b), count(*) FROM plt1 GROUP BY a ORDER BY 1;
+SELECT a, sum(b), min(b), count(*) FROM plt1 GROUP BY a ORDER BY 1;
+
+
+-- Clean-up
+DROP FOREIGN TABLE fplt1_p3;
+DROP FOREIGN TABLE fplt1_p2;
+DROP FOREIGN TABLE fplt1_p1;
+DROP TABLE plt1_p3;
+DROP TABLE plt1_p2;
+DROP TABLE plt1_p1;
+DROP TABLE plt1;
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 8143f80..0ab5c56 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -130,8 +130,6 @@ static void subquery_push_qual(Query *subquery,
 static void recurse_push_qual(Node *setOp, Query *topquery,
 				  RangeTblEntry *rte, Index rti, Node *qual);
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
-static void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
-						List *live_childrels);
 
 
 /*
@@ -1338,7 +1336,7 @@ set_append_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
  * parameterization or ordering. Similarly it collects partial paths from
  * non-dummy children to create partial append paths.
  */
-static void
+void
 add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 						List *live_childrels)
 {
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 220c81c..b2edaca 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -128,6 +128,7 @@ bool		enable_mergejoin = true;
 bool		enable_hashjoin = true;
 bool		enable_gathermerge = true;
 bool		enable_partition_wise_join = false;
+bool		enable_partition_wise_agg = true;
 
 typedef struct
 {
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 0aae4ca..929e0a6 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -1610,6 +1610,7 @@ create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags)
 {
 	Sort	   *plan;
 	Plan	   *subplan;
+	Relids		relids;
 
 	/*
 	 * We don't want any excess columns in the sorted tuples, so request a
@@ -1619,7 +1620,12 @@ create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags)
 	subplan = create_plan_recurse(root, best_path->subpath,
 								  flags | CP_SMALL_TLIST);
 
-	plan = make_sort_from_pathkeys(subplan, best_path->path.pathkeys, NULL);
+	/*
+	 * TODO: we need to fix something here. The "other" upper rels are not
+	 * marked as "OTHER" rels and may not have relids.
+	 */
+	relids = IS_OTHER_REL(best_path->subpath->parent) ? best_path->path.parent->relids : NULL;
+	plan = make_sort_from_pathkeys(subplan, best_path->path.pathkeys, relids);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
 
@@ -3393,15 +3399,8 @@ create_foreignscan_plan(PlannerInfo *root, ForeignPath *best_path,
 	/* Copy foreign server OID; likewise, no need to make FDW do this */
 	scan_plan->fs_server = rel->serverid;
 
-	/*
-	 * Likewise, copy the relids that are represented by this foreign scan. An
-	 * upper rel doesn't have relids set, but it covers all the base relations
-	 * participating in the underlying scan, so use root's all_baserels.
-	 */
-	if (rel->reloptkind == RELOPT_UPPER_REL)
-		scan_plan->fs_relids = root->all_baserels;
-	else
-		scan_plan->fs_relids = best_path->path.parent->relids;
+	/* Likewise, copy the relids from Path to Plan */
+	scan_plan->fs_relids = best_path->path.parent->relids;
 
 	/*
 	 * If this is a foreign join, and to make it valid to push down we had to
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 17fae4f..9f7db45 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -157,6 +157,15 @@ static PathTarget *make_sort_input_target(PlannerInfo *root,
 					   bool *have_postponed_srfs);
 static void adjust_paths_for_srfs(PlannerInfo *root, RelOptInfo *rel,
 					  List *targets, List *targets_contain_srfs);
+static void try_partition_wise_grouping(PlannerInfo *root,
+							RelOptInfo *input_rel,
+							RelOptInfo *grouped_rel,
+							PathTarget *target,
+							const AggClauseCosts *agg_costs,
+							List *rollup_lists,
+							List *rollup_groupclauses);
+static bool have_grouping_by_partkey(RelOptInfo *input_rel, PathTarget *target,
+						 List *groupClause);
 
 
 /*****************************************************************************
@@ -2067,7 +2076,8 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 	if (final_rel->fdwroutine &&
 		final_rel->fdwroutine->GetForeignUpperPaths)
 		final_rel->fdwroutine->GetForeignUpperPaths(root, UPPERREL_FINAL,
-													current_rel, final_rel);
+													current_rel, final_rel,
+													NULL);
 
 	/* Let extensions possibly add some more paths */
 	if (create_upper_paths_hook)
@@ -3283,7 +3293,18 @@ create_grouping_paths(PlannerInfo *root,
 	ListCell   *lc;
 
 	/* For now, do all work in the (GROUP_AGG, NULL) upperrel */
-	grouped_rel = fetch_upper_rel(root, UPPERREL_GROUP_AGG, NULL);
+	if (IS_OTHER_REL(input_rel))
+	{
+
+		/*
+		 * TODO: We should mark these rels as "other upper" rels similar to
+		 * "other" join and base relations.
+		 */
+		grouped_rel = fetch_upper_rel(root, UPPERREL_GROUP_AGG,
+									  input_rel->relids);
+	}
+	else
+		grouped_rel = fetch_upper_rel(root, UPPERREL_GROUP_AGG, NULL);
 
 	/*
 	 * If the input relation is not parallel-safe, then the grouped relation
@@ -3303,6 +3324,9 @@ create_grouping_paths(PlannerInfo *root,
 	grouped_rel->useridiscurrent = input_rel->useridiscurrent;
 	grouped_rel->fdwroutine = input_rel->fdwroutine;
 
+	/* Copy input rels's relids to grouped rel */
+	grouped_rel->relids = input_rel->relids;
+
 	/*
 	 * Check for degenerate grouping.
 	 */
@@ -3377,6 +3401,34 @@ create_grouping_paths(PlannerInfo *root,
 									  rollup_groupclauses);
 
 	/*
+	 * Number of groups estimated above is based on parent relation.  However
+	 * we need to estimate the number of groups for the child.  For that we
+	 * must know the number of partitions.  Find that and devise new estimate
+	 * for number of groups.
+	 *
+	 * FIXME: We might need to do this in get_number_of_groups() itself.  But
+	 * not sure at this time.  Need to revise the logic.
+	 */
+	if (IS_OTHER_REL(input_rel))
+	{
+		RelOptInfo *rel;
+
+		/* Find top-most parent rel */
+		if (IS_JOIN_REL(input_rel))
+			rel = find_join_rel(root, input_rel->top_parent_relids);
+		else
+			rel = find_base_rel(root,
+						bms_singleton_member(input_rel->top_parent_relids));
+
+		/*
+		 * Divide estimated number of groups by number of children to get
+		 * number of groups estimate for child rel.
+		 */
+		if (rel->part_scheme->nparts > 0)
+			dNumGroups = clamp_row_est(dNumGroups / rel->part_scheme->nparts);
+	}
+
+	/*
 	 * Determine whether it's possible to perform sort-based implementations
 	 * of grouping.  (Note that if groupClause is empty,
 	 * grouping_is_sortable() is trivially true, and all the
@@ -3441,6 +3493,11 @@ create_grouping_paths(PlannerInfo *root,
 		/* Insufficient support for partial mode. */
 		try_parallel_aggregation = false;
 	}
+	else if (IS_OTHER_REL(input_rel))
+	{
+		/* TODO: enable parallel query for partition-wise grouping. */
+		try_parallel_aggregation = false;
+	}
 	else
 	{
 		/* Everything looks good. */
@@ -3855,13 +3912,21 @@ create_grouping_paths(PlannerInfo *root,
 				 errdetail("Some of the datatypes only support hashing, while others only support sorting.")));
 
 	/*
+	 * If input relation is partitioned check if we can perform partition-wise
+	 * grouping and/or aggregation.
+	 */
+	try_partition_wise_grouping(root, input_rel, grouped_rel, target,
+								agg_costs, rollup_lists, rollup_groupclauses);
+
+	/*
 	 * If there is an FDW that's responsible for all baserels of the query,
 	 * let it consider adding ForeignPaths.
 	 */
 	if (grouped_rel->fdwroutine &&
 		grouped_rel->fdwroutine->GetForeignUpperPaths)
 		grouped_rel->fdwroutine->GetForeignUpperPaths(root, UPPERREL_GROUP_AGG,
-													  input_rel, grouped_rel);
+													  input_rel, grouped_rel,
+													  target);
 
 	/* Let extensions possibly add some more paths */
 	if (create_upper_paths_hook)
@@ -3885,6 +3950,191 @@ create_grouping_paths(PlannerInfo *root,
 }
 
 /*
+ * If the input relation is partitioned and the partition keys are leading
+ * group by clauses, each partition produces a different set of groups.
+ * Aggregates within each such group can be computed partition-wise. This
+ * might be optimal because of presence of suitable paths with pathkeys or
+ * because the hash tables for most of the partitions fit in the memory.
+ */
+static void
+try_partition_wise_grouping(PlannerInfo *root,
+							RelOptInfo *input_rel,
+							RelOptInfo *grouped_rel,
+							PathTarget *target,
+							const AggClauseCosts *agg_costs,
+							List *rollup_lists,
+							List *rollup_groupclauses)
+{
+	Query  *query = root->parse;
+	int		nparts;
+	int		cnt_parts;
+	PartitionScheme	part_scheme = input_rel->part_scheme;
+	RelOptInfo **part_rels;
+	List   *live_children = NIL;
+	PathTarget  *scanjoin_target;
+	ListCell *lc;
+
+	/* Nothing to do, if user disabled partition-wise aggregation. */
+	if (!enable_partition_wise_agg)
+		return;
+
+	/* Do not handle grouping sets for now.  */
+	if (rollup_groupclauses || rollup_lists)
+		return;
+
+	/* Nothing to do, if the input relation is not partitioned. */
+	if (!part_scheme)
+		return;
+
+	Assert(input_rel->part_rels);
+
+	/*
+	 * TODO: for now do nothing if partition keys are not leading group by
+	 * clauses. In general we may calculate partial aggregates for each
+	 * partition and combine them.
+	 */
+	if (!have_grouping_by_partkey(input_rel, target, query->groupClause))
+		return;
+
+	/* TODO: should be removed in final version */
+	elog(NOTICE, "partition-wise grouping is possible.");
+
+	nparts = part_scheme->nparts;
+	grouped_rel->part_scheme = input_rel->part_scheme;
+	part_rels = (RelOptInfo **) palloc(nparts * sizeof(RelOptInfo *));
+	grouped_rel->part_rels = part_rels;
+
+	/* Add paths for partition-wise aggregation/grouping. */
+	for (cnt_parts = 0; cnt_parts < nparts; cnt_parts++)
+	{
+		RelOptInfo *input_child_rel = input_rel->part_rels[cnt_parts];
+		PathTarget *child_target = copy_pathtarget(target);
+		List	   *appinfos = find_appinfos_by_relids(root,
+													   input_child_rel->relids);
+
+		/*
+		 * Now that there can be multiple grouping relations, if we have to
+		 * manage those in the root, we need separate identifiers for those.
+		 * What better identifier than the input relids themselves?
+		 */
+		part_rels[cnt_parts] = fetch_upper_rel(root, UPPERREL_GROUP_AGG,
+											   input_child_rel->relids);
+
+		/* Ignore empty children. They contribute nothing. */
+		if (IS_DUMMY_REL(input_child_rel))
+		{
+			mark_dummy_rel(part_rels[cnt_parts]);
+			continue;
+		}
+		else
+			live_children = lappend(live_children, part_rels[cnt_parts]);
+
+		/*
+		 * Forcibly apply scan/join target to all the Paths for the scan/join
+		 * rel.
+		 *
+		 * In principle we should re-run set_cheapest() here to identify the
+		 * cheapest path, but it seems unlikely that adding the same tlist
+		 * eval costs to all the paths would change that, so we don't bother.
+		 * Instead, just assume that the cheapest-startup and cheapest-total
+		 * paths remain so.  (There should be no parameterized paths anymore,
+		 * so we needn't worry about updating cheapest_parameterized_paths.)
+		 */
+		scanjoin_target = copy_pathtarget(input_rel->cheapest_startup_path->pathtarget);
+		scanjoin_target->exprs = (List *) adjust_appendrel_attrs(root, (Node *) scanjoin_target->exprs,
+																 appinfos);
+
+		foreach(lc, input_child_rel->pathlist)
+		{
+			Path	   *subpath = (Path *) lfirst(lc);
+			Path	   *path;
+
+			Assert(subpath->param_info == NULL);
+			path = apply_projection_to_path(root, input_child_rel,
+											subpath, scanjoin_target);
+			/* If we had to add a Result, path is different from subpath */
+			if (path != subpath)
+			{
+				lfirst(lc) = path;
+				if (subpath == input_child_rel->cheapest_startup_path)
+					input_child_rel->cheapest_startup_path = path;
+				if (subpath == input_child_rel->cheapest_total_path)
+					input_child_rel->cheapest_total_path = path;
+			}
+		}
+
+		/*
+		 * TODO:
+		 * We should somehow make this target available for FDWs, which are
+		 * expected to fetch it directly from root->upper_targets. That array
+		 * can hold only one target for each kind of upper rel. We will now
+		 * have many such upper relations.
+		 */
+		child_target->exprs = (List *) adjust_appendrel_attrs(root,
+														(Node *) target->exprs,
+																	 appinfos);
+
+		create_grouping_paths(root, input_child_rel, child_target, agg_costs,
+							  rollup_lists, rollup_groupclauses);
+
+	}
+
+	/*
+	 * add_paths_to_append_rel() sets the path target from the given relation.
+	 * In this case grouped_rel doesn't have a target set. So temporarily set
+	 * it.
+	 * TODO: probably we should do something better than this.
+	 */
+	grouped_rel->reltarget = target;
+	add_paths_to_append_rel(root, grouped_rel, live_children);
+	grouped_rel->reltarget = NULL;
+}
+
+/*
+ * Returns true if partition keys of the given relation are leading group by
+ * clauses.
+ */
+static bool
+have_grouping_by_partkey(RelOptInfo *input_rel, PathTarget *target,
+						 List *groupClause)
+{
+	PartitionScheme	part_scheme = input_rel->part_scheme;
+	ListCell   *lc;
+	List	   *tlist = make_tlist_from_pathtarget(target);
+	List	   *group_exprs = get_sortgrouplist_exprs(groupClause, tlist);
+	int			cnt_pk = 0;
+	int			num_pks;
+
+	/* Input relation should be partitioned. */
+	Assert(part_scheme);
+
+	num_pks = part_scheme->partnatts;
+
+	foreach(lc, group_exprs)
+	{
+		Expr   *group_expr = lfirst(lc);
+		List   *pk_exprs;
+
+		/* All partition keys are present in the group clause. */
+		if (cnt_pk >= num_pks)
+			return true;
+
+		pk_exprs = input_rel->partexprs[cnt_pk];
+
+		if (!list_member(pk_exprs, group_expr))
+			return false;
+
+		cnt_pk++;
+	}
+
+	/* All partition keys are present in the group clause. */
+	if (cnt_pk >= num_pks)
+		return true;
+
+	return false;
+}
+
+/*
  * create_window_paths
  *
  * Build a new upperrel containing Paths for window-function evaluation.
@@ -3959,7 +4209,8 @@ create_window_paths(PlannerInfo *root,
 	if (window_rel->fdwroutine &&
 		window_rel->fdwroutine->GetForeignUpperPaths)
 		window_rel->fdwroutine->GetForeignUpperPaths(root, UPPERREL_WINDOW,
-													 input_rel, window_rel);
+													 input_rel, window_rel,
+													 NULL);
 
 	/* Let extensions possibly add some more paths */
 	if (create_upper_paths_hook)
@@ -4263,7 +4514,8 @@ create_distinct_paths(PlannerInfo *root,
 	if (distinct_rel->fdwroutine &&
 		distinct_rel->fdwroutine->GetForeignUpperPaths)
 		distinct_rel->fdwroutine->GetForeignUpperPaths(root, UPPERREL_DISTINCT,
-													input_rel, distinct_rel);
+													input_rel, distinct_rel,
+													NULL);
 
 	/* Let extensions possibly add some more paths */
 	if (create_upper_paths_hook)
@@ -4405,7 +4657,8 @@ create_ordered_paths(PlannerInfo *root,
 	if (ordered_rel->fdwroutine &&
 		ordered_rel->fdwroutine->GetForeignUpperPaths)
 		ordered_rel->fdwroutine->GetForeignUpperPaths(root, UPPERREL_ORDERED,
-													  input_rel, ordered_rel);
+													  input_rel, ordered_rel,
+													  NULL);
 
 	/* Let extensions possibly add some more paths */
 	if (create_upper_paths_hook)
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 7f423c9..0cce5e4 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -920,6 +920,15 @@ static struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
+	{
+		{"enable_partition_wise_agg", PGC_USERSET, QUERY_TUNING_METHOD,
+			gettext_noop("Enables partition-wise aggregation and grouping."),
+			NULL
+		},
+		&enable_partition_wise_agg,
+		true,
+		NULL, NULL, NULL
+	},
 
 	{
 		{"geqo", PGC_USERSET, QUERY_TUNING_GEQO,
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 6ca44f7..10fdf6d 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -62,7 +62,8 @@ typedef void (*GetForeignJoinPaths_function) (PlannerInfo *root,
 typedef void (*GetForeignUpperPaths_function) (PlannerInfo *root,
 													 UpperRelationKind stage,
 													   RelOptInfo *input_rel,
-													 RelOptInfo *output_rel);
+													 RelOptInfo *output_rel,
+													 PathTarget *target);
 
 typedef void (*AddForeignUpdateTargets_function) (Query *parsetree,
 												   RangeTblEntry *target_rte,
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index e7949d37..7cd8e2c 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -68,6 +68,7 @@ extern bool enable_mergejoin;
 extern bool enable_hashjoin;
 extern bool enable_gathermerge;
 extern bool enable_partition_wise_join;
+extern bool enable_partition_wise_agg;
 extern int	constraint_exclusion;
 
 extern double clamp_row_est(double nrows);
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index f31b70e..77388cc 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -60,6 +60,8 @@ extern void create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel,
 										Path *bitmapqual);
 extern void generate_partition_wise_join_paths(PlannerInfo *root,
 											   RelOptInfo *rel);
+extern void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
+						List *live_childrels);
 
 #ifdef OPTIMIZER_DEBUG
 extern void debug_print_rel(PlannerInfo *root, RelOptInfo *rel);
diff --git a/src/test/regress/expected/partition_agg.out b/src/test/regress/expected/partition_agg.out
new file mode 100644
index 0000000..315d396
--- /dev/null
+++ b/src/test/regress/expected/partition_agg.out
@@ -0,0 +1,192 @@
+--
+-- PARTITION_AGG
+-- Test partition-wise aggregation on partitioned tables
+--
+-- Enable partition-wise join, which by default is disabled.
+SET enable_partition_wise_join TO true;
+--
+-- Tests for list partitioned tables.
+--
+CREATE TABLE ptab1 (a int, b int, c text) PARTITION BY LIST(c);
+CREATE TABLE ptab1_p1 PARTITION OF ptab1 FOR VALUES IN ('0000', '0003', '0004', '0010');
+CREATE TABLE ptab1_p2 PARTITION OF ptab1 FOR VALUES IN ('0001', '0005', '0002', '0009');
+CREATE TABLE ptab1_p3 PARTITION OF ptab1 FOR VALUES IN ('0006', '0007', '0008', '0011');
+INSERT INTO ptab1 SELECT i, i, to_char(i/50, 'FM0000') FROM generate_series(0, 599, 2) i;
+ANALYZE ptab1;
+ANALYZE ptab1_p1;
+ANALYZE ptab1_p2;
+ANALYZE ptab1_p3;
+-- TODO: This table is created only for testing the results. Remove once
+-- results are tested.
+CREATE TABLE uptab1 AS SELECT * FROM ptab1;
+ANALYZE uptab1;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT c, sum(a), avg(b), COUNT(*) FROM ptab1 GROUP BY c ORDER BY 1, 2, 3;
+NOTICE:  partition-wise grouping is possible.
+                                  QUERY PLAN                                  
+------------------------------------------------------------------------------
+ Sort
+   Output: ptab1_p1.c, (sum(ptab1_p1.a)), (avg(ptab1_p1.b)), (count(*))
+   Sort Key: ptab1_p1.c, (sum(ptab1_p1.a)), (avg(ptab1_p1.b))
+   ->  Append
+         ->  HashAggregate
+               Output: ptab1_p1.c, sum(ptab1_p1.a), avg(ptab1_p1.b), count(*)
+               Group Key: ptab1_p1.c
+               ->  Seq Scan on public.ptab1_p1
+                     Output: ptab1_p1.c, ptab1_p1.a, ptab1_p1.b
+         ->  HashAggregate
+               Output: ptab1_p2.c, sum(ptab1_p2.a), avg(ptab1_p2.b), count(*)
+               Group Key: ptab1_p2.c
+               ->  Seq Scan on public.ptab1_p2
+                     Output: ptab1_p2.c, ptab1_p2.a, ptab1_p2.b
+         ->  HashAggregate
+               Output: ptab1_p3.c, sum(ptab1_p3.a), avg(ptab1_p3.b), count(*)
+               Group Key: ptab1_p3.c
+               ->  Seq Scan on public.ptab1_p3
+                     Output: ptab1_p3.c, ptab1_p3.a, ptab1_p3.b
+(19 rows)
+
+SELECT c, sum(a), avg(b), COUNT(*) FROM ptab1 GROUP BY c ORDER BY 1, 2, 3;
+NOTICE:  partition-wise grouping is possible.
+  c   |  sum  |         avg          | count 
+------+-------+----------------------+-------
+ 0000 |   600 |  24.0000000000000000 |    25
+ 0001 |  1850 |  74.0000000000000000 |    25
+ 0002 |  3100 | 124.0000000000000000 |    25
+ 0003 |  4350 | 174.0000000000000000 |    25
+ 0004 |  5600 | 224.0000000000000000 |    25
+ 0005 |  6850 | 274.0000000000000000 |    25
+ 0006 |  8100 | 324.0000000000000000 |    25
+ 0007 |  9350 | 374.0000000000000000 |    25
+ 0008 | 10600 | 424.0000000000000000 |    25
+ 0009 | 11850 | 474.0000000000000000 |    25
+ 0010 | 13100 | 524.0000000000000000 |    25
+ 0011 | 14350 | 574.0000000000000000 |    25
+(12 rows)
+
+SELECT c, sum(a), avg(b), COUNT(*) FROM uptab1 GROUP BY c ORDER BY 1, 2, 3;
+  c   |  sum  |         avg          | count 
+------+-------+----------------------+-------
+ 0000 |   600 |  24.0000000000000000 |    25
+ 0001 |  1850 |  74.0000000000000000 |    25
+ 0002 |  3100 | 124.0000000000000000 |    25
+ 0003 |  4350 | 174.0000000000000000 |    25
+ 0004 |  5600 | 224.0000000000000000 |    25
+ 0005 |  6850 | 274.0000000000000000 |    25
+ 0006 |  8100 | 324.0000000000000000 |    25
+ 0007 |  9350 | 374.0000000000000000 |    25
+ 0008 | 10600 | 424.0000000000000000 |    25
+ 0009 | 11850 | 474.0000000000000000 |    25
+ 0010 | 13100 | 524.0000000000000000 |    25
+ 0011 | 14350 | 574.0000000000000000 |    25
+(12 rows)
+
+-- JOIN query
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c, sum(t1.a), avg(t1.b), COUNT(*) FROM ptab1 t1, ptab1 t2 WHERE t1.c = t2.c GROUP BY t1.c ORDER BY 1, 2, 3;
+NOTICE:  partition-wise grouping is possible.
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Sort
+   Output: t1.c, (sum(t1.a)), (avg(t1.b)), (count(*))
+   Sort Key: t1.c, (sum(t1.a)), (avg(t1.b))
+   ->  Append
+         ->  HashAggregate
+               Output: t1.c, sum(t1.a), avg(t1.b), count(*)
+               Group Key: t1.c
+               ->  Hash Join
+                     Output: t1.c, t1.a, t1.b
+                     Hash Cond: (t1.c = t2.c)
+                     ->  Seq Scan on public.ptab1_p1 t1
+                           Output: t1.c, t1.a, t1.b
+                     ->  Hash
+                           Output: t2.c
+                           ->  Seq Scan on public.ptab1_p1 t2
+                                 Output: t2.c
+         ->  HashAggregate
+               Output: t1_1.c, sum(t1_1.a), avg(t1_1.b), count(*)
+               Group Key: t1_1.c
+               ->  Hash Join
+                     Output: t1_1.c, t1_1.a, t1_1.b
+                     Hash Cond: (t1_1.c = t2_1.c)
+                     ->  Seq Scan on public.ptab1_p2 t1_1
+                           Output: t1_1.c, t1_1.a, t1_1.b
+                     ->  Hash
+                           Output: t2_1.c
+                           ->  Seq Scan on public.ptab1_p2 t2_1
+                                 Output: t2_1.c
+         ->  HashAggregate
+               Output: t1_2.c, sum(t1_2.a), avg(t1_2.b), count(*)
+               Group Key: t1_2.c
+               ->  Hash Join
+                     Output: t1_2.c, t1_2.a, t1_2.b
+                     Hash Cond: (t1_2.c = t2_2.c)
+                     ->  Seq Scan on public.ptab1_p3 t1_2
+                           Output: t1_2.c, t1_2.a, t1_2.b
+                     ->  Hash
+                           Output: t2_2.c
+                           ->  Seq Scan on public.ptab1_p3 t2_2
+                                 Output: t2_2.c
+(40 rows)
+
+SELECT t1.c, sum(t1.a), avg(t1.b), COUNT(*) FROM ptab1 t1, ptab1 t2 WHERE t1.c = t2.c GROUP BY t1.c ORDER BY 1, 2, 3;
+NOTICE:  partition-wise grouping is possible.
+  c   |  sum   |         avg          | count 
+------+--------+----------------------+-------
+ 0000 |  15000 |  24.0000000000000000 |   625
+ 0001 |  46250 |  74.0000000000000000 |   625
+ 0002 |  77500 | 124.0000000000000000 |   625
+ 0003 | 108750 | 174.0000000000000000 |   625
+ 0004 | 140000 | 224.0000000000000000 |   625
+ 0005 | 171250 | 274.0000000000000000 |   625
+ 0006 | 202500 | 324.0000000000000000 |   625
+ 0007 | 233750 | 374.0000000000000000 |   625
+ 0008 | 265000 | 424.0000000000000000 |   625
+ 0009 | 296250 | 474.0000000000000000 |   625
+ 0010 | 327500 | 524.0000000000000000 |   625
+ 0011 | 358750 | 574.0000000000000000 |   625
+(12 rows)
+
+SELECT t1.c, sum(t1.a), avg(t1.b), COUNT(*) FROM uptab1 t1, uptab1 t2 WHERE t1.c = t2.c GROUP BY t1.c ORDER BY 1, 2, 3;
+  c   |  sum   |         avg          | count 
+------+--------+----------------------+-------
+ 0000 |  15000 |  24.0000000000000000 |   625
+ 0001 |  46250 |  74.0000000000000000 |   625
+ 0002 |  77500 | 124.0000000000000000 |   625
+ 0003 | 108750 | 174.0000000000000000 |   625
+ 0004 | 140000 | 224.0000000000000000 |   625
+ 0005 | 171250 | 274.0000000000000000 |   625
+ 0006 | 202500 | 324.0000000000000000 |   625
+ 0007 | 233750 | 374.0000000000000000 |   625
+ 0008 | 265000 | 424.0000000000000000 |   625
+ 0009 | 296250 | 474.0000000000000000 |   625
+ 0010 | 327500 | 524.0000000000000000 |   625
+ 0011 | 358750 | 574.0000000000000000 |   625
+(12 rows)
+
+-- Negative testcase
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT COUNT(*) FROM ptab1 GROUP BY a;
+               QUERY PLAN                
+-----------------------------------------
+ HashAggregate
+   Output: count(*), ptab1.a
+   Group Key: ptab1.a
+   ->  Append
+         ->  Seq Scan on public.ptab1
+               Output: ptab1.a
+         ->  Seq Scan on public.ptab1_p1
+               Output: ptab1_p1.a
+         ->  Seq Scan on public.ptab1_p2
+               Output: ptab1_p2.a
+         ->  Seq Scan on public.ptab1_p3
+               Output: ptab1_p3.a
+(12 rows)
+
+-- Cleanup
+DROP TABLE uptab1;
+DROP TABLE ptab1_p3;
+DROP TABLE ptab1_p2;
+DROP TABLE ptab1_p1;
+DROP TABLE ptab1;
+RESET enable_partition_wise_join;
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 27f09fa..67c2041 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1399,44 +1399,49 @@ ANALYZE plt1_e;
 -- test partition matching with N-way join
 EXPLAIN (COSTS OFF)
 SELECT avg(t1.a), avg(t2.b), avg(t3.a + t3.b), t1.c, t2.c, t3.c FROM plt1 t1, plt2 t2, plt1_e t3 WHERE t1.c = t2.c AND ltrim(t3.c, 'A') = t1.c GROUP BY t1.c, t2.c, t3.c ORDER BY t1.c, t2.c, t3.c;
-                                      QUERY PLAN                                      
---------------------------------------------------------------------------------------
+NOTICE:  partition-wise grouping is possible.
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
  Sort
    Sort Key: t1.c, t3.c
-   ->  HashAggregate
-         Group Key: t1.c, t2.c, t3.c
-         ->  Result
-               ->  Append
-                     ->  Hash Join
-                           Hash Cond: (t1.c = t2.c)
-                           ->  Seq Scan on plt1_p1 t1
-                           ->  Hash
-                                 ->  Hash Join
-                                       Hash Cond: (t2.c = ltrim(t3.c, 'A'::text))
-                                       ->  Seq Scan on plt2_p1 t2
-                                       ->  Hash
-                                             ->  Seq Scan on plt1_e_p1 t3
-                     ->  Hash Join
-                           Hash Cond: (t1_1.c = t2_1.c)
-                           ->  Seq Scan on plt1_p2 t1_1
-                           ->  Hash
-                                 ->  Hash Join
-                                       Hash Cond: (t2_1.c = ltrim(t3_1.c, 'A'::text))
-                                       ->  Seq Scan on plt2_p2 t2_1
-                                       ->  Hash
-                                             ->  Seq Scan on plt1_e_p2 t3_1
-                     ->  Hash Join
-                           Hash Cond: (t1_2.c = t2_2.c)
-                           ->  Seq Scan on plt1_p3 t1_2
-                           ->  Hash
-                                 ->  Hash Join
-                                       Hash Cond: (t2_2.c = ltrim(t3_2.c, 'A'::text))
-                                       ->  Seq Scan on plt2_p3 t2_2
-                                       ->  Hash
-                                             ->  Seq Scan on plt1_e_p3 t3_2
-(33 rows)
+   ->  Append
+         ->  HashAggregate
+               Group Key: t1.c, t2.c, t3.c
+               ->  Hash Join
+                     Hash Cond: (t1.c = t2.c)
+                     ->  Seq Scan on plt1_p1 t1
+                     ->  Hash
+                           ->  Hash Join
+                                 Hash Cond: (t2.c = ltrim(t3.c, 'A'::text))
+                                 ->  Seq Scan on plt2_p1 t2
+                                 ->  Hash
+                                       ->  Seq Scan on plt1_e_p1 t3
+         ->  HashAggregate
+               Group Key: t1_1.c, t2_1.c, t3_1.c
+               ->  Hash Join
+                     Hash Cond: (t1_1.c = t2_1.c)
+                     ->  Seq Scan on plt1_p2 t1_1
+                     ->  Hash
+                           ->  Hash Join
+                                 Hash Cond: (t2_1.c = ltrim(t3_1.c, 'A'::text))
+                                 ->  Seq Scan on plt2_p2 t2_1
+                                 ->  Hash
+                                       ->  Seq Scan on plt1_e_p2 t3_1
+         ->  HashAggregate
+               Group Key: t1_2.c, t2_2.c, t3_2.c
+               ->  Hash Join
+                     Hash Cond: (t1_2.c = t2_2.c)
+                     ->  Seq Scan on plt1_p3 t1_2
+                     ->  Hash
+                           ->  Hash Join
+                                 Hash Cond: (t2_2.c = ltrim(t3_2.c, 'A'::text))
+                                 ->  Seq Scan on plt2_p3 t2_2
+                                 ->  Hash
+                                       ->  Seq Scan on plt1_e_p3 t3_2
+(36 rows)
 
 SELECT avg(t1.a), avg(t2.b), avg(t3.a + t3.b), t1.c, t2.c, t3.c FROM plt1 t1, plt2 t2, plt1_e t3 WHERE t1.c = t2.c AND ltrim(t3.c, 'A') = t1.c GROUP BY t1.c, t2.c, t3.c ORDER BY t1.c, t2.c, t3.c;
+NOTICE:  partition-wise grouping is possible.
          avg          |         avg          |          avg          |  c   |  c   |   c   
 ----------------------+----------------------+-----------------------+------+------+-------
   24.0000000000000000 |  24.0000000000000000 |   48.0000000000000000 | 0000 | 0000 | A0000
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index cd1f7f3..f4cd466 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -81,11 +81,12 @@ select name, setting from pg_settings where name like 'enable%';
  enable_material            | on
  enable_mergejoin           | on
  enable_nestloop            | on
+ enable_partition_wise_agg  | on
  enable_partition_wise_join | off
  enable_seqscan             | on
  enable_sort                | on
  enable_tidscan             | on
-(13 rows)
+(14 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 966984d..56c07d3 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -104,6 +104,10 @@ test: publication subscription
 # Another group of parallel tests
 # ----------
 test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock json jsonb json_encoding indirect_toast equivclass partition_join multi_level_partition_join
+# TODO: should be added in parallel tests above, but before that need to make
+# sure we have unique objects to avoid any concurrency issues.
+test: partition_agg
+
 # ----------
 # Another group of parallel tests
 # NB: temp.sql does a reconnect which transiently uses 2 connections,
diff --git a/src/test/regress/sql/partition_agg.sql b/src/test/regress/sql/partition_agg.sql
new file mode 100644
index 0000000..df6dd9c
--- /dev/null
+++ b/src/test/regress/sql/partition_agg.sql
@@ -0,0 +1,48 @@
+--
+-- PARTITION_AGG
+-- Test partition-wise aggregation on partitioned tables
+--
+
+-- Enable partition-wise join, which by default is disabled.
+SET enable_partition_wise_join TO true;
+
+--
+-- Tests for list partitioned tables.
+--
+CREATE TABLE ptab1 (a int, b int, c text) PARTITION BY LIST(c);
+CREATE TABLE ptab1_p1 PARTITION OF ptab1 FOR VALUES IN ('0000', '0003', '0004', '0010');
+CREATE TABLE ptab1_p2 PARTITION OF ptab1 FOR VALUES IN ('0001', '0005', '0002', '0009');
+CREATE TABLE ptab1_p3 PARTITION OF ptab1 FOR VALUES IN ('0006', '0007', '0008', '0011');
+INSERT INTO ptab1 SELECT i, i, to_char(i/50, 'FM0000') FROM generate_series(0, 599, 2) i;
+ANALYZE ptab1;
+ANALYZE ptab1_p1;
+ANALYZE ptab1_p2;
+ANALYZE ptab1_p3;
+-- TODO: This table is created only for testing the results. Remove once
+-- results are tested.
+CREATE TABLE uptab1 AS SELECT * FROM ptab1;
+ANALYZE uptab1;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT c, sum(a), avg(b), COUNT(*) FROM ptab1 GROUP BY c ORDER BY 1, 2, 3;
+SELECT c, sum(a), avg(b), COUNT(*) FROM ptab1 GROUP BY c ORDER BY 1, 2, 3;
+SELECT c, sum(a), avg(b), COUNT(*) FROM uptab1 GROUP BY c ORDER BY 1, 2, 3;
+
+-- JOIN query
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c, sum(t1.a), avg(t1.b), COUNT(*) FROM ptab1 t1, ptab1 t2 WHERE t1.c = t2.c GROUP BY t1.c ORDER BY 1, 2, 3;
+SELECT t1.c, sum(t1.a), avg(t1.b), COUNT(*) FROM ptab1 t1, ptab1 t2 WHERE t1.c = t2.c GROUP BY t1.c ORDER BY 1, 2, 3;
+SELECT t1.c, sum(t1.a), avg(t1.b), COUNT(*) FROM uptab1 t1, uptab1 t2 WHERE t1.c = t2.c GROUP BY t1.c ORDER BY 1, 2, 3;
+
+
+-- Negative testcase
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT COUNT(*) FROM ptab1 GROUP BY a;
+
+-- Cleanup
+DROP TABLE uptab1;
+DROP TABLE ptab1_p3;
+DROP TABLE ptab1_p2;
+DROP TABLE ptab1_p1;
+DROP TABLE ptab1;
+RESET enable_partition_wise_join;
