Proposal : Parallel Merge Join

Started by Dilip Kumarabout 9 years ago39 messages
#1Dilip Kumar
dilipbalaut@gmail.com
3 attachment(s)

I would like to propose a patch for parallelizing merge join path.
This idea is derived by analyzing TPCH results.

I have done this analysis along with my colleagues Rafia sabih and Amit kaplia.

Currently we already have infrastructure for executing parallel join,
so we don't need any change at executor level. Only in path
generation, we need to add partial paths for merge join, like we do
for nest loop and hash join.

After this patch, we will get some new type of paths as shown below.

-> Gather
-> MergeJoin
-> Sort
-> Partial Seq Scan
-> Any parallel safe inner path
or
-> Gather
-> MergeJoin
-> Partial Index Scan path (or any sorted partial path)
-> Any parallel safe inner path

Major benefit of this patch is, it can be helpful in converting some
paths to parallel paths where they were not using parallelism at all,
may be because we need merge join at intermediate level.

Performance:
------------------
- I applied this patch on head and tested TPCH (as of now only with
scale factor 10). And, observed that Q20 is giving 2x gain. On head,
this query was not using parallelism at all, but after parallel merge,
it's able to use parallelism all the way up to top level join (explain
analyze result is attached).

- I need to test this with different scale factor and also with other
patches like parallel index scan, gather merge and I hope to see more
paths getting benefited.

* 'gather merge' will be useful where we expect sorted path from merge
join, because parallel merge join will not give sorted result.

Test configuration and machine detail:
--------------------------------------------------
TPCH: scale factor : 10
work_mem : 100MB
shared buffer : 1GB
max_parallel_workers_per_gather : 3
Machine : POWER 4 socket machine.

Attached file:
------------------
1. parallel_mergejooin_v1.patch : parallel merge join patch
2. 20_head.out : Explain analyze output on head (median of 3 runs)
3. 20_patch.out : Explain analyze output with patch (median of 3 runs)

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

20_head.outapplication/octet-stream; name=20_head.outDownload
20_patch.outapplication/octet-stream; name=20_patch.outDownload
parallel_mergejoin_v1.patchapplication/octet-stream; name=parallel_mergejoin_v1.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 96f00fc..1bb36c6 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -50,7 +50,17 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
-
+static void generate_mergejoin_paths(PlannerInfo *root,
+						 RelOptInfo *joinrel,
+						 RelOptInfo *innerrel,
+						 Path *outerpath,
+						 JoinType jointype,
+						 JoinType save_jointype,
+						 JoinPathExtraData *extra,
+						 bool useallclauses,
+						 Path *inner_cheapest_total,
+						 List *merge_pathkeys,
+						 bool is_partial);
 
 /*
  * add_paths_to_joinrel
@@ -472,6 +482,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+				   RelOptInfo *joinrel,
+				   Path *outer_path,
+				   Path *inner_path,
+				   List *pathkeys,
+				   List *mergeclauses,
+				   List *outersortkeys,
+				   List *innersortkeys,
+				   JoinType jointype,
+				   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_subset(inner_paramrels, outer_path->parent->relids))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+			 create_mergejoin_path(root,
+								   joinrel,
+								   jointype,
+								   &workspace,
+								   extra->sjinfo,
+								   outer_path,
+								   inner_path,
+								   extra->restrictlist,
+								   pathkeys,
+								   NULL,
+								   mergeclauses,
+								   outersortkeys,
+								   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -640,6 +720,7 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
 	List	   *all_pathkeys;
@@ -773,6 +854,37 @@ sort_inner_and_outer(PlannerInfo *root,
 						   innerkeys,
 						   jointype,
 						   extra);
+
+		/*
+		 * If the joinrel is parallel-safe, we may be able to consider a
+		 * partial merge join.  However, we can't handle JOIN_UNIQUE_OUTER,
+		 * because the outer path will be partial, and therefore we won't be
+		 * able to properly guarantee uniqueness.  Similarly, we can't handle
+		 * JOIN_FULL and JOIN_RIGHT, because they can produce false null
+		 * extended rows.  Also, the resulting path must not be parameterized.
+		 */
+		if (joinrel->consider_parallel &&
+			save_jointype != JOIN_UNIQUE_OUTER &&
+			jointype != JOIN_FULL &&
+			jointype != JOIN_RIGHT &&
+			outerrel->partial_pathlist != NIL &&
+			bms_is_empty(joinrel->lateral_relids) &&
+			inner_path->parallel_safe)
+		{
+			Path *cheapest_partial_outer =
+							(Path *) linitial(outerrel->partial_pathlist);
+
+			try_partial_mergejoin_path(root,
+							   joinrel,
+							   cheapest_partial_outer,
+							   inner_path,
+							   merge_pathkeys,
+							   cur_mergeclauses,
+							   outerkeys,
+							   innerkeys,
+							   jointype,
+							   extra);
+		}
 	}
 }
 
@@ -790,15 +902,7 @@ sort_inner_and_outer(PlannerInfo *root,
  * cheapest-total inner-indexscan path (if any), and one on the
  * cheapest-startup inner-indexscan path (if different).
  *
- * We also consider mergejoins if mergejoin clauses are available.  We have
- * two ways to generate the inner path for a mergejoin: sort the cheapest
- * inner path, or use an inner path that is already suitably ordered for the
- * merge.  If we have several mergeclauses, it could be that there is no inner
- * path (or only a very expensive one) for the full list of mergeclauses, but
- * better paths exist if we truncate the mergeclause list (thereby discarding
- * some sort key requirements).  So, we consider truncations of the
- * mergeclause list as well as the full list.  (Ideally we'd consider all
- * subsets of the mergeclause list, but that seems way too expensive.)
+ * We also consider mergejoins if mergejoin clauses are available.
  *
  * 'joinrel' is the join relation
  * 'outerrel' is the outer join relation
@@ -894,13 +998,6 @@ match_unsorted_outer(PlannerInfo *root,
 	{
 		Path	   *outerpath = (Path *) lfirst(lc1);
 		List	   *merge_pathkeys;
-		List	   *mergeclauses;
-		List	   *innersortkeys;
-		List	   *trialsortkeys;
-		Path	   *cheapest_startup_inner;
-		Path	   *cheapest_total_inner;
-		int			num_sortkeys;
-		int			sortkeycnt;
 
 		/*
 		 * We cannot use an outer path that is parameterized by the inner rel.
@@ -986,139 +1083,318 @@ match_unsorted_outer(PlannerInfo *root,
 		if (inner_cheapest_total == NULL)
 			continue;
 
-		/* Look for useful mergeclauses (if any) */
-		mergeclauses = find_mergeclauses_for_pathkeys(root,
-													  outerpath->pathkeys,
-													  true,
-													extra->mergeclause_list);
+		/* Generate merge join paths for the outer path */
+		generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
+								jointype, save_jointype, extra, useallclauses,
+								inner_cheapest_total, merge_pathkeys, false);
+	}
 
-		/*
-		 * Done with this outer path if no chance for a mergejoin.
-		 *
-		 * Special corner case: for "x FULL JOIN y ON true", there will be no
-		 * join clauses at all.  Ordinarily we'd generate a clauseless
-		 * nestloop path, but since mergejoin is our only join type that
-		 * supports FULL JOIN without any join clauses, it's necessary to
-		 * generate a clauseless mergejoin path instead.
-		 */
-		if (mergeclauses == NIL)
+	/*
+	 * Consider partial nestloop and mergejoin plan if the joinrel is
+	 * parallel-safe. However, we can't handle JOIN_UNIQUE_OUTER, because
+	 * the outer path will be partial, and therefore we won't be able to
+	 * properly guarantee uniqueness.  Nor can we handle extra_lateral_rels,
+	 * since partial paths must not be parameterized.
+	 * Similarly, we can't handle JOIN_FULL and JOIN_RIGHT, because they
+	 * can produce false null extended rows.
+	 */
+	if (!joinrel->consider_parallel ||
+		save_jointype == JOIN_UNIQUE_OUTER ||
+		!bms_is_empty(joinrel->lateral_relids) ||
+		jointype == JOIN_FULL ||
+		jointype == JOIN_RIGHT)
+		return;
+
+	if (nestjoinOK)
+		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
+								   save_jointype, extra);
+
+	/* Can't generate mergejoin path if inner rel is parameterized by outer */
+	if (inner_cheapest_total != NULL)
+	{
+		ListCell   *lc1;
+		JoinType	save_jointype = jointype;
+
+		if (jointype == JOIN_UNIQUE_INNER)
+			jointype = JOIN_INNER;
+
+		/* generate merge join path for each partial outer path */
+		foreach(lc1, outerrel->partial_pathlist)
 		{
-			if (jointype == JOIN_FULL)
-				 /* okay to try for mergejoin */ ;
-			else
-				continue;
+			Path	   *outerpath = (Path *) lfirst(lc1);
+			List	   *merge_pathkeys;
+
+			/*
+			 * Figure out what useful ordering any paths we create
+			 * will have.
+			 */
+			merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											   outerpath->pathkeys);
+
+			generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
+									jointype, save_jointype, extra, false,
+									inner_cheapest_total, merge_pathkeys,
+									true);
 		}
-		if (useallclauses && list_length(mergeclauses) != list_length(extra->mergeclause_list))
-			continue;
 
-		/* Compute the required ordering of the inner path */
-		innersortkeys = make_inner_pathkeys_for_merge(root,
-													  mergeclauses,
-													  outerpath->pathkeys);
+	}
+}
 
-		/*
-		 * Generate a mergejoin on the basis of sorting the cheapest inner.
-		 * Since a sort will be needed, only cheapest total cost matters. (But
-		 * try_mergejoin_path will do the right thing if inner_cheapest_total
-		 * is already correctly sorted.)
-		 */
+/*
+ * generate_mergejoin_paths
+ *	Creates possible mergejoin paths for input outerpath.
+ *
+ * We generate mergejoins if mergejoin clauses are available.  We have
+ * two ways to generate the inner path for a mergejoin: sort the cheapest
+ * inner path, or use an inner path that is already suitably ordered for the
+ * merge.  If we have several mergeclauses, it could be that there is no inner
+ * path (or only a very expensive one) for the full list of mergeclauses, but
+ * better paths exist if we truncate the mergeclause list (thereby discarding
+ * some sort key requirements).  So, we consider truncations of the
+ * mergeclause list as well as the full list.  (Ideally we'd consider all
+ * subsets of the mergeclause list, but that seems way too expensive.)
+ *
+ * If is_partial is true then caller will make sure that jointype is neither
+ * FULL JOIN nor RIGHT JOIN
+ *
+ * 'useallclauses' only true in case of JOIN_FULL or JOIN_RIGHT
+ * 'inner_cheapest_total' cheapest total path of inner relation
+ *  'is_partial' generate partial path if this flag is set or else normal path
+ */
+static void
+generate_mergejoin_paths(PlannerInfo *root,
+					 RelOptInfo *joinrel,
+					 RelOptInfo *innerrel,
+					 Path *outerpath,
+					 JoinType jointype,
+					 JoinType save_jointype,
+					 JoinPathExtraData *extra,
+					 bool useallclauses,
+					 Path *inner_cheapest_total,
+					 List *merge_pathkeys,
+					 bool is_partial)
+{
+	List	   *mergeclauses;
+	List	   *innersortkeys;
+	List	   *trialsortkeys;
+	Path	   *cheapest_startup_inner;
+	Path	   *cheapest_total_inner;
+	int			num_sortkeys;
+	int			sortkeycnt;
+
+	/* Look for useful mergeclauses (if any) */
+	mergeclauses = find_mergeclauses_for_pathkeys(root,
+												  outerpath->pathkeys,
+												  true,
+												extra->mergeclause_list);
+
+	/*
+	 * Done with this outer path if no chance for a mergejoin.
+	 *
+	 * Special corner case: for "x FULL JOIN y ON true", there will be no
+	 * join clauses at all.  Ordinarily we'd generate a clauseless
+	 * nestloop path, but since mergejoin is our only join type that
+	 * supports FULL JOIN without any join clauses, it's necessary to
+	 * generate a clauseless mergejoin path instead.
+	 */
+	if (mergeclauses == NIL)
+	{
+		if (jointype == JOIN_FULL)
+			 /* okay to try for mergejoin */ ;
+		else
+			return;
+	}
+	if (useallclauses &&
+			list_length(mergeclauses) != list_length(extra->mergeclause_list))
+		return;
+
+	/* Compute the required ordering of the inner path */
+	innersortkeys = make_inner_pathkeys_for_merge(root,
+												  mergeclauses,
+												  outerpath->pathkeys);
+
+	/*
+	 * Generate a mergejoin on the basis of sorting the cheapest inner.
+	 * Since a sort will be needed, only cheapest total cost matters. (But
+	 * try_mergejoin_path will do the right thing if inner_cheapest_total
+	 * is already correctly sorted.)
+	 */
+	if (!is_partial)
 		try_mergejoin_path(root,
-						   joinrel,
-						   outerpath,
-						   inner_cheapest_total,
-						   merge_pathkeys,
-						   mergeclauses,
-						   NIL,
-						   innersortkeys,
-						   jointype,
-						   extra);
+						joinrel,
+						outerpath,
+						inner_cheapest_total,
+						merge_pathkeys,
+						mergeclauses,
+						NIL,
+						innersortkeys,
+						jointype,
+						extra);
+	/* Generate partial path if inner is parallel safe. */
+	else if (inner_cheapest_total->parallel_safe)
+		try_partial_mergejoin_path(root,
+						joinrel,
+						outerpath,
+						inner_cheapest_total,
+						merge_pathkeys,
+						mergeclauses,
+						NIL,
+						innersortkeys,
+						jointype,
+						extra);
+
+	/* Can't do anything else if inner path needs to be unique'd */
+	if (jointype == JOIN_UNIQUE_INNER)
+		return;
 
-		/* Can't do anything else if inner path needs to be unique'd */
-		if (save_jointype == JOIN_UNIQUE_INNER)
-			continue;
+	/*
+	 * Look for presorted inner paths that satisfy the innersortkey list
+	 * --- or any truncation thereof, if we are allowed to build a
+	 * mergejoin using a subset of the merge clauses.  Here, we consider
+	 * both cheap startup cost and cheap total cost.
+	 *
+	 * Currently we do not consider parameterized inner paths here. This
+	 * interacts with decisions elsewhere that also discriminate against
+	 * mergejoins with parameterized inputs; see comments in
+	 * src/backend/optimizer/README.
+	 *
+	 * As we shorten the sortkey list, we should consider only paths that
+	 * are strictly cheaper than (in particular, not the same as) any path
+	 * found in an earlier iteration.  Otherwise we'd be intentionally
+	 * using fewer merge keys than a given path allows (treating the rest
+	 * as plain joinquals), which is unlikely to be a good idea.  Also,
+	 * eliminating paths here on the basis of compare_path_costs is a lot
+	 * cheaper than building the mergejoin path only to throw it away.
+	 *
+	 * If inner_cheapest_total is well enough sorted to have not required
+	 * a sort in the path made above, we shouldn't make a duplicate path
+	 * with it, either.  We handle that case with the same logic that
+	 * handles the previous consideration, by initializing the variables
+	 * that track cheapest-so-far properly.  Note that we do NOT reject
+	 * inner_cheapest_total if we find it matches some shorter set of
+	 * pathkeys.  That case corresponds to using fewer mergekeys to avoid
+	 * sorting inner_cheapest_total, whereas we did sort it above, so the
+	 * plans being considered are different.
+	 */
+	if (pathkeys_contained_in(innersortkeys,
+							  inner_cheapest_total->pathkeys))
+	{
+		/* inner_cheapest_total didn't require a sort */
+		cheapest_startup_inner = inner_cheapest_total;
+		cheapest_total_inner = inner_cheapest_total;
+	}
+	else
+	{
+		/* it did require a sort, at least for the full set of keys */
+		cheapest_startup_inner = NULL;
+		cheapest_total_inner = NULL;
+	}
+	num_sortkeys = list_length(innersortkeys);
+	if (num_sortkeys > 1 && !useallclauses)
+		trialsortkeys = list_copy(innersortkeys);	/* need modifiable copy */
+	else
+		trialsortkeys = innersortkeys;		/* won't really truncate */
+
+	for (sortkeycnt = num_sortkeys; sortkeycnt > 0; sortkeycnt--)
+	{
+		Path	   *innerpath;
+		List	   *newclauses = NIL;
 
 		/*
-		 * Look for presorted inner paths that satisfy the innersortkey list
-		 * --- or any truncation thereof, if we are allowed to build a
-		 * mergejoin using a subset of the merge clauses.  Here, we consider
-		 * both cheap startup cost and cheap total cost.
-		 *
-		 * Currently we do not consider parameterized inner paths here. This
-		 * interacts with decisions elsewhere that also discriminate against
-		 * mergejoins with parameterized inputs; see comments in
-		 * src/backend/optimizer/README.
-		 *
-		 * As we shorten the sortkey list, we should consider only paths that
-		 * are strictly cheaper than (in particular, not the same as) any path
-		 * found in an earlier iteration.  Otherwise we'd be intentionally
-		 * using fewer merge keys than a given path allows (treating the rest
-		 * as plain joinquals), which is unlikely to be a good idea.  Also,
-		 * eliminating paths here on the basis of compare_path_costs is a lot
-		 * cheaper than building the mergejoin path only to throw it away.
-		 *
-		 * If inner_cheapest_total is well enough sorted to have not required
-		 * a sort in the path made above, we shouldn't make a duplicate path
-		 * with it, either.  We handle that case with the same logic that
-		 * handles the previous consideration, by initializing the variables
-		 * that track cheapest-so-far properly.  Note that we do NOT reject
-		 * inner_cheapest_total if we find it matches some shorter set of
-		 * pathkeys.  That case corresponds to using fewer mergekeys to avoid
-		 * sorting inner_cheapest_total, whereas we did sort it above, so the
-		 * plans being considered are different.
+		 * Look for an inner path ordered well enough for the first
+		 * 'sortkeycnt' innersortkeys.  NB: trialsortkeys list is modified
+		 * destructively, which is why we made a copy...
 		 */
-		if (pathkeys_contained_in(innersortkeys,
-								  inner_cheapest_total->pathkeys))
+		trialsortkeys = list_truncate(trialsortkeys, sortkeycnt);
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   TOTAL_COST);
+		if (innerpath != NULL &&
+			(cheapest_total_inner == NULL ||
+			 compare_path_costs(innerpath, cheapest_total_inner,
+								TOTAL_COST) < 0))
 		{
-			/* inner_cheapest_total didn't require a sort */
-			cheapest_startup_inner = inner_cheapest_total;
-			cheapest_total_inner = inner_cheapest_total;
-		}
-		else
-		{
-			/* it did require a sort, at least for the full set of keys */
-			cheapest_startup_inner = NULL;
-			cheapest_total_inner = NULL;
-		}
-		num_sortkeys = list_length(innersortkeys);
-		if (num_sortkeys > 1 && !useallclauses)
-			trialsortkeys = list_copy(innersortkeys);	/* need modifiable copy */
-		else
-			trialsortkeys = innersortkeys;		/* won't really truncate */
+			/* Found a cheap (or even-cheaper) sorted path */
+			/* Select the right mergeclauses, if we didn't already */
+			if (sortkeycnt < num_sortkeys)
+			{
+				newclauses =
+					find_mergeclauses_for_pathkeys(root,
+												   trialsortkeys,
+												   false,
+												   mergeclauses);
+				Assert(newclauses != NIL);
+			}
+			else
+				newclauses = mergeclauses;
 
-		for (sortkeycnt = num_sortkeys; sortkeycnt > 0; sortkeycnt--)
-		{
-			Path	   *innerpath;
-			List	   *newclauses = NIL;
+			if (!is_partial)
+			{
+				try_mergejoin_path(root,
+								joinrel,
+								outerpath,
+								innerpath,
+								merge_pathkeys,
+								newclauses,
+								NIL,
+								NIL,
+								jointype,
+								extra);
 
-			/*
-			 * Look for an inner path ordered well enough for the first
-			 * 'sortkeycnt' innersortkeys.  NB: trialsortkeys list is modified
-			 * destructively, which is why we made a copy...
-			 */
-			trialsortkeys = list_truncate(trialsortkeys, sortkeycnt);
-			innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
-													   trialsortkeys,
-													   NULL,
-													   TOTAL_COST);
-			if (innerpath != NULL &&
-				(cheapest_total_inner == NULL ||
-				 compare_path_costs(innerpath, cheapest_total_inner,
-									TOTAL_COST) < 0))
+				cheapest_total_inner = innerpath;
+			}
+			/* Generate partial path only if innerpath is parallel safe. */
+			else if (innerpath->parallel_safe)
+			{
+				try_partial_mergejoin_path(root,
+								joinrel,
+								outerpath,
+								innerpath,
+								merge_pathkeys,
+								newclauses,
+								NIL,
+								NIL,
+								jointype,
+								extra);
+				cheapest_total_inner = innerpath;
+			}
+		}
+		/* Same on the basis of cheapest startup cost ... */
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   STARTUP_COST);
+		if (innerpath != NULL &&
+			(cheapest_startup_inner == NULL ||
+			 compare_path_costs(innerpath, cheapest_startup_inner,
+								STARTUP_COST) < 0))
+		{
+			/* Found a cheap (or even-cheaper) sorted path */
+			if (innerpath != cheapest_total_inner)
 			{
-				/* Found a cheap (or even-cheaper) sorted path */
-				/* Select the right mergeclauses, if we didn't already */
-				if (sortkeycnt < num_sortkeys)
+				/*
+				 * Avoid rebuilding clause list if we already made one;
+				 * saves memory in big join trees...
+				 */
+				if (newclauses == NIL)
 				{
-					newclauses =
-						find_mergeclauses_for_pathkeys(root,
-													   trialsortkeys,
-													   false,
-													   mergeclauses);
-					Assert(newclauses != NIL);
+					if (sortkeycnt < num_sortkeys)
+					{
+						newclauses =
+							find_mergeclauses_for_pathkeys(root,
+														   trialsortkeys,
+														   false,
+														   mergeclauses);
+						Assert(newclauses != NIL);
+					}
+					else
+						newclauses = mergeclauses;
 				}
-				else
-					newclauses = mergeclauses;
-				try_mergejoin_path(root,
+
+				if (!is_partial)
+				{
+					try_mergejoin_path(root,
 								   joinrel,
 								   outerpath,
 								   innerpath,
@@ -1128,74 +1404,32 @@ match_unsorted_outer(PlannerInfo *root,
 								   NIL,
 								   jointype,
 								   extra);
-				cheapest_total_inner = innerpath;
-			}
-			/* Same on the basis of cheapest startup cost ... */
-			innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
-													   trialsortkeys,
-													   NULL,
-													   STARTUP_COST);
-			if (innerpath != NULL &&
-				(cheapest_startup_inner == NULL ||
-				 compare_path_costs(innerpath, cheapest_startup_inner,
-									STARTUP_COST) < 0))
-			{
-				/* Found a cheap (or even-cheaper) sorted path */
-				if (innerpath != cheapest_total_inner)
+					cheapest_startup_inner = innerpath;
+				}
+				/* Generate partial path only if innerpath is parallel safe. */
+				else if (innerpath->parallel_safe)
 				{
-					/*
-					 * Avoid rebuilding clause list if we already made one;
-					 * saves memory in big join trees...
-					 */
-					if (newclauses == NIL)
-					{
-						if (sortkeycnt < num_sortkeys)
-						{
-							newclauses =
-								find_mergeclauses_for_pathkeys(root,
-															   trialsortkeys,
-															   false,
-															   mergeclauses);
-							Assert(newclauses != NIL);
-						}
-						else
-							newclauses = mergeclauses;
-					}
-					try_mergejoin_path(root,
-									   joinrel,
-									   outerpath,
-									   innerpath,
-									   merge_pathkeys,
-									   newclauses,
-									   NIL,
-									   NIL,
-									   jointype,
-									   extra);
+					try_partial_mergejoin_path(root,
+								   joinrel,
+								   outerpath,
+								   innerpath,
+								   merge_pathkeys,
+								   newclauses,
+								   NIL,
+								   NIL,
+								   jointype,
+								   extra);
+					cheapest_startup_inner = innerpath;
 				}
-				cheapest_startup_inner = innerpath;
 			}
-
-			/*
-			 * Don't consider truncated sortkeys if we need all clauses.
-			 */
-			if (useallclauses)
-				break;
 		}
-	}
 
-	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
-	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
-		save_jointype != JOIN_UNIQUE_OUTER &&
-		bms_is_empty(joinrel->lateral_relids))
-		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
-								   save_jointype, extra);
+		/*
+		 * Don't consider truncated sortkeys if we need all clauses.
+		 */
+		if (useallclauses)
+			break;
+	}
 }
 
 /*
#2Peter Geoghegan
pg@heroku.com
In reply to: Dilip Kumar (#1)
Re: Proposal : Parallel Merge Join

On Sat, Dec 10, 2016 at 4:59 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

3. 20_patch.out : Explain analyze output with patch (median of 3 runs)

I noticed that the partially parallelized Merge Join EXPLAIN ANALYZE
that you attach has a big misestimation:

-> Merge Join (cost=3405052.45..3676948.66 rows=320 width=32)
(actual time=21165.849..37814.551 rows=1357812 loops=4)

Is this the best test case to show off the patch? This node is the
immediate outer child of a Nested Loop Semi Join, and so I'm concerned
that we measuring the wrong thing.

--
Peter Geoghegan

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3Dilip Kumar
dilipbalaut@gmail.com
In reply to: Peter Geoghegan (#2)
Re: Proposal : Parallel Merge Join

On Sun, Dec 11, 2016 at 8:14 AM, Peter Geoghegan <pg@heroku.com> wrote:

I noticed that the partially parallelized Merge Join EXPLAIN ANALYZE
that you attach has a big misestimation:

-> Merge Join (cost=3405052.45..3676948.66 rows=320 width=32)
(actual time=21165.849..37814.551 rows=1357812 loops=4)

Is this the best test case to show off the patch? This node is the
immediate outer child of a Nested Loop Semi Join, and so I'm concerned
that we measuring the wrong thing.

Actually main purpose of showing this example is that, by enabling
parallelism with merge join we can enable the parallelism in complete
tree, and we can get completely different kind of plan which is way
cheaper.

But I completely agree with your point that, in this particular
example estimation is wrong and may be with correct estimation plan
can be entirely different. I am planning to test this with more self
generated test case where first we can guarantee that our estimation
is almost correct and see the impact of the patch.

Here is one such example, this time I tested my patch with Amit's
parallel index scan patch [1]/messages/by-id/CAA4eK1KA4LwTYXZG=k3J2bA+ZOEYnz2gkyWBKgY=_q0xJRBMDw@mail.gmail.com, just to enable more scope for parallel
merge join.

All configurations and machines are same what I mentioned up thread.

create table test1(a, b) as select generate_series(1,
10000000),generate_series(1, 10000000);
create table test2(a, b) as select generate_series(1,
10000000),generate_series(1, 10000000);
create index idx1 on test1(a);
create index idx2 on test2(a);

Query:
explain analyze select * from test1, test2 where test1.a=test2.a and
test1.b = test2.b and test1.a+test2.b < 3 and test1.a < 3000000 and
test2.a < 3000000;

On Head:

Merge Join (cost=6.92..228073.90 rows=1 width=16) (actual
time=0.033..3123.400 rows=1 loops=1)
Merge Cond: (test1.a = test2.a)
Join Filter: ((test1.b = test2.b) AND ((test1.a + test2.b) < 3))
Rows Removed by Join Filter: 2999998
-> Index Scan using idx1 on test1 (cost=0.43..98626.90
rows=2998198 width=8) (actual time=0.013..746.382 rows=2999999
loops=1)
Index Cond: (a < 3000000)
-> Index Scan using idx2 on test2 (cost=0.43..98708.62
rows=3000639 width=8) (actual time=0.012..751.558 rows=2999999
loops=1)
Index Cond: (a < 3000000)
Planning time: 0.604 ms
Execution time: 3123.442 ms

With Patch:

Gather (cost=1005.46..193001.64 rows=1 width=16) (actual
time=0.244..1883.087 rows=1 loops=1)
Workers Planned: 3
Workers Launched: 3
-> Merge Join (cost=5.46..192000.64 rows=1 width=16) (actual
time=1409.686..1880.394 rows=0 loops=4)
Merge Cond: (test2.a = test1.a)
Join Filter: ((test1.b = test2.b) AND ((test1.a + test2.b) < 3))
Rows Removed by Join Filter: 750000
-> Parallel Index Scan using idx2 on test2
(cost=0.43..78381.71 rows=967948 width=8) (actual time=0.023..221.172
rows=750000 loops=4)
Index Cond: (a < 3000000)
-> Index Scan using idx1 on test1 (cost=0.43..98626.90
rows=2998198 width=8) (actual time=0.024..893.081 rows=2999254
loops=4)
Index Cond: (a < 3000000)
Planning time: 0.281 ms
Execution time: 1888.750 ms

This example shows direct improvement with merge join, but IMHO the
main value of this patch is that we can parallelize multi level join
query, where parallelism is not used becuase of merge join at some
level.

[1]: /messages/by-id/CAA4eK1KA4LwTYXZG=k3J2bA+ZOEYnz2gkyWBKgY=_q0xJRBMDw@mail.gmail.com

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#4Robert Haas
robertmhaas@gmail.com
In reply to: Dilip Kumar (#1)
Re: Proposal : Parallel Merge Join

On Sat, Dec 10, 2016 at 7:59 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

I would like to propose a patch for parallelizing merge join path.
This idea is derived by analyzing TPCH results.

I have done this analysis along with my colleagues Rafia sabih and Amit kaplia.

Currently we already have infrastructure for executing parallel join,
so we don't need any change at executor level. Only in path
generation, we need to add partial paths for merge join, like we do
for nest loop and hash join.

Hmm, so it seems my initial guess that we didn't need to bother
generating such paths was wrong. Oops.

This patch is hard to read because it is reorganizing a bunch of code
as well as adding new functionality. Perhaps you could separate it
into two patches, one with the refactoring and then the other with the
new functionality. Actually, though, I don't understand why you need
so much rearrangement....

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#5Dilip Kumar
dilipbalaut@gmail.com
In reply to: Robert Haas (#4)
Re: Proposal : Parallel Merge Join

On Tue, Dec 13, 2016 at 12:11 AM, Robert Haas <robertmhaas@gmail.com> wrote:

Hmm, so it seems my initial guess that we didn't need to bother
generating such paths was wrong. Oops.

This patch is hard to read because it is reorganizing a bunch of code
as well as adding new functionality. Perhaps you could separate it
into two patches, one with the refactoring and then the other with the
new functionality.

Okay, I can do that.

Actually, though, I don't understand why you need
so much rearrangement....

Actually match_unsorted_outer is quite complex function and
considering merge join path for many cases, And In my opinion those
all paths should be considered for partial outer as well.

So one option was to duplicate that part of code. But I chose to move
all that code from match_unsorted_outer (which is related to
generating various merge join path) to new function called
generate_mergejoin_path. And, use it for normal outer path as well as
for partial outer path.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#6Dilip Kumar
dilipbalaut@gmail.com
In reply to: Dilip Kumar (#5)
2 attachment(s)
Re: Proposal : Parallel Merge Join

On Tue, Dec 13, 2016 at 6:42 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

This patch is hard to read because it is reorganizing a bunch of code
as well as adding new functionality. Perhaps you could separate it
into two patches, one with the refactoring and then the other with the
new functionality.

Okay, I can do that.

I have created two patches as per the suggestion.

1. mergejoin_refactoring_v2.patch --> Move functionality of
considering various merge join path into new function.
2. parallel_mergejoin_v2.patch -> This adds the functionality of
supporting partial mergejoin paths. This will apply on top of
mergejoin_refactoring_v2.patch.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

mergejoin_refactoring_v2.patchapplication/octet-stream; name=mergejoin_refactoring_v2.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 96f00fc..95538c5 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -50,6 +50,15 @@ static List *select_mergejoin_clauses(PlannerInfo *root,
 						 List *restrictlist,
 						 JoinType jointype,
 						 bool *mergejoin_allowed);
+static void generate_mergejoin_paths(PlannerInfo *root,
+						 RelOptInfo *joinrel,
+						 RelOptInfo *innerrel,
+						 Path *outerpath,
+						 JoinType jointype,
+						 JoinPathExtraData *extra,
+						 bool useallclauses,
+						 Path *inner_cheapest_total,
+						 List *merge_pathkeys);
 
 
 /*
@@ -777,6 +786,241 @@ sort_inner_and_outer(PlannerInfo *root,
 }
 
 /*
+ * generate_mergejoin_paths
+ *	Creates possible mergejoin paths for input outerpath.
+ *
+ * We generate mergejoins if mergejoin clauses are available.  We have
+ * two ways to generate the inner path for a mergejoin: sort the cheapest
+ * inner path, or use an inner path that is already suitably ordered for the
+ * merge.  If we have several mergeclauses, it could be that there is no inner
+ * path (or only a very expensive one) for the full list of mergeclauses, but
+ * better paths exist if we truncate the mergeclause list (thereby discarding
+ * some sort key requirements).  So, we consider truncations of the
+ * mergeclause list as well as the full list.  (Ideally we'd consider all
+ * subsets of the mergeclause list, but that seems way too expensive.)
+ */
+static void
+generate_mergejoin_paths(PlannerInfo *root,
+						 RelOptInfo *joinrel,
+						 RelOptInfo *innerrel,
+						 Path *outerpath,
+						 JoinType jointype,
+						 JoinPathExtraData *extra,
+						 bool useallclauses,
+						 Path *inner_cheapest_total,
+						 List *merge_pathkeys)
+{
+	List	   *mergeclauses;
+	List	   *innersortkeys;
+	List	   *trialsortkeys;
+	Path	   *cheapest_startup_inner;
+	Path	   *cheapest_total_inner;
+	JoinType	save_jointype = jointype;
+	int			num_sortkeys;
+	int			sortkeycnt;
+
+	if (jointype == JOIN_UNIQUE_OUTER || jointype == JOIN_UNIQUE_INNER)
+		jointype = JOIN_INNER;
+
+	/* Look for useful mergeclauses (if any) */
+	mergeclauses = find_mergeclauses_for_pathkeys(root,
+												  outerpath->pathkeys,
+												  true,
+												  extra->mergeclause_list);
+
+	/*
+	 * Done with this outer path if no chance for a mergejoin.
+	 *
+	 * Special corner case: for "x FULL JOIN y ON true", there will be no
+	 * join clauses at all.  Ordinarily we'd generate a clauseless
+	 * nestloop path, but since mergejoin is our only join type that
+	 * supports FULL JOIN without any join clauses, it's necessary to
+	 * generate a clauseless mergejoin path instead.
+	 */
+	if (mergeclauses == NIL)
+	{
+		if (jointype == JOIN_FULL)
+			 /* okay to try for mergejoin */ ;
+		else
+			return;
+	}
+	if (useallclauses &&
+		list_length(mergeclauses) != list_length(extra->mergeclause_list))
+		return;
+
+	/* Compute the required ordering of the inner path */
+	innersortkeys = make_inner_pathkeys_for_merge(root,
+												  mergeclauses,
+												  outerpath->pathkeys);
+
+	/*
+	 * Generate a mergejoin on the basis of sorting the cheapest inner.
+	 * Since a sort will be needed, only cheapest total cost matters. (But
+	 * try_mergejoin_path will do the right thing if inner_cheapest_total
+	 * is already correctly sorted.)
+	 */
+	try_mergejoin_path(root,
+					   joinrel,
+					   outerpath,
+					   inner_cheapest_total,
+					   merge_pathkeys,
+					   mergeclauses,
+					   NIL,
+					   innersortkeys,
+					   jointype,
+					   extra);
+
+	/* Can't do anything else if inner path needs to be unique'd */
+	if (save_jointype == JOIN_UNIQUE_INNER)
+		return;
+
+	/*
+	 * Look for presorted inner paths that satisfy the innersortkey list
+	 * --- or any truncation thereof, if we are allowed to build a
+	 * mergejoin using a subset of the merge clauses.  Here, we consider
+	 * both cheap startup cost and cheap total cost.
+	 *
+	 * Currently we do not consider parameterized inner paths here. This
+	 * interacts with decisions elsewhere that also discriminate against
+	 * mergejoins with parameterized inputs; see comments in
+	 * src/backend/optimizer/README.
+	 *
+	 * As we shorten the sortkey list, we should consider only paths that
+	 * are strictly cheaper than (in particular, not the same as) any path
+	 * found in an earlier iteration.  Otherwise we'd be intentionally
+	 * using fewer merge keys than a given path allows (treating the rest
+	 * as plain joinquals), which is unlikely to be a good idea.  Also,
+	 * eliminating paths here on the basis of compare_path_costs is a lot
+	 * cheaper than building the mergejoin path only to throw it away.
+	 *
+	 * If inner_cheapest_total is well enough sorted to have not required
+	 * a sort in the path made above, we shouldn't make a duplicate path
+	 * with it, either.  We handle that case with the same logic that
+	 * handles the previous consideration, by initializing the variables
+	 * that track cheapest-so-far properly.  Note that we do NOT reject
+	 * inner_cheapest_total if we find it matches some shorter set of
+	 * pathkeys.  That case corresponds to using fewer mergekeys to avoid
+	 * sorting inner_cheapest_total, whereas we did sort it above, so the
+	 * plans being considered are different.
+	 */
+	if (pathkeys_contained_in(innersortkeys,
+							  inner_cheapest_total->pathkeys))
+	{
+		/* inner_cheapest_total didn't require a sort */
+		cheapest_startup_inner = inner_cheapest_total;
+		cheapest_total_inner = inner_cheapest_total;
+	}
+	else
+	{
+		/* it did require a sort, at least for the full set of keys */
+		cheapest_startup_inner = NULL;
+		cheapest_total_inner = NULL;
+	}
+	num_sortkeys = list_length(innersortkeys);
+	if (num_sortkeys > 1 && !useallclauses)
+		trialsortkeys = list_copy(innersortkeys);	/* need modifiable copy */
+	else
+		trialsortkeys = innersortkeys;		/* won't really truncate */
+
+	for (sortkeycnt = num_sortkeys; sortkeycnt > 0; sortkeycnt--)
+	{
+		Path	   *innerpath;
+		List	   *newclauses = NIL;
+
+		/*
+		 * Look for an inner path ordered well enough for the first
+		 * 'sortkeycnt' innersortkeys.  NB: trialsortkeys list is modified
+		 * destructively, which is why we made a copy...
+		 */
+		trialsortkeys = list_truncate(trialsortkeys, sortkeycnt);
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   TOTAL_COST);
+		if (innerpath != NULL &&
+			(cheapest_total_inner == NULL ||
+			 compare_path_costs(innerpath, cheapest_total_inner,
+								TOTAL_COST) < 0))
+		{
+			/* Found a cheap (or even-cheaper) sorted path */
+			/* Select the right mergeclauses, if we didn't already */
+			if (sortkeycnt < num_sortkeys)
+			{
+				newclauses =
+					find_mergeclauses_for_pathkeys(root,
+												   trialsortkeys,
+												   false,
+												   mergeclauses);
+				Assert(newclauses != NIL);
+			}
+			else
+				newclauses = mergeclauses;
+			try_mergejoin_path(root,
+							   joinrel,
+							   outerpath,
+							   innerpath,
+							   merge_pathkeys,
+							   newclauses,
+							   NIL,
+							   NIL,
+							   jointype,
+							   extra);
+			cheapest_total_inner = innerpath;
+		}
+		/* Same on the basis of cheapest startup cost ... */
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   STARTUP_COST);
+		if (innerpath != NULL &&
+			(cheapest_startup_inner == NULL ||
+			 compare_path_costs(innerpath, cheapest_startup_inner,
+								STARTUP_COST) < 0))
+		{
+			/* Found a cheap (or even-cheaper) sorted path */
+			if (innerpath != cheapest_total_inner)
+			{
+				/*
+				 * Avoid rebuilding clause list if we already made one;
+				 * saves memory in big join trees...
+				 */
+				if (newclauses == NIL)
+				{
+					if (sortkeycnt < num_sortkeys)
+					{
+						newclauses =
+							find_mergeclauses_for_pathkeys(root,
+														   trialsortkeys,
+														   false,
+														   mergeclauses);
+						Assert(newclauses != NIL);
+					}
+					else
+						newclauses = mergeclauses;
+				}
+				try_mergejoin_path(root,
+								   joinrel,
+								   outerpath,
+								   innerpath,
+								   merge_pathkeys,
+								   newclauses,
+								   NIL,
+								   NIL,
+								   jointype,
+								   extra);
+			}
+			cheapest_startup_inner = innerpath;
+		}
+
+		/*
+		 * Don't consider truncated sortkeys if we need all clauses.
+		 */
+		if (useallclauses)
+			break;
+	}
+}
+
+/*
  * match_unsorted_outer
  *	  Creates possible join paths for processing a single join relation
  *	  'joinrel' by employing either iterative substitution or
@@ -790,16 +1034,8 @@ sort_inner_and_outer(PlannerInfo *root,
  * cheapest-total inner-indexscan path (if any), and one on the
  * cheapest-startup inner-indexscan path (if different).
  *
- * We also consider mergejoins if mergejoin clauses are available.  We have
- * two ways to generate the inner path for a mergejoin: sort the cheapest
- * inner path, or use an inner path that is already suitably ordered for the
- * merge.  If we have several mergeclauses, it could be that there is no inner
- * path (or only a very expensive one) for the full list of mergeclauses, but
- * better paths exist if we truncate the mergeclause list (thereby discarding
- * some sort key requirements).  So, we consider truncations of the
- * mergeclause list as well as the full list.  (Ideally we'd consider all
- * subsets of the mergeclause list, but that seems way too expensive.)
- *
+ * We also consider mergejoins if mergejoin clauses are available. refer
+ * detailed comments in generate_mergejoin_paths.
  * 'joinrel' is the join relation
  * 'outerrel' is the outer join relation
  * 'innerrel' is the inner join relation
@@ -894,13 +1130,6 @@ match_unsorted_outer(PlannerInfo *root,
 	{
 		Path	   *outerpath = (Path *) lfirst(lc1);
 		List	   *merge_pathkeys;
-		List	   *mergeclauses;
-		List	   *innersortkeys;
-		List	   *trialsortkeys;
-		Path	   *cheapest_startup_inner;
-		Path	   *cheapest_total_inner;
-		int			num_sortkeys;
-		int			sortkeycnt;
 
 		/*
 		 * We cannot use an outer path that is parameterized by the inner rel.
@@ -986,201 +1215,10 @@ match_unsorted_outer(PlannerInfo *root,
 		if (inner_cheapest_total == NULL)
 			continue;
 
-		/* Look for useful mergeclauses (if any) */
-		mergeclauses = find_mergeclauses_for_pathkeys(root,
-													  outerpath->pathkeys,
-													  true,
-													extra->mergeclause_list);
-
-		/*
-		 * Done with this outer path if no chance for a mergejoin.
-		 *
-		 * Special corner case: for "x FULL JOIN y ON true", there will be no
-		 * join clauses at all.  Ordinarily we'd generate a clauseless
-		 * nestloop path, but since mergejoin is our only join type that
-		 * supports FULL JOIN without any join clauses, it's necessary to
-		 * generate a clauseless mergejoin path instead.
-		 */
-		if (mergeclauses == NIL)
-		{
-			if (jointype == JOIN_FULL)
-				 /* okay to try for mergejoin */ ;
-			else
-				continue;
-		}
-		if (useallclauses && list_length(mergeclauses) != list_length(extra->mergeclause_list))
-			continue;
-
-		/* Compute the required ordering of the inner path */
-		innersortkeys = make_inner_pathkeys_for_merge(root,
-													  mergeclauses,
-													  outerpath->pathkeys);
-
-		/*
-		 * Generate a mergejoin on the basis of sorting the cheapest inner.
-		 * Since a sort will be needed, only cheapest total cost matters. (But
-		 * try_mergejoin_path will do the right thing if inner_cheapest_total
-		 * is already correctly sorted.)
-		 */
-		try_mergejoin_path(root,
-						   joinrel,
-						   outerpath,
-						   inner_cheapest_total,
-						   merge_pathkeys,
-						   mergeclauses,
-						   NIL,
-						   innersortkeys,
-						   jointype,
-						   extra);
-
-		/* Can't do anything else if inner path needs to be unique'd */
-		if (save_jointype == JOIN_UNIQUE_INNER)
-			continue;
-
-		/*
-		 * Look for presorted inner paths that satisfy the innersortkey list
-		 * --- or any truncation thereof, if we are allowed to build a
-		 * mergejoin using a subset of the merge clauses.  Here, we consider
-		 * both cheap startup cost and cheap total cost.
-		 *
-		 * Currently we do not consider parameterized inner paths here. This
-		 * interacts with decisions elsewhere that also discriminate against
-		 * mergejoins with parameterized inputs; see comments in
-		 * src/backend/optimizer/README.
-		 *
-		 * As we shorten the sortkey list, we should consider only paths that
-		 * are strictly cheaper than (in particular, not the same as) any path
-		 * found in an earlier iteration.  Otherwise we'd be intentionally
-		 * using fewer merge keys than a given path allows (treating the rest
-		 * as plain joinquals), which is unlikely to be a good idea.  Also,
-		 * eliminating paths here on the basis of compare_path_costs is a lot
-		 * cheaper than building the mergejoin path only to throw it away.
-		 *
-		 * If inner_cheapest_total is well enough sorted to have not required
-		 * a sort in the path made above, we shouldn't make a duplicate path
-		 * with it, either.  We handle that case with the same logic that
-		 * handles the previous consideration, by initializing the variables
-		 * that track cheapest-so-far properly.  Note that we do NOT reject
-		 * inner_cheapest_total if we find it matches some shorter set of
-		 * pathkeys.  That case corresponds to using fewer mergekeys to avoid
-		 * sorting inner_cheapest_total, whereas we did sort it above, so the
-		 * plans being considered are different.
-		 */
-		if (pathkeys_contained_in(innersortkeys,
-								  inner_cheapest_total->pathkeys))
-		{
-			/* inner_cheapest_total didn't require a sort */
-			cheapest_startup_inner = inner_cheapest_total;
-			cheapest_total_inner = inner_cheapest_total;
-		}
-		else
-		{
-			/* it did require a sort, at least for the full set of keys */
-			cheapest_startup_inner = NULL;
-			cheapest_total_inner = NULL;
-		}
-		num_sortkeys = list_length(innersortkeys);
-		if (num_sortkeys > 1 && !useallclauses)
-			trialsortkeys = list_copy(innersortkeys);	/* need modifiable copy */
-		else
-			trialsortkeys = innersortkeys;		/* won't really truncate */
-
-		for (sortkeycnt = num_sortkeys; sortkeycnt > 0; sortkeycnt--)
-		{
-			Path	   *innerpath;
-			List	   *newclauses = NIL;
-
-			/*
-			 * Look for an inner path ordered well enough for the first
-			 * 'sortkeycnt' innersortkeys.  NB: trialsortkeys list is modified
-			 * destructively, which is why we made a copy...
-			 */
-			trialsortkeys = list_truncate(trialsortkeys, sortkeycnt);
-			innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
-													   trialsortkeys,
-													   NULL,
-													   TOTAL_COST);
-			if (innerpath != NULL &&
-				(cheapest_total_inner == NULL ||
-				 compare_path_costs(innerpath, cheapest_total_inner,
-									TOTAL_COST) < 0))
-			{
-				/* Found a cheap (or even-cheaper) sorted path */
-				/* Select the right mergeclauses, if we didn't already */
-				if (sortkeycnt < num_sortkeys)
-				{
-					newclauses =
-						find_mergeclauses_for_pathkeys(root,
-													   trialsortkeys,
-													   false,
-													   mergeclauses);
-					Assert(newclauses != NIL);
-				}
-				else
-					newclauses = mergeclauses;
-				try_mergejoin_path(root,
-								   joinrel,
-								   outerpath,
-								   innerpath,
-								   merge_pathkeys,
-								   newclauses,
-								   NIL,
-								   NIL,
-								   jointype,
-								   extra);
-				cheapest_total_inner = innerpath;
-			}
-			/* Same on the basis of cheapest startup cost ... */
-			innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
-													   trialsortkeys,
-													   NULL,
-													   STARTUP_COST);
-			if (innerpath != NULL &&
-				(cheapest_startup_inner == NULL ||
-				 compare_path_costs(innerpath, cheapest_startup_inner,
-									STARTUP_COST) < 0))
-			{
-				/* Found a cheap (or even-cheaper) sorted path */
-				if (innerpath != cheapest_total_inner)
-				{
-					/*
-					 * Avoid rebuilding clause list if we already made one;
-					 * saves memory in big join trees...
-					 */
-					if (newclauses == NIL)
-					{
-						if (sortkeycnt < num_sortkeys)
-						{
-							newclauses =
-								find_mergeclauses_for_pathkeys(root,
-															   trialsortkeys,
-															   false,
-															   mergeclauses);
-							Assert(newclauses != NIL);
-						}
-						else
-							newclauses = mergeclauses;
-					}
-					try_mergejoin_path(root,
-									   joinrel,
-									   outerpath,
-									   innerpath,
-									   merge_pathkeys,
-									   newclauses,
-									   NIL,
-									   NIL,
-									   jointype,
-									   extra);
-				}
-				cheapest_startup_inner = innerpath;
-			}
-
-			/*
-			 * Don't consider truncated sortkeys if we need all clauses.
-			 */
-			if (useallclauses)
-				break;
-		}
+		/* Generate merge join paths for the outer path */
+		generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
+								 save_jointype, extra, useallclauses,
+								 inner_cheapest_total, merge_pathkeys);
 	}
 
 	/*
parallel_mergejoin_v2.patchapplication/octet-stream; name=parallel_mergejoin_v2.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 95538c5..8105ffe 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -58,7 +58,9 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 JoinPathExtraData *extra,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
-						 List *merge_pathkeys);
+						 List *merge_pathkeys,
+						 bool is_partial);
+
 
 
 /*
@@ -481,6 +483,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+				   RelOptInfo *joinrel,
+				   Path *outer_path,
+				   Path *inner_path,
+				   List *pathkeys,
+				   List *mergeclauses,
+				   List *outersortkeys,
+				   List *innersortkeys,
+				   JoinType jointype,
+				   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_subset(inner_paramrels, outer_path->parent->relids))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+			 create_mergejoin_path(root,
+								   joinrel,
+								   jointype,
+								   &workspace,
+								   extra->sjinfo,
+								   outer_path,
+								   inner_path,
+								   extra->restrictlist,
+								   pathkeys,
+								   NULL,
+								   mergeclauses,
+								   outersortkeys,
+								   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -649,6 +721,7 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
 	List	   *all_pathkeys;
@@ -782,6 +855,37 @@ sort_inner_and_outer(PlannerInfo *root,
 						   innerkeys,
 						   jointype,
 						   extra);
+
+		/*
+		 * If the joinrel is parallel-safe, we may be able to consider a
+		 * partial merge join.  However, we can't handle JOIN_UNIQUE_OUTER,
+		 * because the outer path will be partial, and therefore we won't be
+		 * able to properly guarantee uniqueness.  Similarly, we can't handle
+		 * JOIN_FULL and JOIN_RIGHT, because they can produce false null
+		 * extended rows.  Also, the resulting path must not be parameterized.
+		 */
+		if (joinrel->consider_parallel &&
+			save_jointype != JOIN_UNIQUE_OUTER &&
+			jointype != JOIN_FULL &&
+			jointype != JOIN_RIGHT &&
+			outerrel->partial_pathlist != NIL &&
+			bms_is_empty(joinrel->lateral_relids) &&
+			inner_path->parallel_safe)
+		{
+			Path *cheapest_partial_outer =
+							(Path *) linitial(outerrel->partial_pathlist);
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   cheapest_partial_outer,
+									   inner_path,
+									   merge_pathkeys,
+									   cur_mergeclauses,
+									   outerkeys,
+									   innerkeys,
+									   jointype,
+									   extra);
+		}
 	}
 }
 
@@ -798,6 +902,8 @@ sort_inner_and_outer(PlannerInfo *root,
  * some sort key requirements).  So, we consider truncations of the
  * mergeclause list as well as the full list.  (Ideally we'd consider all
  * subsets of the mergeclause list, but that seems way too expensive.)
+ *
+ * is_partial passed true for generating partial merge join paths.
  */
 static void
 generate_mergejoin_paths(PlannerInfo *root,
@@ -808,7 +914,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 						 JoinPathExtraData *extra,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
-						 List *merge_pathkeys)
+						 List *merge_pathkeys,
+						 bool is_partial)
 {
 	List	   *mergeclauses;
 	List	   *innersortkeys;
@@ -859,16 +966,30 @@ generate_mergejoin_paths(PlannerInfo *root,
 	 * try_mergejoin_path will do the right thing if inner_cheapest_total
 	 * is already correctly sorted.)
 	 */
-	try_mergejoin_path(root,
-					   joinrel,
-					   outerpath,
-					   inner_cheapest_total,
-					   merge_pathkeys,
-					   mergeclauses,
-					   NIL,
-					   innersortkeys,
-					   jointype,
-					   extra);
+	if (!is_partial)
+		try_mergejoin_path(root,
+						joinrel,
+						outerpath,
+						inner_cheapest_total,
+						merge_pathkeys,
+						mergeclauses,
+						NIL,
+						innersortkeys,
+						jointype,
+						extra);
+
+	/* Generate partial path if inner is parallel safe. */
+	else if (inner_cheapest_total->parallel_safe)
+		try_partial_mergejoin_path(root,
+						joinrel,
+						outerpath,
+						inner_cheapest_total,
+						merge_pathkeys,
+						mergeclauses,
+						NIL,
+						innersortkeys,
+						jointype,
+						extra);
 
 	/* Can't do anything else if inner path needs to be unique'd */
 	if (save_jointype == JOIN_UNIQUE_INNER)
@@ -955,16 +1076,30 @@ generate_mergejoin_paths(PlannerInfo *root,
 			}
 			else
 				newclauses = mergeclauses;
-			try_mergejoin_path(root,
-							   joinrel,
-							   outerpath,
-							   innerpath,
-							   merge_pathkeys,
-							   newclauses,
-							   NIL,
-							   NIL,
-							   jointype,
-							   extra);
+			if (!is_partial)
+				try_mergejoin_path(root,
+								joinrel,
+								outerpath,
+								innerpath,
+								merge_pathkeys,
+								newclauses,
+								NIL,
+								NIL,
+								jointype,
+								extra);
+			/* Generate partial path only if innerpath is parallel safe. */
+			else if (innerpath->parallel_safe)
+				try_partial_mergejoin_path(root,
+										   joinrel,
+										   outerpath,
+										   innerpath,
+										   merge_pathkeys,
+										   newclauses,
+										   NIL,
+										   NIL,
+										   jointype,
+										   extra);
+
 			cheapest_total_inner = innerpath;
 		}
 		/* Same on the basis of cheapest startup cost ... */
@@ -998,16 +1133,29 @@ generate_mergejoin_paths(PlannerInfo *root,
 					else
 						newclauses = mergeclauses;
 				}
-				try_mergejoin_path(root,
-								   joinrel,
-								   outerpath,
-								   innerpath,
-								   merge_pathkeys,
-								   newclauses,
-								   NIL,
-								   NIL,
-								   jointype,
-								   extra);
+				if (!is_partial)
+					try_mergejoin_path(root,
+									   joinrel,
+									   outerpath,
+									   innerpath,
+									   merge_pathkeys,
+									   newclauses,
+									   NIL,
+									   NIL,
+									   jointype,
+									   extra);
+				/* Generate partial path only if innerpath is parallel safe. */
+				else if (innerpath->parallel_safe)
+					try_partial_mergejoin_path(root,
+											   joinrel,
+											   outerpath,
+											   innerpath,
+											   merge_pathkeys,
+											   newclauses,
+											   NIL,
+											   NIL,
+											   jointype,
+											   extra);
 			}
 			cheapest_startup_inner = innerpath;
 		}
@@ -1218,22 +1366,59 @@ match_unsorted_outer(PlannerInfo *root,
 		/* Generate merge join paths for the outer path */
 		generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
 								 save_jointype, extra, useallclauses,
-								 inner_cheapest_total, merge_pathkeys);
+								 inner_cheapest_total, merge_pathkeys,
+								 false);
 	}
 
 	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
+	 * Consider partial nestloop and mergejoin plan if the joinrel is
+	 * parallel-safe. However, we can't handle JOIN_UNIQUE_OUTER, because
+	 * the outer path will be partial, and therefore we won't be able to
+	 * properly guarantee uniqueness.  Nor can we handle extra_lateral_rels,
+	 * since partial paths must not be parameterized.
+	 * Similarly, we can't handle JOIN_FULL and JOIN_RIGHT, because they
+	 * can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
-		save_jointype != JOIN_UNIQUE_OUTER &&
-		bms_is_empty(joinrel->lateral_relids))
+	if (!joinrel->consider_parallel ||
+		save_jointype == JOIN_UNIQUE_OUTER ||
+		!bms_is_empty(joinrel->lateral_relids) ||
+		jointype == JOIN_FULL ||
+		jointype == JOIN_RIGHT)
+		return;
+
+	if (nestjoinOK)
 		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
 								   save_jointype, extra);
+
+	/* Can't generate mergejoin path if inner rel is parameterized by outer */
+	if (inner_cheapest_total != NULL)
+	{
+		ListCell   *lc1;
+		JoinType	save_jointype = jointype;
+
+		if (jointype == JOIN_UNIQUE_INNER)
+			jointype = JOIN_INNER;
+
+		/* generate merge join path for each partial outer path */
+		foreach(lc1, outerrel->partial_pathlist)
+		{
+			Path	   *outerpath = (Path *) lfirst(lc1);
+			List	   *merge_pathkeys;
+
+			/*
+			 * Figure out what useful ordering any paths we create
+			 * will have.
+			 */
+			merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											   outerpath->pathkeys);
+
+			generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
+									 save_jointype, extra, false,
+									 inner_cheapest_total, merge_pathkeys,
+									 true);
+		}
+
+	}
 }
 
 /*
#7Dilip Kumar
dilipbalaut@gmail.com
In reply to: Dilip Kumar (#6)
2 attachment(s)
Re: Proposal : Parallel Merge Join

On Tue, Dec 13, 2016 at 8:34 PM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

I have created two patches as per the suggestion.

1. mergejoin_refactoring_v2.patch --> Move functionality of
considering various merge join path into new function.
2. parallel_mergejoin_v2.patch -> This adds the functionality of
supporting partial mergejoin paths. This will apply on top of
mergejoin_refactoring_v2.patch.

We have done further analysis of the performance with TPCH benchmark
at higher scale factor. I have tested parallel merge join patch along
with parallel index scan[1]/messages/by-id/CAA4eK1KA4LwTYXZG=k3J2bA+ZOEYnz2gkyWBKgY=_q0xJRBMDw@mail.gmail.com

I have observed that with query3, we are getting linear scalability
('explain analyze' results are attached).

Test Setup:
---------------
TPCH 300 scale factor
work_mem = 1GB
shared_buffer = 1GB
random_page_cost=seq_page_cost=0.1
max_parallel_workers_per_gather=4 (warm cache ensured)
The median of 3 runs (reading are quite stable).

On Head: 2702568.099 ms
With Patch: 547363.164 ms

Other Experiments:

* I have also verified reading on the head, without modifying
random_page_cost=seq_page_cost, but there is no change in plan or
execution time.

* I have tried to increase the max_parallel_workers_per_gather to 8
but I did not observe further scaling.

[1]: /messages/by-id/CAA4eK1KA4LwTYXZG=k3J2bA+ZOEYnz2gkyWBKgY=_q0xJRBMDw@mail.gmail.com

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

3_head.outapplication/octet-stream; name=3_head.outDownload
3_patch.outapplication/octet-stream; name=3_patch.outDownload
#8Robert Haas
robertmhaas@gmail.com
In reply to: Dilip Kumar (#6)
Re: Proposal : Parallel Merge Join

On Tue, Dec 13, 2016 at 10:04 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Tue, Dec 13, 2016 at 6:42 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

This patch is hard to read because it is reorganizing a bunch of code
as well as adding new functionality. Perhaps you could separate it
into two patches, one with the refactoring and then the other with the
new functionality.

Okay, I can do that.

I have created two patches as per the suggestion.

1. mergejoin_refactoring_v2.patch --> Move functionality of
considering various merge join path into new function.
2. parallel_mergejoin_v2.patch -> This adds the functionality of
supporting partial mergejoin paths. This will apply on top of
mergejoin_refactoring_v2.patch.

Committed the refactoring patch with some mild cosmetic adjustments.

As to the second patch:

+        if (jointype == JOIN_UNIQUE_INNER)
+            jointype = JOIN_INNER;

Isn't this dead code? save_jointype might that value, but jointype won't.

Apart from that and some cosmetic issues it looks basically OK to me
on a first read-through.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#9Dilip Kumar
dilipbalaut@gmail.com
In reply to: Robert Haas (#8)
1 attachment(s)
Re: Proposal : Parallel Merge Join

On Wed, Dec 21, 2016 at 8:39 PM, Robert Haas <robertmhaas@gmail.com> wrote:

Committed the refactoring patch with some mild cosmetic adjustments.

Thanks..

As to the second patch:

+        if (jointype == JOIN_UNIQUE_INNER)
+            jointype = JOIN_INNER;

Isn't this dead code? save_jointype might that value, but jointype won't.

Yes, it is.

I have fixed this, and also observed that comment for
try_partial_mergejoin_path header was having some problem, fixed that
too.

Apart from that and some cosmetic issues it looks basically OK to me
on a first read-through.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

parallel_mergejoin_v3.patchapplication/octet-stream; name=parallel_mergejoin_v3.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index b5cbcf4..a29fb0e 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -58,7 +58,9 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 JoinPathExtraData *extra,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
-						 List *merge_pathkeys);
+						 List *merge_pathkeys,
+						 bool is_partial);
+
 
 
 /*
@@ -481,6 +483,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a partial merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+				   RelOptInfo *joinrel,
+				   Path *outer_path,
+				   Path *inner_path,
+				   List *pathkeys,
+				   List *mergeclauses,
+				   List *outersortkeys,
+				   List *innersortkeys,
+				   JoinType jointype,
+				   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_subset(inner_paramrels, outer_path->parent->relids))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+			 create_mergejoin_path(root,
+								   joinrel,
+								   jointype,
+								   &workspace,
+								   extra->sjinfo,
+								   outer_path,
+								   inner_path,
+								   extra->restrictlist,
+								   pathkeys,
+								   NULL,
+								   mergeclauses,
+								   outersortkeys,
+								   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -649,6 +721,7 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
 	List	   *all_pathkeys;
@@ -782,6 +855,37 @@ sort_inner_and_outer(PlannerInfo *root,
 						   innerkeys,
 						   jointype,
 						   extra);
+
+		/*
+		 * If the joinrel is parallel-safe, we may be able to consider a
+		 * partial merge join.  However, we can't handle JOIN_UNIQUE_OUTER,
+		 * because the outer path will be partial, and therefore we won't be
+		 * able to properly guarantee uniqueness.  Similarly, we can't handle
+		 * JOIN_FULL and JOIN_RIGHT, because they can produce false null
+		 * extended rows.  Also, the resulting path must not be parameterized.
+		 */
+		if (joinrel->consider_parallel &&
+			save_jointype != JOIN_UNIQUE_OUTER &&
+			jointype != JOIN_FULL &&
+			jointype != JOIN_RIGHT &&
+			outerrel->partial_pathlist != NIL &&
+			bms_is_empty(joinrel->lateral_relids) &&
+			inner_path->parallel_safe)
+		{
+			Path *cheapest_partial_outer =
+							(Path *) linitial(outerrel->partial_pathlist);
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   cheapest_partial_outer,
+									   inner_path,
+									   merge_pathkeys,
+									   cur_mergeclauses,
+									   outerkeys,
+									   innerkeys,
+									   jointype,
+									   extra);
+		}
 	}
 }
 
@@ -798,6 +902,8 @@ sort_inner_and_outer(PlannerInfo *root,
  * some sort key requirements).  So, we consider truncations of the
  * mergeclause list as well as the full list.  (Ideally we'd consider all
  * subsets of the mergeclause list, but that seems way too expensive.)
+ *
+ * is_partial passed true for generating partial merge join paths.
  */
 static void
 generate_mergejoin_paths(PlannerInfo *root,
@@ -808,7 +914,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 						 JoinPathExtraData *extra,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
-						 List *merge_pathkeys)
+						 List *merge_pathkeys,
+						 bool is_partial)
 {
 	List	   *mergeclauses;
 	List	   *innersortkeys;
@@ -859,16 +966,30 @@ generate_mergejoin_paths(PlannerInfo *root,
 	 * try_mergejoin_path will do the right thing if inner_cheapest_total is
 	 * already correctly sorted.)
 	 */
-	try_mergejoin_path(root,
-					   joinrel,
-					   outerpath,
-					   inner_cheapest_total,
-					   merge_pathkeys,
-					   mergeclauses,
-					   NIL,
-					   innersortkeys,
-					   jointype,
-					   extra);
+	if (!is_partial)
+		try_mergejoin_path(root,
+						joinrel,
+						outerpath,
+						inner_cheapest_total,
+						merge_pathkeys,
+						mergeclauses,
+						NIL,
+						innersortkeys,
+						jointype,
+						extra);
+
+	/* Generate partial path if inner is parallel safe. */
+	else if (inner_cheapest_total->parallel_safe)
+		try_partial_mergejoin_path(root,
+						joinrel,
+						outerpath,
+						inner_cheapest_total,
+						merge_pathkeys,
+						mergeclauses,
+						NIL,
+						innersortkeys,
+						jointype,
+						extra);
 
 	/* Can't do anything else if inner path needs to be unique'd */
 	if (save_jointype == JOIN_UNIQUE_INNER)
@@ -955,16 +1076,30 @@ generate_mergejoin_paths(PlannerInfo *root,
 			}
 			else
 				newclauses = mergeclauses;
-			try_mergejoin_path(root,
-							   joinrel,
-							   outerpath,
-							   innerpath,
-							   merge_pathkeys,
-							   newclauses,
-							   NIL,
-							   NIL,
-							   jointype,
-							   extra);
+			if (!is_partial)
+				try_mergejoin_path(root,
+								joinrel,
+								outerpath,
+								innerpath,
+								merge_pathkeys,
+								newclauses,
+								NIL,
+								NIL,
+								jointype,
+								extra);
+			/* Generate partial path only if innerpath is parallel safe. */
+			else if (innerpath->parallel_safe)
+				try_partial_mergejoin_path(root,
+										   joinrel,
+										   outerpath,
+										   innerpath,
+										   merge_pathkeys,
+										   newclauses,
+										   NIL,
+										   NIL,
+										   jointype,
+										   extra);
+
 			cheapest_total_inner = innerpath;
 		}
 		/* Same on the basis of cheapest startup cost ... */
@@ -998,16 +1133,29 @@ generate_mergejoin_paths(PlannerInfo *root,
 					else
 						newclauses = mergeclauses;
 				}
-				try_mergejoin_path(root,
-								   joinrel,
-								   outerpath,
-								   innerpath,
-								   merge_pathkeys,
-								   newclauses,
-								   NIL,
-								   NIL,
-								   jointype,
-								   extra);
+				if (!is_partial)
+					try_mergejoin_path(root,
+									   joinrel,
+									   outerpath,
+									   innerpath,
+									   merge_pathkeys,
+									   newclauses,
+									   NIL,
+									   NIL,
+									   jointype,
+									   extra);
+				/* Generate partial path only if innerpath is parallel safe. */
+				else if (innerpath->parallel_safe)
+					try_partial_mergejoin_path(root,
+											   joinrel,
+											   outerpath,
+											   innerpath,
+											   merge_pathkeys,
+											   newclauses,
+											   NIL,
+											   NIL,
+											   jointype,
+											   extra);
 			}
 			cheapest_startup_inner = innerpath;
 		}
@@ -1219,22 +1367,55 @@ match_unsorted_outer(PlannerInfo *root,
 		/* Generate merge join paths */
 		generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
 								 save_jointype, extra, useallclauses,
-								 inner_cheapest_total, merge_pathkeys);
+								 inner_cheapest_total, merge_pathkeys,
+								 false);
 	}
 
 	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
+	 * Consider partial nestloop and mergejoin plan if the joinrel is
+	 * parallel-safe. However, we can't handle JOIN_UNIQUE_OUTER, because
+	 * the outer path will be partial, and therefore we won't be able to
+	 * properly guarantee uniqueness.  Nor can we handle extra_lateral_rels,
+	 * since partial paths must not be parameterized.
+	 * Similarly, we can't handle JOIN_FULL and JOIN_RIGHT, because they
+	 * can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
-		save_jointype != JOIN_UNIQUE_OUTER &&
-		bms_is_empty(joinrel->lateral_relids))
+	if (!joinrel->consider_parallel ||
+		save_jointype == JOIN_UNIQUE_OUTER ||
+		!bms_is_empty(joinrel->lateral_relids) ||
+		jointype == JOIN_FULL ||
+		jointype == JOIN_RIGHT)
+		return;
+
+	if (nestjoinOK)
 		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
 								   save_jointype, extra);
+
+	/* Can't generate mergejoin path if inner rel is parameterized by outer */
+	if (inner_cheapest_total != NULL)
+	{
+		ListCell   *lc1;
+
+		/* generate merge join path for each partial outer path */
+		foreach(lc1, outerrel->partial_pathlist)
+		{
+			Path	   *outerpath = (Path *) lfirst(lc1);
+			List	   *merge_pathkeys;
+
+			/*
+			 * Figure out what useful ordering any paths we create
+			 * will have.
+			 */
+			merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											   outerpath->pathkeys);
+
+			generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
+									 save_jointype, extra, false,
+									 inner_cheapest_total, merge_pathkeys,
+									 true);
+		}
+
+	}
 }
 
 /*
#10Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Dilip Kumar (#9)
Re: Proposal : Parallel Merge Join

Hi,

On 12/21/2016 04:53 PM, Dilip Kumar wrote:

On Wed, Dec 21, 2016 at 8:39 PM, Robert Haas <robertmhaas@gmail.com> wrote:

Committed the refactoring patch with some mild cosmetic adjustments.

Thanks..

As to the second patch:

+        if (jointype == JOIN_UNIQUE_INNER)
+            jointype = JOIN_INNER;

Isn't this dead code? save_jointype might that value, but jointype won't.

Yes, it is.

I have fixed this, and also observed that comment for
try_partial_mergejoin_path header was having some problem, fixed that
too.

FWIW, I've done quite a bit of testing on this patch, and also on the
other patches adding parallel index scans and bitmap heap scan. I've
been running TPC-H and TPC-DS on 16GB data sets with each patch, looking
for regressions or crashes.

I haven't found any of that so far, which is good of course. It however
seems the plan changes only for very few queries in those benchmarks
with any of the patches, even after tweaking the costs to make parallel
plans more likely.

I'm going to try with larger scales and also --enable-cassert and post
the results during CF 2017-1.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#11Dilip Kumar
dilipbalaut@gmail.com
In reply to: Tomas Vondra (#10)
Re: Proposal : Parallel Merge Join

On Thu, Dec 29, 2016 at 3:15 AM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

FWIW, I've done quite a bit of testing on this patch, and also on the other
patches adding parallel index scans and bitmap heap scan. I've been running
TPC-H and TPC-DS on 16GB data sets with each patch, looking for regressions
or crashes.

Thanks for looking into this.

I haven't found any of that so far, which is good of course. It however
seems the plan changes only for very few queries in those benchmarks with
any of the patches, even after tweaking the costs to make parallel plans
more likely.

You can also try with reducing random_page_cost (that will help
parallel merge join with index scan), in case your data fits in memory
and you are ensuring warm cache environment.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#12Amit Kapila
amit.kapila16@gmail.com
In reply to: Dilip Kumar (#9)
Re: Proposal : Parallel Merge Join

On Wed, Dec 21, 2016 at 9:23 PM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Wed, Dec 21, 2016 at 8:39 PM, Robert Haas <robertmhaas@gmail.com> wrote:

Committed the refactoring patch with some mild cosmetic adjustments.

Thanks..

As to the second patch:

+        if (jointype == JOIN_UNIQUE_INNER)
+            jointype = JOIN_INNER;

Isn't this dead code? save_jointype might that value, but jointype won't.

Yes, it is.

I have fixed this, and also observed that comment for
try_partial_mergejoin_path header was having some problem, fixed that
too.

Review comments:
1.
+ bool is_partial);
+

Seems additional new line is not required.

2.
+ * try_partial_mergejoin_path
+ *  Consider a partial merge join path; if it appears useful,
push it into
+ *  the joinrel's pathlist via add_path().
+ */

I think in above comment, you should say ".. joinrel's
partial_pathlist via add_partial_path()"

3.
+ /*
+ * See comments in try_partial_nestloop_path().
+ */
+ Assert(bms_is_empty
(joinrel->lateral_relids));
+ if (inner_path->param_info != NULL)
+ {
+ Relids
inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+ if (!bms_is_subset
(inner_paramrels, outer_path->parent->relids))
+ return;
+ }

This same code exists in try_partial_nestloop_path() and
try_partial_hashjoin_path() with minor difference of code in
try_partial_hashjoin_path(). Can't we write a separate inline
function for this code and call from all the three places.

4.
+ /*
+ * See comments in try_nestloop_path().
+ */
+ initial_cost_mergejoin(root,
&workspace, jointype, mergeclauses,
+   outer_path,
inner_path,
+   outersortkeys, innersortkeys,
+
  extra->sjinfo);

I think in above comments, you want to say try_partial_nestloop_path().

5.
- if (joinrel->consider_parallel && nestjoinOK &&
- save_jointype != JOIN_UNIQUE_OUTER &&
- bms_is_empty(joinrel->lateral_relids))
+ if (!joinrel->consider_parallel ||
+ save_jointype == JOIN_UNIQUE_OUTER ||
+ !bms_is_empty(joinrel->lateral_relids) ||
+ jointype == JOIN_FULL ||
+ jointype == JOIN_RIGHT)
+ return;
+
+ if (nestjoinOK)

Here, we only want to create partial paths when
outerrel->partial_pathlist is not NILL, so I think you can include
that check as well. Another point to note is that in HEAD, we are not
checking for jointype as JOIN_RIGHT or JOIN_FULL for considering
parallel nestloop paths, whereas with your patch, it will do those
checks, is it a bug of HEAD or is there any other reason for including
those checks for parallel nestloop paths?

6.
+ /* Can't generate mergejoin path if inner rel is parameterized by outer */
+ if (inner_cheapest_total != NULL)
+ {
+ ListCell   *lc1;
+
+ /* generate merge join path for each partial outer path */
+ foreach(lc1, outerrel->partial_pathlist)
+ {
+ Path   *outerpath = (Path *) lfirst(lc1);
+ List   *merge_pathkeys;
+
+ /*
+ * Figure out what useful ordering any paths we create
+ * will have.
+ */
+ merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+   outerpath->pathkeys);
+
+ generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
+ save_jointype, extra, false,
+ inner_cheapest_total, merge_pathkeys,
+ true);
+ }
+
+ }

Won't it be better if you encapsulate the above chunk of code in
function consider_parallel_merjejoin() similar to
consider_parallel_nestloop()? I think that way code will look
cleaner.

7.
+ /*
+ * Figure out what useful ordering any paths we create
+ * will have.
+ */
+ merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+   outerpath->pathkeys);

indentation problem with variable outerpath->pathkeys.

8.
- try_mergejoin_path(root,
-   joinrel,
-   outerpath,
-   inner_cheapest_total,
-   merge_pathkeys,
-   mergeclauses,
-   NIL,
-   innersortkeys,
-   jointype,
-   extra);
+ if (!is_partial)
+ try_mergejoin_path(root,
+ joinrel,
+ outerpath,
+ inner_cheapest_total,
+ merge_pathkeys,
+ mergeclauses,
+ NIL,
+ innersortkeys,
+ jointype,
+ extra);
+
+ /* Generate partial path if inner is parallel safe. */
+ else if (inner_cheapest_total->parallel_safe)
+ try_partial_mergejoin_path(root,
+ joinrel,
+ outerpath,
+ inner_cheapest_total,
+ merge_pathkeys,
+ mergeclauses,
+ NIL,
+ innersortkeys,
+ jointype,
+ extra);

In above code indentation is broken, similarly, there is another code
in a patch where it is broken, try using pgindent.

--
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#13Dilip Kumar
dilipbalaut@gmail.com
In reply to: Amit Kapila (#12)
1 attachment(s)
Re: Proposal : Parallel Merge Join

On Wed, Jan 4, 2017 at 12:02 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

Review comments:
1.
+ bool is_partial);
+

Seems additional new line is not required.

Fixed

2.
+ * try_partial_mergejoin_path
+ *  Consider a partial merge join path; if it appears useful,
push it into
+ *  the joinrel's pathlist via add_path().
+ */

I think in above comment, you should say ".. joinrel's
partial_pathlist via add_partial_path()"

Fixed

3.
+ /*
+ * See comments in try_partial_nestloop_path().
+ */

This same code exists in try_partial_nestloop_path() and
try_partial_hashjoin_path() with minor difference of code in
try_partial_hashjoin_path(). Can't we write a separate inline
function for this code and call from all the three places.

It's a small check, is it ok to make it the separate function. I have
also observed similar kind of duplicate code in try_mergejoin_path and
try_hashjoin_path. However, if you think that it will be better to
move that check to an inline function I can submit a seperate patch in
this thread as a refactoring patch?

I observed one more issue that in case of partial merge join
inner_paramrels should be empty not subset of outer, so fixed the same
in the attached version. The code in the previous patch will also not
create any problem, because if the inner path is parameterized by
outerrel it will not create any merge join path.

4.
+ /*
+ * See comments in try_nestloop_path().
+ */
+ initial_cost_mergejoin(root,
&workspace, jointype, mergeclauses,
+   outer_path,
inner_path,
+   outersortkeys, innersortkeys,
+
extra->sjinfo);

I think in above comments, you want to say try_partial_nestloop_path().

Fixed

5.
- if (joinrel->consider_parallel && nestjoinOK &&
- save_jointype != JOIN_UNIQUE_OUTER &&
- bms_is_empty(joinrel->lateral_relids))
+ if (!joinrel->consider_parallel ||
+ save_jointype == JOIN_UNIQUE_OUTER ||
+ !bms_is_empty(joinrel->lateral_relids) ||
+ jointype == JOIN_FULL ||
+ jointype == JOIN_RIGHT)
+ return;
+
+ if (nestjoinOK)

Here, we only want to create partial paths when
outerrel->partial_pathlist is not NILL, so I think you can include
that check as well.

Done.

Another point to note is that in HEAD, we are not

checking for jointype as JOIN_RIGHT or JOIN_FULL for considering
parallel nestloop paths, whereas with your patch, it will do those

is al> checks, is it a bug of HEAD or is there any other reason for including

those checks for parallel nestloop paths?

Actually we can not create parallel join path in case of JOIN_RIGHT or
JOIN_FULL. nestjoinOK is marked false in case of JOIN_RIGHT or
JOIN_FULL. So it was not considered in base code as well. Now we have
mergejoin as well so it's better to keep a common check.

6.
+ /* Can't generate mergejoin path if inner rel is parameterized by outer */
+ if (inner_cheapest_total != NULL)
+ {
+ ListCell   *lc1;
+
+ /* generate merge join path for each partial outer path */
+ foreach(lc1, outerrel->partial_pathlist)
+ {
+ Path   *outerpath = (Path *) lfirst(lc1);
+ List   *merge_pathkeys;
+
+ /*
+ * Figure out what useful ordering any paths we create
+ * will have.
+ */
+ merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+   outerpath->pathkeys);
+
+ generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
+ save_jointype, extra, false,
+ inner_cheapest_total, merge_pathkeys,
+ true);
+ }
+
+ }

Won't it be better if you encapsulate the above chunk of code in
function consider_parallel_merjejoin() similar to
consider_parallel_nestloop()? I think that way code will look
cleaner.

Done

7.
+ /*
+ * Figure out what useful ordering any paths we create
+ * will have.
+ */
+ merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+   outerpath->pathkeys);

indentation problem with variable outerpath->pathkeys.

Fixed

8.
- try_mergejoin_path(root,
-   joinrel,
-   outerpath,
-   inner_cheapest_total,
-   merge_pathkeys,
-   mergeclauses,
-   NIL,
-   innersortkeys,
-   jointype,
-   extra);
+ if (!is_partial)
+ try_mergejoin_path(root,
+ joinrel,
+ outerpath,
+ inner_cheapest_total,
+ merge_pathkeys,
+ mergeclauses,
+ NIL,
+ innersortkeys,
+ jointype,
+ extra);
+
+ /* Generate partial path if inner is parallel safe. */
+ else if (inner_cheapest_total->parallel_safe)
+ try_partial_mergejoin_path(root,
+ joinrel,
+ outerpath,
+ inner_cheapest_total,
+ merge_pathkeys,
+ mergeclauses,
+ NIL,
+ innersortkeys,
+ jointype,
+ extra);

In above code indentation is broken, similarly, there is another code
in a patch where it is broken, try using pgindent.

Fixed with pgindent.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

parallel_mergejoin_v4.patchapplication/octet-stream; name=parallel_mergejoin_v4.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 7c30ec6..d0d4af9 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -40,6 +40,13 @@ static void consider_parallel_nestloop(PlannerInfo *root,
 						   RelOptInfo *innerrel,
 						   JoinType jointype,
 						   JoinPathExtraData *extra);
+static void consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -58,7 +65,8 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 JoinPathExtraData *extra,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
-						 List *merge_pathkeys);
+						 List *merge_pathkeys,
+						 bool is_partial);
 
 
 /*
@@ -481,6 +489,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a partial merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_partial_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+						   RelOptInfo *joinrel,
+						   Path *outer_path,
+						   Path *inner_path,
+						   List *pathkeys,
+						   List *mergeclauses,
+						   List *outersortkeys,
+						   List *innersortkeys,
+						   JoinType jointype,
+						   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_hashjoin_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_empty(inner_paramrels))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+					 create_mergejoin_path(root,
+										   joinrel,
+										   jointype,
+										   &workspace,
+										   extra->sjinfo,
+										   outer_path,
+										   inner_path,
+										   extra->restrictlist,
+										   pathkeys,
+										   NULL,
+										   mergeclauses,
+										   outersortkeys,
+										   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -649,6 +727,7 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
 	List	   *all_pathkeys;
@@ -782,6 +861,37 @@ sort_inner_and_outer(PlannerInfo *root,
 						   innerkeys,
 						   jointype,
 						   extra);
+
+		/*
+		 * If the joinrel is parallel-safe, we may be able to consider a
+		 * partial merge join.  However, we can't handle JOIN_UNIQUE_OUTER,
+		 * because the outer path will be partial, and therefore we won't be
+		 * able to properly guarantee uniqueness.  Similarly, we can't handle
+		 * JOIN_FULL and JOIN_RIGHT, because they can produce false null
+		 * extended rows.  Also, the resulting path must not be parameterized.
+		 */
+		if (joinrel->consider_parallel &&
+			save_jointype != JOIN_UNIQUE_OUTER &&
+			jointype != JOIN_FULL &&
+			jointype != JOIN_RIGHT &&
+			outerrel->partial_pathlist != NIL &&
+			bms_is_empty(joinrel->lateral_relids) &&
+			inner_path->parallel_safe)
+		{
+			Path	   *cheapest_partial_outer =
+			(Path *) linitial(outerrel->partial_pathlist);
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   cheapest_partial_outer,
+									   inner_path,
+									   merge_pathkeys,
+									   cur_mergeclauses,
+									   outerkeys,
+									   innerkeys,
+									   jointype,
+									   extra);
+		}
 	}
 }
 
@@ -798,6 +908,8 @@ sort_inner_and_outer(PlannerInfo *root,
  * some sort key requirements).  So, we consider truncations of the
  * mergeclause list as well as the full list.  (Ideally we'd consider all
  * subsets of the mergeclause list, but that seems way too expensive.)
+ *
+ * is_partial passed true for generating partial merge join paths.
  */
 static void
 generate_mergejoin_paths(PlannerInfo *root,
@@ -808,7 +920,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 						 JoinPathExtraData *extra,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
-						 List *merge_pathkeys)
+						 List *merge_pathkeys,
+						 bool is_partial)
 {
 	List	   *mergeclauses;
 	List	   *innersortkeys;
@@ -859,16 +972,30 @@ generate_mergejoin_paths(PlannerInfo *root,
 	 * try_mergejoin_path will do the right thing if inner_cheapest_total is
 	 * already correctly sorted.)
 	 */
-	try_mergejoin_path(root,
-					   joinrel,
-					   outerpath,
-					   inner_cheapest_total,
-					   merge_pathkeys,
-					   mergeclauses,
-					   NIL,
-					   innersortkeys,
-					   jointype,
-					   extra);
+	if (!is_partial)
+		try_mergejoin_path(root,
+						   joinrel,
+						   outerpath,
+						   inner_cheapest_total,
+						   merge_pathkeys,
+						   mergeclauses,
+						   NIL,
+						   innersortkeys,
+						   jointype,
+						   extra);
+
+	/* Generate partial path if inner is parallel safe. */
+	else if (inner_cheapest_total->parallel_safe)
+		try_partial_mergejoin_path(root,
+								   joinrel,
+								   outerpath,
+								   inner_cheapest_total,
+								   merge_pathkeys,
+								   mergeclauses,
+								   NIL,
+								   innersortkeys,
+								   jointype,
+								   extra);
 
 	/* Can't do anything else if inner path needs to be unique'd */
 	if (save_jointype == JOIN_UNIQUE_INNER)
@@ -955,16 +1082,30 @@ generate_mergejoin_paths(PlannerInfo *root,
 			}
 			else
 				newclauses = mergeclauses;
-			try_mergejoin_path(root,
-							   joinrel,
-							   outerpath,
-							   innerpath,
-							   merge_pathkeys,
-							   newclauses,
-							   NIL,
-							   NIL,
-							   jointype,
-							   extra);
+			if (!is_partial)
+				try_mergejoin_path(root,
+								   joinrel,
+								   outerpath,
+								   innerpath,
+								   merge_pathkeys,
+								   newclauses,
+								   NIL,
+								   NIL,
+								   jointype,
+								   extra);
+			/* Generate partial path only if innerpath is parallel safe. */
+			else if (innerpath->parallel_safe)
+				try_partial_mergejoin_path(root,
+										   joinrel,
+										   outerpath,
+										   innerpath,
+										   merge_pathkeys,
+										   newclauses,
+										   NIL,
+										   NIL,
+										   jointype,
+										   extra);
+
 			cheapest_total_inner = innerpath;
 		}
 		/* Same on the basis of cheapest startup cost ... */
@@ -998,16 +1139,29 @@ generate_mergejoin_paths(PlannerInfo *root,
 					else
 						newclauses = mergeclauses;
 				}
-				try_mergejoin_path(root,
-								   joinrel,
-								   outerpath,
-								   innerpath,
-								   merge_pathkeys,
-								   newclauses,
-								   NIL,
-								   NIL,
-								   jointype,
-								   extra);
+				if (!is_partial)
+					try_mergejoin_path(root,
+									   joinrel,
+									   outerpath,
+									   innerpath,
+									   merge_pathkeys,
+									   newclauses,
+									   NIL,
+									   NIL,
+									   jointype,
+									   extra);
+				/* Generate partial path only if innerpath is parallel safe. */
+				else if (innerpath->parallel_safe)
+					try_partial_mergejoin_path(root,
+											   joinrel,
+											   outerpath,
+											   innerpath,
+											   merge_pathkeys,
+											   newclauses,
+											   NIL,
+											   NIL,
+											   jointype,
+											   extra);
 			}
 			cheapest_startup_inner = innerpath;
 		}
@@ -1219,22 +1373,78 @@ match_unsorted_outer(PlannerInfo *root,
 		/* Generate merge join paths */
 		generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
 								 save_jointype, extra, useallclauses,
-								 inner_cheapest_total, merge_pathkeys);
+								 inner_cheapest_total, merge_pathkeys,
+								 false);
 	}
 
 	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
+	 * Consider partial nestloop and mergejoin plan if outerrel has any
+	 * partial path and the joinrel is parallel-safe.  However, we can't
+	 * handle JOIN_UNIQUE_OUTER, because the outer path will be partial, and
+	 * therefore we won't be able to properly guarantee uniqueness.  Nor can
+	 * we handle extra_lateral_rels, since partial paths must not be
+	 * parameterized. Similarly, we can't handle JOIN_FULL and JOIN_RIGHT,
+	 * because they can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
-		save_jointype != JOIN_UNIQUE_OUTER &&
-		bms_is_empty(joinrel->lateral_relids))
+	if (outerrel->partial_pathlist == NIL ||
+		!joinrel->consider_parallel ||
+		save_jointype == JOIN_UNIQUE_OUTER ||
+		!bms_is_empty(joinrel->lateral_relids) ||
+		jointype == JOIN_FULL ||
+		jointype == JOIN_RIGHT)
+		return;
+
+	if (nestjoinOK)
 		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
 								   save_jointype, extra);
+
+	/* Can't generate mergejoin path if inner rel is parameterized by outer */
+	if (inner_cheapest_total != NULL)
+		consider_parallel_mergejoin(root, joinrel, outerrel, innerrel,
+									save_jointype, extra,
+									inner_cheapest_total);
+}
+
+/*
+ * consider_parallel_mergejoin
+ *	  Try to build partial paths for a joinrel by joining a partial path
+ *	  for the outer relation to a complete path for the inner relation.
+ *
+ * 'joinrel' is the join relation
+ * 'outerrel' is the outer join relation
+ * 'innerrel' is the inner join relation
+ * 'jointype' is the type of join to do
+ * 'extra' contains additional input values
+ * 'inner_cheapest_total' cheapest total path for innerrel.
+ */
+static void
+consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total)
+{
+	ListCell   *lc1;
+
+	/* generate merge join path for each partial outer path */
+	foreach(lc1, outerrel->partial_pathlist)
+	{
+		Path	   *outerpath = (Path *) lfirst(lc1);
+		List	   *merge_pathkeys;
+
+		/*
+		 * Figure out what useful ordering any paths we create will have.
+		 */
+		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											 outerpath->pathkeys);
+
+		generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
+								 jointype, extra, false,
+								 inner_cheapest_total, merge_pathkeys,
+								 true);
+	}
 }
 
 /*
#14Michael Paquier
michael.paquier@gmail.com
In reply to: Dilip Kumar (#13)
Re: Proposal : Parallel Merge Join

On Thu, Jan 5, 2017 at 12:57 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Wed, Jan 4, 2017 at 12:02 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

Review comments:
1.
+ bool is_partial);
+

Seems additional new line is not required.

Fixed

This patch has a patch, no new reviews. Moved to CF 2017-03.
--
Michael

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#15Amit Kapila
amit.kapila16@gmail.com
In reply to: Dilip Kumar (#13)
Re: Proposal : Parallel Merge Join

On Wed, Jan 4, 2017 at 9:27 PM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Wed, Jan 4, 2017 at 12:02 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

3.
+ /*
+ * See comments in try_partial_nestloop_path().
+ */

This same code exists in try_partial_nestloop_path() and
try_partial_hashjoin_path() with minor difference of code in
try_partial_hashjoin_path(). Can't we write a separate inline
function for this code and call from all the three places.

It's a small check, is it ok to make it the separate function. I have
also observed similar kind of duplicate code in try_mergejoin_path and
try_hashjoin_path. However, if you think that it will be better to
move that check to an inline function I can submit a seperate patch in
this thread as a refactoring patch?

Let's keep that check unchanged.

Few review comments on the latest version of the patch:
1.
- if (joinrel->consider_parallel && nestjoinOK &&
- save_jointype != JOIN_UNIQUE_OUTER &&
- bms_is_empty(joinrel->lateral_relids))
+ if (outerrel->partial_pathlist == NIL ||
+ !joinrel->consider_parallel ||
+ save_jointype == JOIN_UNIQUE_OUTER ||
+ !bms_is_empty(joinrel->lateral_relids) ||
+ jointype == JOIN_FULL ||
+ jointype == JOIN_RIGHT)
+ return;

For the matter of consistency, I think the above checks should be in
same order as they are in function hash_inner_and_outer(). I wonder
why are you using jointype in above check instead of save_jointype, it
seems to me we can use save_jointype for all the cases.

2.
+ generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
+
jointype, extra, false,
+
inner_cheapest_total, merge_pathkeys,
+
true);

IIUC, the reason of passing a 7th parameter (useallclauses) as false
is that it can be true only for Right and Full joins and for both we
don't generate partial merge join paths. If so, then I think it is
better to add a comment about such an assumption, so that we don't
forget to update this code in future if we need to useallclauses for
something else. Another way to deal with this is that you can pass
the value of useallclauses to consider_parallel_mergejoin() and then
to generate_mergejoin_paths(). I feel second way is better, but if
for some reason you don't find it appropriate then at least add a
comment.

3.
generate_mergejoin_paths()
{
..
..
- try_mergejoin_path(root,
- joinrel,
- outerpath,
- innerpath,
- merge_pathkeys,
- newclauses,
- NIL,
- NIL,
- jointype,
- extra);

+ if (!is_partial)
+ try_mergejoin_path(root,
+   joinrel,
+   outerpath,
+   innerpath,
+   merge_pathkeys,
+   newclauses,
+   NIL,
+   NIL,
+   jointype,
+   extra);
+ /* Generate partial path only if innerpath is parallel safe. */
+ else if (innerpath->parallel_safe)
+ try_partial_mergejoin_path(root,
+   joinrel,
+   outerpath,
+   innerpath,
+   merge_pathkeys,
+   newclauses,
+   NIL,
+   NIL,
+   jointype,
+   extra);

..
}

The above and similar changes in generate_mergejoin_paths() doesn't
look good and in some cases when innerpath is *parallel-unsafe*, you
need to perform some extra computation which is not required. How
about writing a separate function generate_partial_mergejoin_paths()?
I think you can save some extra computation and it will look neat. I
understand that there will be some code duplication, but not sure if
there is any other better way.

How about adding one simple test case as I have kept for other
parallel patches like parallel index scan, parallel subplan, etc.?

--
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#16Dilip Kumar
dilipbalaut@gmail.com
In reply to: Amit Kapila (#15)
1 attachment(s)
Re: Proposal : Parallel Merge Join

On Tue, Feb 14, 2017 at 12:25 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

Few review comments on the latest version of the patch:
1.
- if (joinrel->consider_parallel && nestjoinOK &&
- save_jointype != JOIN_UNIQUE_OUTER &&
- bms_is_empty(joinrel->lateral_relids))
+ if (outerrel->partial_pathlist == NIL ||
+ !joinrel->consider_parallel ||
+ save_jointype == JOIN_UNIQUE_OUTER ||
+ !bms_is_empty(joinrel->lateral_relids) ||
+ jointype == JOIN_FULL ||
+ jointype == JOIN_RIGHT)
+ return;

For the matter of consistency, I think the above checks should be in
same order as they are in function hash_inner_and_outer(). I wonder
why are you using jointype in above check instead of save_jointype, it
seems to me we can use save_jointype for all the cases.

Done

2.
+ generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
+
jointype, extra, false,
+
inner_cheapest_total, merge_pathkeys,
+
true);

IIUC, the reason of passing a 7th parameter (useallclauses) as false
is that it can be true only for Right and Full joins and for both we
don't generate partial merge join paths. If so, then I think it is
better to add a comment about such an assumption, so that we don't
forget to update this code in future if we need to useallclauses for
something else. Another way to deal with this is that you can pass
the value of useallclauses to consider_parallel_mergejoin() and then
to generate_mergejoin_paths(). I feel second way is better, but if
for some reason you don't find it appropriate then at least add a
comment.

After fixing 3rd comments this will not be needed.

3.
generate_mergejoin_paths()
{

The above and similar changes in generate_mergejoin_paths() doesn't
look good and in some cases when innerpath is *parallel-unsafe*, you
need to perform some extra computation which is not required. How
about writing a separate function generate_partial_mergejoin_paths()?
I think you can save some extra computation and it will look neat. I
understand that there will be some code duplication, but not sure if
there is any other better way.

Okay, I have done as suggested.

Apart from this, there was one small problem in an earlier version
which I have corrected in this.

+ /* Consider only parallel safe inner path */
+ if (innerpath != NULL &&
+ innerpath->parallel_safe &&
+ (cheapest_total_inner == NULL ||
+ cheapest_total_inner->parallel_safe == false ||
+ compare_path_costs(innerpath, cheapest_total_inner,
+ TOTAL_COST) < 0))

In this comparison, we were only checking if innerpath is cheaper than
the cheapest_total_inner then generate path with this new inner path
as well. Now, I have added one more check if cheapest_total_inner was
not parallel safe then also consider a path with this new inner
(provided this inner is parallel safe).

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

parallel_mergejoin_v5.patchapplication/octet-stream; name=parallel_mergejoin_v5.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 2897245..f31a52f 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -40,6 +40,13 @@ static void consider_parallel_nestloop(PlannerInfo *root,
 						   RelOptInfo *innerrel,
 						   JoinType jointype,
 						   JoinPathExtraData *extra);
+static void consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -59,6 +66,14 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
 						 List *merge_pathkeys);
+static void generate_parallel_mergejoin_paths(PlannerInfo *root,
+								  RelOptInfo *joinrel,
+								  RelOptInfo *innerrel,
+								  Path *outerpath,
+								  JoinType jointype,
+								  JoinPathExtraData *extra,
+								  Path *inner_cheapest_total,
+								  List *merge_pathkeys);
 
 
 /*
@@ -481,6 +496,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a partial merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_partial_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+						   RelOptInfo *joinrel,
+						   Path *outer_path,
+						   Path *inner_path,
+						   List *pathkeys,
+						   List *mergeclauses,
+						   List *outersortkeys,
+						   List *innersortkeys,
+						   JoinType jointype,
+						   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_hashjoin_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_empty(inner_paramrels))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+					 create_mergejoin_path(root,
+										   joinrel,
+										   jointype,
+										   &workspace,
+										   extra->sjinfo,
+										   outer_path,
+										   inner_path,
+										   extra->restrictlist,
+										   pathkeys,
+										   NULL,
+										   mergeclauses,
+										   outersortkeys,
+										   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -649,6 +734,7 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
 	List	   *all_pathkeys;
@@ -782,6 +868,37 @@ sort_inner_and_outer(PlannerInfo *root,
 						   innerkeys,
 						   jointype,
 						   extra);
+
+		/*
+		 * If the joinrel is parallel-safe, we may be able to consider a
+		 * partial merge join.  However, we can't handle JOIN_UNIQUE_OUTER,
+		 * because the outer path will be partial, and therefore we won't be
+		 * able to properly guarantee uniqueness.  Similarly, we can't handle
+		 * JOIN_FULL and JOIN_RIGHT, because they can produce false null
+		 * extended rows.  Also, the resulting path must not be parameterized.
+		 */
+		if (joinrel->consider_parallel &&
+			save_jointype != JOIN_UNIQUE_OUTER &&
+			save_jointype != JOIN_FULL &&
+			save_jointype != JOIN_RIGHT &&
+			outerrel->partial_pathlist != NIL &&
+			bms_is_empty(joinrel->lateral_relids) &&
+			inner_path->parallel_safe)
+		{
+			Path	   *cheapest_partial_outer = (Path *) linitial(
+												 outerrel->partial_pathlist);
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   cheapest_partial_outer,
+									   inner_path,
+									   merge_pathkeys,
+									   cur_mergeclauses,
+									   outerkeys,
+									   innerkeys,
+									   jointype,
+									   extra);
+		}
 	}
 }
 
@@ -1021,6 +1138,190 @@ generate_mergejoin_paths(PlannerInfo *root,
 }
 
 /*
+ * generate_parallel_mergejoin_paths
+ *
+ *	As above, but it will generate parallel merge join paths.
+ */
+static void
+generate_parallel_mergejoin_paths(PlannerInfo *root,
+								  RelOptInfo *joinrel,
+								  RelOptInfo *innerrel,
+								  Path *outerpath,
+								  JoinType jointype,
+								  JoinPathExtraData *extra,
+								  Path *inner_cheapest_total,
+								  List *merge_pathkeys)
+{
+	List	   *mergeclauses;
+	List	   *innersortkeys;
+	List	   *trialsortkeys;
+	Path	   *cheapest_startup_inner;
+	Path	   *cheapest_total_inner;
+	JoinType	save_jointype = jointype;
+	int			num_sortkeys;
+	int			sortkeycnt;
+
+	/* We should never come here for JOIN_UNIQUE_OUTER */
+	Assert(jointype != JOIN_UNIQUE_OUTER);
+
+	if (jointype == JOIN_UNIQUE_INNER)
+		jointype = JOIN_INNER;
+
+	/* Look for useful mergeclauses (if any) */
+	mergeclauses = find_mergeclauses_for_pathkeys(root,
+												  outerpath->pathkeys,
+												  true,
+												  extra->mergeclause_list);
+
+	/*
+	 * Done with this outer path if no chance for a mergejoin.
+	 */
+	if (mergeclauses == NIL)
+		return;
+
+	/* Compute the required ordering of the inner path */
+	innersortkeys = make_inner_pathkeys_for_merge(root,
+												  mergeclauses,
+												  outerpath->pathkeys);
+
+	/* Generate partial path if inner is parallel safe. */
+	if (inner_cheapest_total->parallel_safe)
+		try_partial_mergejoin_path(root,
+								   joinrel,
+								   outerpath,
+								   inner_cheapest_total,
+								   merge_pathkeys,
+								   mergeclauses,
+								   NIL,
+								   innersortkeys,
+								   jointype,
+								   extra);
+
+	/* Can't do anything else if inner path needs to be unique'd */
+	if (save_jointype == JOIN_UNIQUE_INNER)
+		return;
+
+	/*
+	 * See comments in generate_mergejoin_paths
+	 */
+	if (pathkeys_contained_in(innersortkeys,
+							  inner_cheapest_total->pathkeys))
+	{
+		/* inner_cheapest_total didn't require a sort */
+		cheapest_startup_inner = inner_cheapest_total;
+		cheapest_total_inner = inner_cheapest_total;
+	}
+	else
+	{
+		/* it did require a sort, at least for the full set of keys */
+		cheapest_startup_inner = NULL;
+		cheapest_total_inner = NULL;
+	}
+	num_sortkeys = list_length(innersortkeys);
+	if (num_sortkeys > 1)
+		trialsortkeys = list_copy(innersortkeys);		/* need modifiable copy */
+	else
+		trialsortkeys = innersortkeys;	/* won't really truncate */
+
+	for (sortkeycnt = num_sortkeys; sortkeycnt > 0; sortkeycnt--)
+	{
+		Path	   *innerpath;
+		List	   *newclauses = NIL;
+
+		/*
+		 * Look for an inner path ordered well enough for the first
+		 * 'sortkeycnt' innersortkeys.  NB: trialsortkeys list is modified
+		 * destructively, which is why we made a copy...
+		 */
+		trialsortkeys = list_truncate(trialsortkeys, sortkeycnt);
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   TOTAL_COST);
+		/* Consider only parallel safe inner path */
+		if (innerpath != NULL &&
+			innerpath->parallel_safe &&
+			(cheapest_total_inner == NULL ||
+			 cheapest_total_inner->parallel_safe == false ||
+			 compare_path_costs(innerpath, cheapest_total_inner,
+								TOTAL_COST) < 0))
+		{
+			/* Found a cheap (or even-cheaper) sorted parallel safer path */
+			/* Select the right mergeclauses, if we didn't already */
+			if (sortkeycnt < num_sortkeys)
+			{
+				newclauses = find_mergeclauses_for_pathkeys(root,
+															trialsortkeys,
+															false,
+															mergeclauses);
+				Assert(newclauses != NIL);
+			}
+			else
+				newclauses = mergeclauses;
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   outerpath,
+									   innerpath,
+									   merge_pathkeys,
+									   newclauses,
+									   NIL,
+									   NIL,
+									   jointype,
+									   extra);
+
+			cheapest_total_inner = innerpath;
+		}
+		/* Same on the basis of cheapest startup cost ... */
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   STARTUP_COST);
+		if (innerpath != NULL &&
+			innerpath->parallel_safe &&
+			(cheapest_startup_inner == NULL ||
+			 cheapest_startup_inner->parallel_safe == false ||
+			 compare_path_costs(innerpath, cheapest_startup_inner,
+								STARTUP_COST) < 0))
+		{
+			/* Found a cheap (or even-cheaper) sorted parallel safe path */
+			if (innerpath != cheapest_total_inner)
+			{
+				/*
+				 * Avoid rebuilding clause list if we already made one; saves
+				 * memory in big join trees...
+				 */
+				if (newclauses == NIL)
+				{
+					if (sortkeycnt < num_sortkeys)
+					{
+						newclauses =
+							find_mergeclauses_for_pathkeys(root,
+														   trialsortkeys,
+														   false,
+														   mergeclauses);
+						Assert(newclauses != NIL);
+					}
+					else
+						newclauses = mergeclauses;
+				}
+				try_partial_mergejoin_path(root,
+										   joinrel,
+										   outerpath,
+										   innerpath,
+										   merge_pathkeys,
+										   newclauses,
+										   NIL,
+										   NIL,
+										   jointype,
+										   extra);
+			}
+			cheapest_startup_inner = innerpath;
+		}
+	}
+}
+
+/*
  * match_unsorted_outer
  *	  Creates possible join paths for processing a single join relation
  *	  'joinrel' by employing either iterative substitution or
@@ -1223,18 +1524,73 @@ match_unsorted_outer(PlannerInfo *root,
 	}
 
 	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
+	 * Consider partial nestloop and mergejoin plan if outerrel has any
+	 * partial path and the joinrel is parallel-safe.  However, we can't
+	 * handle JOIN_UNIQUE_OUTER, because the outer path will be partial, and
+	 * therefore we won't be able to properly guarantee uniqueness.  Nor can
+	 * we handle extra_lateral_rels, since partial paths must not be
+	 * parameterized. Similarly, we can't handle JOIN_FULL and JOIN_RIGHT,
+	 * because they can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
-		save_jointype != JOIN_UNIQUE_OUTER &&
-		bms_is_empty(joinrel->lateral_relids))
+	if (!(joinrel->consider_parallel &&
+		  save_jointype != JOIN_UNIQUE_OUTER &&
+		  save_jointype != JOIN_FULL &&
+		  save_jointype != JOIN_RIGHT &&
+		  outerrel->partial_pathlist != NIL &&
+		  bms_is_empty(joinrel->lateral_relids)))
+		return;
+
+	if (nestjoinOK)
 		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
 								   save_jointype, extra);
+
+	/* Can't generate mergejoin path if inner rel is parameterized by outer */
+	if (inner_cheapest_total != NULL)
+		consider_parallel_mergejoin(root, joinrel, outerrel, innerrel,
+									save_jointype, extra,
+									inner_cheapest_total);
+}
+
+/*
+ * consider_parallel_mergejoin
+ *	  Try to build partial paths for a joinrel by joining a partial path
+ *	  for the outer relation to a complete path for the inner relation.
+ *
+ * 'joinrel' is the join relation
+ * 'outerrel' is the outer join relation
+ * 'innerrel' is the inner join relation
+ * 'jointype' is the type of join to do
+ * 'extra' contains additional input values
+ * 'inner_cheapest_total' cheapest total path for innerrel.
+ */
+static void
+consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total)
+{
+	ListCell   *lc1;
+
+	/* generate merge join path for each partial outer path */
+	foreach(lc1, outerrel->partial_pathlist)
+	{
+		Path	   *outerpath = (Path *) lfirst(lc1);
+		List	   *merge_pathkeys;
+
+		/*
+		 * Figure out what useful ordering any paths we create will have.
+		 */
+		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											 outerpath->pathkeys);
+
+		generate_parallel_mergejoin_paths(root, joinrel, innerrel,
+										  outerpath, jointype, extra,
+										  inner_cheapest_total,
+										  merge_pathkeys);
+	}
 }
 
 /*
#17Amit Kapila
amit.kapila16@gmail.com
In reply to: Dilip Kumar (#16)
Re: Proposal : Parallel Merge Join

On Tue, Feb 14, 2017 at 5:22 PM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Tue, Feb 14, 2017 at 12:25 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:
Apart from this, there was one small problem in an earlier version
which I have corrected in this.

+ /* Consider only parallel safe inner path */
+ if (innerpath != NULL &&
+ innerpath->parallel_safe &&
+ (cheapest_total_inner == NULL ||
+ cheapest_total_inner->parallel_safe == false ||
+ compare_path_costs(innerpath, cheapest_total_inner,
+ TOTAL_COST) < 0))

In this comparison, we were only checking if innerpath is cheaper than
the cheapest_total_inner then generate path with this new inner path
as well. Now, I have added one more check if cheapest_total_inner was
not parallel safe then also consider a path with this new inner
(provided this inner is parallel safe).

What advantage do you see for considering such a path when the cost of
innerpath is more than cheapest_total_inner? Remember the more paths
we try to consider, the more time we spend in the planner. By any
chance are you able to generate any query where it will give benefit
by considering costlier innerpath?

2.
+static void generate_parallel_mergejoin_paths(PlannerInfo *root,
+  RelOptInfo *joinrel,
+  RelOptInfo *innerrel,
+  Path *outerpath,
+  JoinType jointype,
+  JoinPathExtraData *extra,
+  Path *inner_cheapest_total,
+  List *merge_pathkeys);

It is better to name this function as
generate_partial_mergejoin_paths() as we are generating only partial
paths in this function and accordingly change the comment on top of
the function. I see that you might be naming it based on
consider_parallel_*, however, I think it is better to use partial in
the name as that is what we are doing inside that function. Also, I
think this function has removed/changed some handling related to
unique outer and full joins, so it is better to mention that in the
function comments, something like "unlike above function, this
function doesn't expect to handle join types JOIN_UNIQUE_OUTER or
JOIN_FULL" and add Assert for both of those types.

3.
A test case is still missing.

--
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#18Dilip Kumar
dilipbalaut@gmail.com
In reply to: Amit Kapila (#17)
Re: Proposal : Parallel Merge Join

On Fri, Feb 24, 2017 at 3:04 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

Thanks for the comments.

What advantage do you see for considering such a path when the cost of
innerpath is more than cheapest_total_inner? Remember the more paths
we try to consider, the more time we spend in the planner. By any
chance are you able to generate any query where it will give benefit
by considering costlier innerpath?

If inner_cheapest_total path is not parallel safe then I am trying to
find the cheapest parallel safe path and generate partial merge join.
I agree that it will consider one extra path while considering every
partial merge join path (I think with this addition we will not get
parallel merge path for the any more TPCH query). I feel in general
we can get some better plan by this especially when gather is the
cheapest path for inner relation(which is not parallel safe) but at
the cost of considering some extra paths. What is your thought on
this?

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#19Amit Kapila
amit.kapila16@gmail.com
In reply to: Dilip Kumar (#18)
Re: Proposal : Parallel Merge Join

On Fri, Feb 24, 2017 at 3:42 PM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Fri, Feb 24, 2017 at 3:04 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

Thanks for the comments.

What advantage do you see for considering such a path when the cost of
innerpath is more than cheapest_total_inner? Remember the more paths
we try to consider, the more time we spend in the planner. By any
chance are you able to generate any query where it will give benefit
by considering costlier innerpath?

If inner_cheapest_total path is not parallel safe then I am trying to
find the cheapest parallel safe path and generate partial merge join.

Not sure, if we can just ignore the cheapest inner path because it is
not parallel safe. It is also possible that you pick costly inner
path just because it is parallel safe and then, later on, you have to
discard it because the overall cost of partial merge join is much
more.

I agree that it will consider one extra path while considering every
partial merge join path

Hmm, AFAICS, it will consider two extra paths per sort key (the loop
around considering such paths is up to num_sortkeys)

(I think with this addition we will not get
parallel merge path for the any more TPCH query). I feel in general
we can get some better plan by this especially when gather is the
cheapest path for inner relation(which is not parallel safe) but at
the cost of considering some extra paths.

I agree in some cases it could be better, but I think benefits are not
completely clear, so probably we can leave it as of now and if later
any one comes with a clear use case or can see the benefits of such
path then we can include it.

--
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#20Dilip Kumar
dilipbalaut@gmail.com
In reply to: Amit Kapila (#17)
1 attachment(s)
Re: Proposal : Parallel Merge Join

On Fri, Feb 24, 2017 at 3:04 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

What advantage do you see for considering such a path when the cost of
innerpath is more than cheapest_total_inner? Remember the more paths
we try to consider, the more time we spend in the planner. By any
chance are you able to generate any query where it will give benefit
by considering costlier innerpath?

Changed

2.
+static void generate_parallel_mergejoin_paths(PlannerInfo *root,
+  RelOptInfo *joinrel,
+  RelOptInfo *innerrel,
+  Path *outerpath,
+  JoinType jointype,
+  JoinPathExtraData *extra,
+  Path *inner_cheapest_total,
+  List *merge_pathkeys);

It is better to name this function as
generate_partial_mergejoin_paths() as we are generating only partial
paths in this function and accordingly change the comment on top of
the function. I see that you might be naming it based on
consider_parallel_*, however, I think it is better to use partial in
the name as that is what we are doing inside that function. Also, I
think this function has removed/changed some handling related to
unique outer and full joins, so it is better to mention that in the
function comments, something like "unlike above function, this
function doesn't expect to handle join types JOIN_UNIQUE_OUTER or
JOIN_FULL" and add Assert for both of those types.

Done

3.
A test case is still missing.

Added

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

parallel_mergejoin_v6.patchapplication/octet-stream; name=parallel_mergejoin_v6.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 2897245..dd34860 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -40,6 +40,13 @@ static void consider_parallel_nestloop(PlannerInfo *root,
 						   RelOptInfo *innerrel,
 						   JoinType jointype,
 						   JoinPathExtraData *extra);
+static void consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -59,6 +66,14 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
 						 List *merge_pathkeys);
+static void generate_partial_mergejoin_paths(PlannerInfo *root,
+								 RelOptInfo *joinrel,
+								 RelOptInfo *innerrel,
+								 Path *outerpath,
+								 JoinType jointype,
+								 JoinPathExtraData *extra,
+								 Path *inner_cheapest_total,
+								 List *merge_pathkeys);
 
 
 /*
@@ -481,6 +496,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a partial merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_partial_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+						   RelOptInfo *joinrel,
+						   Path *outer_path,
+						   Path *inner_path,
+						   List *pathkeys,
+						   List *mergeclauses,
+						   List *outersortkeys,
+						   List *innersortkeys,
+						   JoinType jointype,
+						   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_hashjoin_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_empty(inner_paramrels))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+					 create_mergejoin_path(root,
+										   joinrel,
+										   jointype,
+										   &workspace,
+										   extra->sjinfo,
+										   outer_path,
+										   inner_path,
+										   extra->restrictlist,
+										   pathkeys,
+										   NULL,
+										   mergeclauses,
+										   outersortkeys,
+										   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -649,6 +734,7 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
 	List	   *all_pathkeys;
@@ -782,6 +868,37 @@ sort_inner_and_outer(PlannerInfo *root,
 						   innerkeys,
 						   jointype,
 						   extra);
+
+		/*
+		 * If the joinrel is parallel-safe, we may be able to consider a
+		 * partial merge join.  However, we can't handle JOIN_UNIQUE_OUTER,
+		 * because the outer path will be partial, and therefore we won't be
+		 * able to properly guarantee uniqueness.  Similarly, we can't handle
+		 * JOIN_FULL and JOIN_RIGHT, because they can produce false null
+		 * extended rows.  Also, the resulting path must not be parameterized.
+		 */
+		if (joinrel->consider_parallel &&
+			save_jointype != JOIN_UNIQUE_OUTER &&
+			save_jointype != JOIN_FULL &&
+			save_jointype != JOIN_RIGHT &&
+			outerrel->partial_pathlist != NIL &&
+			bms_is_empty(joinrel->lateral_relids) &&
+			inner_path->parallel_safe)
+		{
+			Path	   *cheapest_partial_outer = (Path *) linitial(
+												 outerrel->partial_pathlist);
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   cheapest_partial_outer,
+									   inner_path,
+									   merge_pathkeys,
+									   cur_mergeclauses,
+									   outerkeys,
+									   innerkeys,
+									   jointype,
+									   extra);
+		}
 	}
 }
 
@@ -1021,6 +1138,197 @@ generate_mergejoin_paths(PlannerInfo *root,
 }
 
 /*
+ * generate_partial_mergejoin_paths
+ *
+ *	As above, but it will generate partial paths.  And, also in this function
+ *	we don't handle JOIN_UNIQUE_OUTER, JOIN_FULL or JOIN_RIGHT join types
+ *	because we don't support these join types for partial paths.
+ */
+static void
+generate_partial_mergejoin_paths(PlannerInfo *root,
+								 RelOptInfo *joinrel,
+								 RelOptInfo *innerrel,
+								 Path *outerpath,
+								 JoinType jointype,
+								 JoinPathExtraData *extra,
+								 Path *inner_cheapest_total,
+								 List *merge_pathkeys)
+{
+	List	   *mergeclauses;
+	List	   *innersortkeys;
+	List	   *trialsortkeys;
+	Path	   *cheapest_startup_inner;
+	Path	   *cheapest_total_inner;
+	JoinType	save_jointype = jointype;
+	int			num_sortkeys;
+	int			sortkeycnt;
+
+	/*
+	 * We should never come here for JOIN_UNIQUE_OUTER, JOIN_FULL or
+	 * JOIN_RIGHT.
+	 */
+	Assert(jointype != JOIN_UNIQUE_OUTER);
+	Assert(jointype != JOIN_FULL);
+	Assert(jointype != JOIN_RIGHT);
+
+	if (jointype == JOIN_UNIQUE_INNER)
+		jointype = JOIN_INNER;
+
+	/* Look for useful mergeclauses (if any) */
+	mergeclauses = find_mergeclauses_for_pathkeys(root,
+												  outerpath->pathkeys,
+												  true,
+												  extra->mergeclause_list);
+
+	/*
+	 * Done with this outer path if no chance for a mergejoin.
+	 */
+	if (mergeclauses == NIL)
+		return;
+
+	/* Compute the required ordering of the inner path */
+	innersortkeys = make_inner_pathkeys_for_merge(root,
+												  mergeclauses,
+												  outerpath->pathkeys);
+
+	/* Generate partial path if inner is parallel safe. */
+	if (inner_cheapest_total->parallel_safe)
+		try_partial_mergejoin_path(root,
+								   joinrel,
+								   outerpath,
+								   inner_cheapest_total,
+								   merge_pathkeys,
+								   mergeclauses,
+								   NIL,
+								   innersortkeys,
+								   jointype,
+								   extra);
+
+	/* Can't do anything else if inner path needs to be unique'd */
+	if (save_jointype == JOIN_UNIQUE_INNER)
+		return;
+
+	/*
+	 * See comments in generate_mergejoin_paths
+	 */
+	if (pathkeys_contained_in(innersortkeys,
+							  inner_cheapest_total->pathkeys))
+	{
+		/* inner_cheapest_total didn't require a sort */
+		cheapest_startup_inner = inner_cheapest_total;
+		cheapest_total_inner = inner_cheapest_total;
+	}
+	else
+	{
+		/* it did require a sort, at least for the full set of keys */
+		cheapest_startup_inner = NULL;
+		cheapest_total_inner = NULL;
+	}
+	num_sortkeys = list_length(innersortkeys);
+	if (num_sortkeys > 1)
+		trialsortkeys = list_copy(innersortkeys);		/* need modifiable copy */
+	else
+		trialsortkeys = innersortkeys;	/* won't really truncate */
+
+	for (sortkeycnt = num_sortkeys; sortkeycnt > 0; sortkeycnt--)
+	{
+		Path	   *innerpath;
+		List	   *newclauses = NIL;
+
+		/*
+		 * Look for an inner path ordered well enough for the first
+		 * 'sortkeycnt' innersortkeys.  NB: trialsortkeys list is modified
+		 * destructively, which is why we made a copy...
+		 */
+		trialsortkeys = list_truncate(trialsortkeys, sortkeycnt);
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   TOTAL_COST);
+		/* Consider only parallel safe inner path */
+		if (innerpath != NULL &&
+			innerpath->parallel_safe &&
+			(cheapest_total_inner == NULL ||
+			 compare_path_costs(innerpath, cheapest_total_inner,
+								TOTAL_COST) < 0))
+
+		{
+			/* Found a cheap (or even-cheaper) sorted parallel safer path */
+			/* Select the right mergeclauses, if we didn't already */
+			if (sortkeycnt < num_sortkeys)
+			{
+				newclauses = find_mergeclauses_for_pathkeys(root,
+															trialsortkeys,
+															false,
+															mergeclauses);
+				Assert(newclauses != NIL);
+			}
+			else
+				newclauses = mergeclauses;
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   outerpath,
+									   innerpath,
+									   merge_pathkeys,
+									   newclauses,
+									   NIL,
+									   NIL,
+									   jointype,
+									   extra);
+
+			cheapest_total_inner = innerpath;
+		}
+		/* Same on the basis of cheapest startup cost ... */
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   STARTUP_COST);
+		if (innerpath != NULL &&
+			innerpath->parallel_safe &&
+			(cheapest_startup_inner == NULL ||
+			 cheapest_startup_inner->parallel_safe == false ||
+			 compare_path_costs(innerpath, cheapest_startup_inner,
+								STARTUP_COST) < 0))
+		{
+			/* Found a cheap (or even-cheaper) sorted parallel safe path */
+			if (innerpath != cheapest_total_inner)
+			{
+				/*
+				 * Avoid rebuilding clause list if we already made one; saves
+				 * memory in big join trees...
+				 */
+				if (newclauses == NIL)
+				{
+					if (sortkeycnt < num_sortkeys)
+					{
+						newclauses =
+							find_mergeclauses_for_pathkeys(root,
+														   trialsortkeys,
+														   false,
+														   mergeclauses);
+						Assert(newclauses != NIL);
+					}
+					else
+						newclauses = mergeclauses;
+				}
+				try_partial_mergejoin_path(root,
+										   joinrel,
+										   outerpath,
+										   innerpath,
+										   merge_pathkeys,
+										   newclauses,
+										   NIL,
+										   NIL,
+										   jointype,
+										   extra);
+			}
+			cheapest_startup_inner = innerpath;
+		}
+	}
+}
+
+/*
  * match_unsorted_outer
  *	  Creates possible join paths for processing a single join relation
  *	  'joinrel' by employing either iterative substitution or
@@ -1223,18 +1531,73 @@ match_unsorted_outer(PlannerInfo *root,
 	}
 
 	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
+	 * Consider partial nestloop and mergejoin plan if outerrel has any
+	 * partial path and the joinrel is parallel-safe.  However, we can't
+	 * handle JOIN_UNIQUE_OUTER, because the outer path will be partial, and
+	 * therefore we won't be able to properly guarantee uniqueness.  Nor can
+	 * we handle extra_lateral_rels, since partial paths must not be
+	 * parameterized. Similarly, we can't handle JOIN_FULL and JOIN_RIGHT,
+	 * because they can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
-		save_jointype != JOIN_UNIQUE_OUTER &&
-		bms_is_empty(joinrel->lateral_relids))
+	if (!(joinrel->consider_parallel &&
+		  save_jointype != JOIN_UNIQUE_OUTER &&
+		  save_jointype != JOIN_FULL &&
+		  save_jointype != JOIN_RIGHT &&
+		  outerrel->partial_pathlist != NIL &&
+		  bms_is_empty(joinrel->lateral_relids)))
+		return;
+
+	if (nestjoinOK)
 		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
 								   save_jointype, extra);
+
+	/* Can't generate mergejoin path if inner rel is parameterized by outer */
+	if (inner_cheapest_total != NULL)
+		consider_parallel_mergejoin(root, joinrel, outerrel, innerrel,
+									save_jointype, extra,
+									inner_cheapest_total);
+}
+
+/*
+ * consider_parallel_mergejoin
+ *	  Try to build partial paths for a joinrel by joining a partial path
+ *	  for the outer relation to a complete path for the inner relation.
+ *
+ * 'joinrel' is the join relation
+ * 'outerrel' is the outer join relation
+ * 'innerrel' is the inner join relation
+ * 'jointype' is the type of join to do
+ * 'extra' contains additional input values
+ * 'inner_cheapest_total' cheapest total path for innerrel.
+ */
+static void
+consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total)
+{
+	ListCell   *lc1;
+
+	/* generate merge join path for each partial outer path */
+	foreach(lc1, outerrel->partial_pathlist)
+	{
+		Path	   *outerpath = (Path *) lfirst(lc1);
+		List	   *merge_pathkeys;
+
+		/*
+		 * Figure out what useful ordering any paths we create will have.
+		 */
+		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											 outerpath->pathkeys);
+
+		generate_partial_mergejoin_paths(root, joinrel, innerrel,
+										 outerpath, jointype, extra,
+										 inner_cheapest_total,
+										 merge_pathkeys);
+	}
 }
 
 /*
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index a5a2232..75558d0 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -169,6 +169,31 @@ select  count(*) from tenk1 where thousand > 95;
 
 reset enable_seqscan;
 reset enable_bitmapscan;
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ Finalize Aggregate
+   ->  Gather
+         Workers Planned: 4
+         ->  Partial Aggregate
+               ->  Merge Join
+                     Merge Cond: (tenk1.unique1 = tenk2.unique1)
+                     ->  Parallel Index Only Scan using tenk1_unique1 on tenk1
+                     ->  Index Only Scan using tenk2_unique1 on tenk2
+(8 rows)
+
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+ count 
+-------
+ 10000
+(1 row)
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 explain (costs off)
   select stringu1::int2 from tenk1 where unique1 = 1;
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index d72addf..ebdae7e 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -64,6 +64,16 @@ select  count(*) from tenk1 where thousand > 95;
 reset enable_seqscan;
 reset enable_bitmapscan;
 
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 
 explain (costs off)
#21Amit Kapila
amit.kapila16@gmail.com
In reply to: Dilip Kumar (#20)
Re: Proposal : Parallel Merge Join

On Fri, Feb 24, 2017 at 4:49 PM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Fri, Feb 24, 2017 at 3:04 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

What advantage do you see for considering such a path when the cost of
innerpath is more than cheapest_total_inner? Remember the more paths
we try to consider, the more time we spend in the planner. By any
chance are you able to generate any query where it will give benefit
by considering costlier innerpath?

Changed

It seems you have forgotten to change in the below check:

+ if (innerpath != NULL &&
+ innerpath->parallel_safe &&
+ (cheapest_startup_inner == NULL ||
+ cheapest_startup_inner->parallel_safe == false ||
+ compare_path_costs(innerpath, cheapest_startup_inner,
+ STARTUP_COST) < 0))

+ /* Found a cheap (or even-cheaper) sorted parallel safer path */

typo
/safer/safe

Note - Change the patch status in CF app (to Needs Review) whenever
you post a new patch. I could see that your other patch also needs a
similar update in CF app.

--
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#22Dilip Kumar
dilipbalaut@gmail.com
In reply to: Amit Kapila (#21)
1 attachment(s)
Re: Proposal : Parallel Merge Join

On Sat, Feb 25, 2017 at 11:29 AM, Amit Kapila <amit.kapila16@gmail.com> wrote:

It seems you have forgotten to change in the below check:

+ if (innerpath != NULL &&
+ innerpath->parallel_safe &&
+ (cheapest_startup_inner == NULL ||
+ cheapest_startup_inner->parallel_safe == false ||
+ compare_path_costs(innerpath, cheapest_startup_inner,
+ STARTUP_COST) < 0))

Oops, Fixed.

+ /* Found a cheap (or even-cheaper) sorted parallel safer path */

typo
/safer/safe

Fixed

Note - Change the patch status in CF app (to Needs Review) whenever
you post a new patch. I could see that your other patch also needs a
similar update in CF app.

Will changes, Thanks.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

parallel_mergejoin_v7.patchapplication/octet-stream; name=parallel_mergejoin_v7.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 2897245..f8e3be4 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -40,6 +40,13 @@ static void consider_parallel_nestloop(PlannerInfo *root,
 						   RelOptInfo *innerrel,
 						   JoinType jointype,
 						   JoinPathExtraData *extra);
+static void consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -59,6 +66,14 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
 						 List *merge_pathkeys);
+static void generate_partial_mergejoin_paths(PlannerInfo *root,
+								 RelOptInfo *joinrel,
+								 RelOptInfo *innerrel,
+								 Path *outerpath,
+								 JoinType jointype,
+								 JoinPathExtraData *extra,
+								 Path *inner_cheapest_total,
+								 List *merge_pathkeys);
 
 
 /*
@@ -481,6 +496,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a partial merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_partial_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+						   RelOptInfo *joinrel,
+						   Path *outer_path,
+						   Path *inner_path,
+						   List *pathkeys,
+						   List *mergeclauses,
+						   List *outersortkeys,
+						   List *innersortkeys,
+						   JoinType jointype,
+						   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_hashjoin_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_empty(inner_paramrels))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+					 create_mergejoin_path(root,
+										   joinrel,
+										   jointype,
+										   &workspace,
+										   extra->sjinfo,
+										   outer_path,
+										   inner_path,
+										   extra->restrictlist,
+										   pathkeys,
+										   NULL,
+										   mergeclauses,
+										   outersortkeys,
+										   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -649,6 +734,7 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
 	List	   *all_pathkeys;
@@ -782,6 +868,37 @@ sort_inner_and_outer(PlannerInfo *root,
 						   innerkeys,
 						   jointype,
 						   extra);
+
+		/*
+		 * If the joinrel is parallel-safe, we may be able to consider a
+		 * partial merge join.  However, we can't handle JOIN_UNIQUE_OUTER,
+		 * because the outer path will be partial, and therefore we won't be
+		 * able to properly guarantee uniqueness.  Similarly, we can't handle
+		 * JOIN_FULL and JOIN_RIGHT, because they can produce false null
+		 * extended rows.  Also, the resulting path must not be parameterized.
+		 */
+		if (joinrel->consider_parallel &&
+			save_jointype != JOIN_UNIQUE_OUTER &&
+			save_jointype != JOIN_FULL &&
+			save_jointype != JOIN_RIGHT &&
+			outerrel->partial_pathlist != NIL &&
+			bms_is_empty(joinrel->lateral_relids) &&
+			inner_path->parallel_safe)
+		{
+			Path	   *cheapest_partial_outer = (Path *) linitial(
+												 outerrel->partial_pathlist);
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   cheapest_partial_outer,
+									   inner_path,
+									   merge_pathkeys,
+									   cur_mergeclauses,
+									   outerkeys,
+									   innerkeys,
+									   jointype,
+									   extra);
+		}
 	}
 }
 
@@ -1021,6 +1138,196 @@ generate_mergejoin_paths(PlannerInfo *root,
 }
 
 /*
+ * generate_partial_mergejoin_paths
+ *
+ *	As above, but it will generate partial paths.  And, also in this function
+ *	we don't handle JOIN_UNIQUE_OUTER, JOIN_FULL or JOIN_RIGHT join types
+ *	because we don't support these join types for partial paths.
+ */
+static void
+generate_partial_mergejoin_paths(PlannerInfo *root,
+								 RelOptInfo *joinrel,
+								 RelOptInfo *innerrel,
+								 Path *outerpath,
+								 JoinType jointype,
+								 JoinPathExtraData *extra,
+								 Path *inner_cheapest_total,
+								 List *merge_pathkeys)
+{
+	List	   *mergeclauses;
+	List	   *innersortkeys;
+	List	   *trialsortkeys;
+	Path	   *cheapest_startup_inner;
+	Path	   *cheapest_total_inner;
+	JoinType	save_jointype = jointype;
+	int			num_sortkeys;
+	int			sortkeycnt;
+
+	/*
+	 * We should never come here for JOIN_UNIQUE_OUTER, JOIN_FULL or
+	 * JOIN_RIGHT.
+	 */
+	Assert(jointype != JOIN_UNIQUE_OUTER);
+	Assert(jointype != JOIN_FULL);
+	Assert(jointype != JOIN_RIGHT);
+
+	if (jointype == JOIN_UNIQUE_INNER)
+		jointype = JOIN_INNER;
+
+	/* Look for useful mergeclauses (if any) */
+	mergeclauses = find_mergeclauses_for_pathkeys(root,
+												  outerpath->pathkeys,
+												  true,
+												  extra->mergeclause_list);
+
+	/*
+	 * Done with this outer path if no chance for a mergejoin.
+	 */
+	if (mergeclauses == NIL)
+		return;
+
+	/* Compute the required ordering of the inner path */
+	innersortkeys = make_inner_pathkeys_for_merge(root,
+												  mergeclauses,
+												  outerpath->pathkeys);
+
+	/* Generate partial path if inner is parallel safe. */
+	if (inner_cheapest_total->parallel_safe)
+		try_partial_mergejoin_path(root,
+								   joinrel,
+								   outerpath,
+								   inner_cheapest_total,
+								   merge_pathkeys,
+								   mergeclauses,
+								   NIL,
+								   innersortkeys,
+								   jointype,
+								   extra);
+
+	/* Can't do anything else if inner path needs to be unique'd */
+	if (save_jointype == JOIN_UNIQUE_INNER)
+		return;
+
+	/*
+	 * See comments in generate_mergejoin_paths
+	 */
+	if (pathkeys_contained_in(innersortkeys,
+							  inner_cheapest_total->pathkeys))
+	{
+		/* inner_cheapest_total didn't require a sort */
+		cheapest_startup_inner = inner_cheapest_total;
+		cheapest_total_inner = inner_cheapest_total;
+	}
+	else
+	{
+		/* it did require a sort, at least for the full set of keys */
+		cheapest_startup_inner = NULL;
+		cheapest_total_inner = NULL;
+	}
+	num_sortkeys = list_length(innersortkeys);
+	if (num_sortkeys > 1)
+		trialsortkeys = list_copy(innersortkeys);		/* need modifiable copy */
+	else
+		trialsortkeys = innersortkeys;	/* won't really truncate */
+
+	for (sortkeycnt = num_sortkeys; sortkeycnt > 0; sortkeycnt--)
+	{
+		Path	   *innerpath;
+		List	   *newclauses = NIL;
+
+		/*
+		 * Look for an inner path ordered well enough for the first
+		 * 'sortkeycnt' innersortkeys.  NB: trialsortkeys list is modified
+		 * destructively, which is why we made a copy...
+		 */
+		trialsortkeys = list_truncate(trialsortkeys, sortkeycnt);
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   TOTAL_COST);
+		/* Consider only parallel safe inner path */
+		if (innerpath != NULL &&
+			innerpath->parallel_safe &&
+			(cheapest_total_inner == NULL ||
+			 compare_path_costs(innerpath, cheapest_total_inner,
+								TOTAL_COST) < 0))
+
+		{
+			/* Found a cheap (or even-cheaper) sorted parallel safe path */
+			/* Select the right mergeclauses, if we didn't already */
+			if (sortkeycnt < num_sortkeys)
+			{
+				newclauses = find_mergeclauses_for_pathkeys(root,
+															trialsortkeys,
+															false,
+															mergeclauses);
+				Assert(newclauses != NIL);
+			}
+			else
+				newclauses = mergeclauses;
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   outerpath,
+									   innerpath,
+									   merge_pathkeys,
+									   newclauses,
+									   NIL,
+									   NIL,
+									   jointype,
+									   extra);
+
+			cheapest_total_inner = innerpath;
+		}
+		/* Same on the basis of cheapest startup cost ... */
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   STARTUP_COST);
+		if (innerpath != NULL &&
+			innerpath->parallel_safe &&
+			(cheapest_startup_inner == NULL ||
+			 compare_path_costs(innerpath, cheapest_startup_inner,
+								STARTUP_COST) < 0))
+		{
+			/* Found a cheap (or even-cheaper) sorted parallel safe path */
+			if (innerpath != cheapest_total_inner)
+			{
+				/*
+				 * Avoid rebuilding clause list if we already made one; saves
+				 * memory in big join trees...
+				 */
+				if (newclauses == NIL)
+				{
+					if (sortkeycnt < num_sortkeys)
+					{
+						newclauses =
+							find_mergeclauses_for_pathkeys(root,
+														   trialsortkeys,
+														   false,
+														   mergeclauses);
+						Assert(newclauses != NIL);
+					}
+					else
+						newclauses = mergeclauses;
+				}
+				try_partial_mergejoin_path(root,
+										   joinrel,
+										   outerpath,
+										   innerpath,
+										   merge_pathkeys,
+										   newclauses,
+										   NIL,
+										   NIL,
+										   jointype,
+										   extra);
+			}
+			cheapest_startup_inner = innerpath;
+		}
+	}
+}
+
+/*
  * match_unsorted_outer
  *	  Creates possible join paths for processing a single join relation
  *	  'joinrel' by employing either iterative substitution or
@@ -1223,18 +1530,73 @@ match_unsorted_outer(PlannerInfo *root,
 	}
 
 	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
+	 * Consider partial nestloop and mergejoin plan if outerrel has any
+	 * partial path and the joinrel is parallel-safe.  However, we can't
+	 * handle JOIN_UNIQUE_OUTER, because the outer path will be partial, and
+	 * therefore we won't be able to properly guarantee uniqueness.  Nor can
+	 * we handle extra_lateral_rels, since partial paths must not be
+	 * parameterized. Similarly, we can't handle JOIN_FULL and JOIN_RIGHT,
+	 * because they can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
-		save_jointype != JOIN_UNIQUE_OUTER &&
-		bms_is_empty(joinrel->lateral_relids))
+	if (!(joinrel->consider_parallel &&
+		  save_jointype != JOIN_UNIQUE_OUTER &&
+		  save_jointype != JOIN_FULL &&
+		  save_jointype != JOIN_RIGHT &&
+		  outerrel->partial_pathlist != NIL &&
+		  bms_is_empty(joinrel->lateral_relids)))
+		return;
+
+	if (nestjoinOK)
 		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
 								   save_jointype, extra);
+
+	/* Can't generate mergejoin path if inner rel is parameterized by outer */
+	if (inner_cheapest_total != NULL)
+		consider_parallel_mergejoin(root, joinrel, outerrel, innerrel,
+									save_jointype, extra,
+									inner_cheapest_total);
+}
+
+/*
+ * consider_parallel_mergejoin
+ *	  Try to build partial paths for a joinrel by joining a partial path
+ *	  for the outer relation to a complete path for the inner relation.
+ *
+ * 'joinrel' is the join relation
+ * 'outerrel' is the outer join relation
+ * 'innerrel' is the inner join relation
+ * 'jointype' is the type of join to do
+ * 'extra' contains additional input values
+ * 'inner_cheapest_total' cheapest total path for innerrel.
+ */
+static void
+consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total)
+{
+	ListCell   *lc1;
+
+	/* generate merge join path for each partial outer path */
+	foreach(lc1, outerrel->partial_pathlist)
+	{
+		Path	   *outerpath = (Path *) lfirst(lc1);
+		List	   *merge_pathkeys;
+
+		/*
+		 * Figure out what useful ordering any paths we create will have.
+		 */
+		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											 outerpath->pathkeys);
+
+		generate_partial_mergejoin_paths(root, joinrel, innerrel,
+										 outerpath, jointype, extra,
+										 inner_cheapest_total,
+										 merge_pathkeys);
+	}
 }
 
 /*
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index a5a2232..75558d0 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -169,6 +169,31 @@ select  count(*) from tenk1 where thousand > 95;
 
 reset enable_seqscan;
 reset enable_bitmapscan;
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ Finalize Aggregate
+   ->  Gather
+         Workers Planned: 4
+         ->  Partial Aggregate
+               ->  Merge Join
+                     Merge Cond: (tenk1.unique1 = tenk2.unique1)
+                     ->  Parallel Index Only Scan using tenk1_unique1 on tenk1
+                     ->  Index Only Scan using tenk2_unique1 on tenk2
+(8 rows)
+
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+ count 
+-------
+ 10000
+(1 row)
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 explain (costs off)
   select stringu1::int2 from tenk1 where unique1 = 1;
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index d72addf..ebdae7e 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -64,6 +64,16 @@ select  count(*) from tenk1 where thousand > 95;
 reset enable_seqscan;
 reset enable_bitmapscan;
 
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 
 explain (costs off)
#23Amit Kapila
amit.kapila16@gmail.com
In reply to: Dilip Kumar (#22)
Re: Proposal : Parallel Merge Join

On Sat, Feb 25, 2017 at 3:22 PM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Sat, Feb 25, 2017 at 11:29 AM, Amit Kapila <amit.kapila16@gmail.com> wrote:

It seems you have forgotten to change in the below check:

+ if (innerpath != NULL &&
+ innerpath->parallel_safe &&
+ (cheapest_startup_inner == NULL ||
+ cheapest_startup_inner->parallel_safe == false ||
+ compare_path_costs(innerpath, cheapest_startup_inner,
+ STARTUP_COST) < 0))

Oops, Fixed.

Thanks, patch looks good to me (apart from some of the code comments
which might need some adjustment and I think it is better if Committer
takes care of same as required rather than we argue about it) and I
have changed the status as Ready For Committer.

--
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#24Robert Haas
robertmhaas@gmail.com
In reply to: Amit Kapila (#19)
Re: Proposal : Parallel Merge Join

On Fri, Feb 24, 2017 at 3:54 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

I agree in some cases it could be better, but I think benefits are not
completely clear, so probably we can leave it as of now and if later
any one comes with a clear use case or can see the benefits of such
path then we can include it.

TBH, I think Dilip had the right idea here. cheapest_total_inner
isn't anything magical; it's just that there's no reason to use
anything but the cheapest path for a relation when forming a join path
unless that first path lacks some property that you need, like having
the right sortkeys or being parallel-safe. Since there are lots of
join paths that just want to do things in the cheapest way possible,
we identify the cheapest path and hang on to it; when that's not what
we need, we go find the path or paths that have the properties we
want. I'm not sure why this case should be an exception. You could
argue that if the cheapest parallel-safe path is already more
expensive then the parallel join may not pay off, but that's hard to
say; it depends on what happens higher up in the plan tree. That's
why the current code handles partial hash joins this way:

/*
* Normally, given that the joinrel is parallel-safe, the cheapest
* total inner path will also be parallel-safe, but if not, we'll
* have to search cheapest_parameterized_paths for the cheapest
* safe, unparameterized inner path. If doing JOIN_UNIQUE_INNER,
* we can't use any alternative inner path.
*/
if (cheapest_total_inner->parallel_safe)
cheapest_safe_inner = cheapest_total_inner;
else if (save_jointype != JOIN_UNIQUE_INNER)
{
ListCell *lc;

foreach(lc, innerrel->cheapest_parameterized_paths)
{
Path *innerpath = (Path *) lfirst(lc);

if (innerpath->parallel_safe &&
bms_is_empty(PATH_REQ_OUTER(innerpath)))
{
cheapest_safe_inner = innerpath;
break;
}
}
}

I would tend to think this case ought to be handled in a similar way.
And if not, then we ought to go change the hash join logic too so that
they match.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#25Amit Kapila
amit.kapila16@gmail.com
In reply to: Robert Haas (#24)
Re: Proposal : Parallel Merge Join

On Sun, Feb 26, 2017 at 12:01 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Fri, Feb 24, 2017 at 3:54 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

I agree in some cases it could be better, but I think benefits are not
completely clear, so probably we can leave it as of now and if later
any one comes with a clear use case or can see the benefits of such
path then we can include it.

TBH, I think Dilip had the right idea here. cheapest_total_inner
isn't anything magical; it's just that there's no reason to use
anything but the cheapest path for a relation when forming a join path
unless that first path lacks some property that you need, like having
the right sortkeys or being parallel-safe. Since there are lots of
join paths that just want to do things in the cheapest way possible,
we identify the cheapest path and hang on to it; when that's not what
we need, we go find the path or paths that have the properties we
want. I'm not sure why this case should be an exception. You could
argue that if the cheapest parallel-safe path is already more
expensive then the parallel join may not pay off, but that's hard to
say; it depends on what happens higher up in the plan tree.

Okay, but in that case don't you think it is better to consider the
parallel safety of cheapest_total_inner only when we don't find any
cheap parallel_safe innerpath by reducing the sort keys?

--
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#26Dilip Kumar
dilipbalaut@gmail.com
In reply to: Amit Kapila (#25)
Re: Proposal : Parallel Merge Join

On Mon, Feb 27, 2017 at 10:27 AM, Amit Kapila <amit.kapila16@gmail.com> wrote:

Okay, but in that case don't you think it is better to consider the
parallel safety of cheapest_total_inner only when we don't find any
cheap parallel_safe innerpath by reducing the sort keys?

Well, we can do that but suppose cheapest_total_inner is not parallel
safe and we do not get any parallel safe path which is cheaper than
cheapest_total_inner, then we just end up making the merge join path
with the cheapest parallel safe path but we might have missed some of
the paths whose pathkey is covering more ordered keys. Still, it's
hard to argue what it better because we can always say that if we try
only cheapest parallel safe path we will generate fewer paths.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#27Amit Kapila
amit.kapila16@gmail.com
In reply to: Dilip Kumar (#26)
Re: Proposal : Parallel Merge Join

On Tue, Feb 28, 2017 at 9:28 PM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Mon, Feb 27, 2017 at 10:27 AM, Amit Kapila <amit.kapila16@gmail.com> wrote:

Okay, but in that case don't you think it is better to consider the
parallel safety of cheapest_total_inner only when we don't find any
cheap parallel_safe innerpath by reducing the sort keys?

Well, we can do that but suppose cheapest_total_inner is not parallel
safe and we do not get any parallel safe path which is cheaper than
cheapest_total_inner, then we just end up making the merge join path
with the cheapest parallel safe path but we might have missed some of
the paths whose pathkey is covering more ordered keys. Still, it's
hard to argue what it better because we can always say that if we try
only cheapest parallel safe path we will generate fewer paths.

I think for now we can keep the parallel safety check for cheapest
inner path, though it will be of use only for the very first time we
compare the paths in that loop. I am not sure if there is any other
better way to handle the same.

--
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#28Dilip Kumar
dilipbalaut@gmail.com
In reply to: Amit Kapila (#27)
1 attachment(s)
Re: Proposal : Parallel Merge Join

On Wed, Mar 1, 2017 at 11:13 AM, Amit Kapila <amit.kapila16@gmail.com> wrote:

I think for now we can keep the parallel safety check for cheapest
inner path, though it will be of use only for the very first time we
compare the paths in that loop. I am not sure if there is any other
better way to handle the same.

Done that way.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

parallel_mergejoin_v8.patchapplication/octet-stream; name=parallel_mergejoin_v8.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 2897245..d160814 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -40,6 +40,13 @@ static void consider_parallel_nestloop(PlannerInfo *root,
 						   RelOptInfo *innerrel,
 						   JoinType jointype,
 						   JoinPathExtraData *extra);
+static void consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -59,6 +66,14 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
 						 List *merge_pathkeys);
+static void generate_partial_mergejoin_paths(PlannerInfo *root,
+								 RelOptInfo *joinrel,
+								 RelOptInfo *innerrel,
+								 Path *outerpath,
+								 JoinType jointype,
+								 JoinPathExtraData *extra,
+								 Path *inner_cheapest_total,
+								 List *merge_pathkeys);
 
 
 /*
@@ -481,6 +496,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a partial merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_partial_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+						   RelOptInfo *joinrel,
+						   Path *outer_path,
+						   Path *inner_path,
+						   List *pathkeys,
+						   List *mergeclauses,
+						   List *outersortkeys,
+						   List *innersortkeys,
+						   JoinType jointype,
+						   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_hashjoin_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_empty(inner_paramrels))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+					 create_mergejoin_path(root,
+										   joinrel,
+										   jointype,
+										   &workspace,
+										   extra->sjinfo,
+										   outer_path,
+										   inner_path,
+										   extra->restrictlist,
+										   pathkeys,
+										   NULL,
+										   mergeclauses,
+										   outersortkeys,
+										   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -649,6 +734,7 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
 	List	   *all_pathkeys;
@@ -782,6 +868,37 @@ sort_inner_and_outer(PlannerInfo *root,
 						   innerkeys,
 						   jointype,
 						   extra);
+
+		/*
+		 * If the joinrel is parallel-safe, we may be able to consider a
+		 * partial merge join.  However, we can't handle JOIN_UNIQUE_OUTER,
+		 * because the outer path will be partial, and therefore we won't be
+		 * able to properly guarantee uniqueness.  Similarly, we can't handle
+		 * JOIN_FULL and JOIN_RIGHT, because they can produce false null
+		 * extended rows.  Also, the resulting path must not be parameterized.
+		 */
+		if (joinrel->consider_parallel &&
+			save_jointype != JOIN_UNIQUE_OUTER &&
+			save_jointype != JOIN_FULL &&
+			save_jointype != JOIN_RIGHT &&
+			outerrel->partial_pathlist != NIL &&
+			bms_is_empty(joinrel->lateral_relids) &&
+			inner_path->parallel_safe)
+		{
+			Path	   *cheapest_partial_outer = (Path *) linitial(
+												 outerrel->partial_pathlist);
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   cheapest_partial_outer,
+									   inner_path,
+									   merge_pathkeys,
+									   cur_mergeclauses,
+									   outerkeys,
+									   innerkeys,
+									   jointype,
+									   extra);
+		}
 	}
 }
 
@@ -1021,6 +1138,198 @@ generate_mergejoin_paths(PlannerInfo *root,
 }
 
 /*
+ * generate_partial_mergejoin_paths
+ *
+ *	As above, but it will generate partial paths.  And, also in this function
+ *	we don't handle JOIN_UNIQUE_OUTER, JOIN_FULL or JOIN_RIGHT join types
+ *	because we don't support these join types for partial paths.
+ */
+static void
+generate_partial_mergejoin_paths(PlannerInfo *root,
+								 RelOptInfo *joinrel,
+								 RelOptInfo *innerrel,
+								 Path *outerpath,
+								 JoinType jointype,
+								 JoinPathExtraData *extra,
+								 Path *inner_cheapest_total,
+								 List *merge_pathkeys)
+{
+	List	   *mergeclauses;
+	List	   *innersortkeys;
+	List	   *trialsortkeys;
+	Path	   *cheapest_startup_inner;
+	Path	   *cheapest_total_inner;
+	JoinType	save_jointype = jointype;
+	int			num_sortkeys;
+	int			sortkeycnt;
+
+	/*
+	 * We should never come here for JOIN_UNIQUE_OUTER, JOIN_FULL or
+	 * JOIN_RIGHT.
+	 */
+	Assert(jointype != JOIN_UNIQUE_OUTER);
+	Assert(jointype != JOIN_FULL);
+	Assert(jointype != JOIN_RIGHT);
+
+	if (jointype == JOIN_UNIQUE_INNER)
+		jointype = JOIN_INNER;
+
+	/* Look for useful mergeclauses (if any) */
+	mergeclauses = find_mergeclauses_for_pathkeys(root,
+												  outerpath->pathkeys,
+												  true,
+												  extra->mergeclause_list);
+
+	/*
+	 * Done with this outer path if no chance for a mergejoin.
+	 */
+	if (mergeclauses == NIL)
+		return;
+
+	/* Compute the required ordering of the inner path */
+	innersortkeys = make_inner_pathkeys_for_merge(root,
+												  mergeclauses,
+												  outerpath->pathkeys);
+
+	/* Generate partial path if inner is parallel safe. */
+	if (inner_cheapest_total->parallel_safe)
+		try_partial_mergejoin_path(root,
+								   joinrel,
+								   outerpath,
+								   inner_cheapest_total,
+								   merge_pathkeys,
+								   mergeclauses,
+								   NIL,
+								   innersortkeys,
+								   jointype,
+								   extra);
+
+	/* Can't do anything else if inner path needs to be unique'd */
+	if (save_jointype == JOIN_UNIQUE_INNER)
+		return;
+
+	/*
+	 * See comments in generate_mergejoin_paths
+	 */
+	if (pathkeys_contained_in(innersortkeys,
+							  inner_cheapest_total->pathkeys))
+	{
+		/* inner_cheapest_total didn't require a sort */
+		cheapest_startup_inner = inner_cheapest_total;
+		cheapest_total_inner = inner_cheapest_total;
+	}
+	else
+	{
+		/* it did require a sort, at least for the full set of keys */
+		cheapest_startup_inner = NULL;
+		cheapest_total_inner = NULL;
+	}
+	num_sortkeys = list_length(innersortkeys);
+	if (num_sortkeys > 1)
+		trialsortkeys = list_copy(innersortkeys);		/* need modifiable copy */
+	else
+		trialsortkeys = innersortkeys;	/* won't really truncate */
+
+	for (sortkeycnt = num_sortkeys; sortkeycnt > 0; sortkeycnt--)
+	{
+		Path	   *innerpath;
+		List	   *newclauses = NIL;
+
+		/*
+		 * Look for an inner path ordered well enough for the first
+		 * 'sortkeycnt' innersortkeys.  NB: trialsortkeys list is modified
+		 * destructively, which is why we made a copy...
+		 */
+		trialsortkeys = list_truncate(trialsortkeys, sortkeycnt);
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   TOTAL_COST);
+		/* Consider only parallel safe inner path */
+		if (innerpath != NULL &&
+			innerpath->parallel_safe &&
+			(cheapest_total_inner == NULL ||
+			 cheapest_total_inner->parallel_safe == false ||
+			 compare_path_costs(innerpath, cheapest_total_inner,
+								TOTAL_COST) < 0))
+
+		{
+			/* Found a cheap (or even-cheaper) sorted parallel safe path */
+			/* Select the right mergeclauses, if we didn't already */
+			if (sortkeycnt < num_sortkeys)
+			{
+				newclauses = find_mergeclauses_for_pathkeys(root,
+															trialsortkeys,
+															false,
+															mergeclauses);
+				Assert(newclauses != NIL);
+			}
+			else
+				newclauses = mergeclauses;
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   outerpath,
+									   innerpath,
+									   merge_pathkeys,
+									   newclauses,
+									   NIL,
+									   NIL,
+									   jointype,
+									   extra);
+
+			cheapest_total_inner = innerpath;
+		}
+		/* Same on the basis of cheapest startup cost ... */
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   STARTUP_COST);
+		if (innerpath != NULL &&
+			innerpath->parallel_safe &&
+			(cheapest_startup_inner == NULL ||
+			 cheapest_startup_inner->parallel_safe == false ||
+			 compare_path_costs(innerpath, cheapest_startup_inner,
+								STARTUP_COST) < 0))
+		{
+			/* Found a cheap (or even-cheaper) sorted parallel safe path */
+			if (innerpath != cheapest_total_inner)
+			{
+				/*
+				 * Avoid rebuilding clause list if we already made one; saves
+				 * memory in big join trees...
+				 */
+				if (newclauses == NIL)
+				{
+					if (sortkeycnt < num_sortkeys)
+					{
+						newclauses =
+							find_mergeclauses_for_pathkeys(root,
+														   trialsortkeys,
+														   false,
+														   mergeclauses);
+						Assert(newclauses != NIL);
+					}
+					else
+						newclauses = mergeclauses;
+				}
+				try_partial_mergejoin_path(root,
+										   joinrel,
+										   outerpath,
+										   innerpath,
+										   merge_pathkeys,
+										   newclauses,
+										   NIL,
+										   NIL,
+										   jointype,
+										   extra);
+			}
+			cheapest_startup_inner = innerpath;
+		}
+	}
+}
+
+/*
  * match_unsorted_outer
  *	  Creates possible join paths for processing a single join relation
  *	  'joinrel' by employing either iterative substitution or
@@ -1223,18 +1532,73 @@ match_unsorted_outer(PlannerInfo *root,
 	}
 
 	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
+	 * Consider partial nestloop and mergejoin plan if outerrel has any
+	 * partial path and the joinrel is parallel-safe.  However, we can't
+	 * handle JOIN_UNIQUE_OUTER, because the outer path will be partial, and
+	 * therefore we won't be able to properly guarantee uniqueness.  Nor can
+	 * we handle extra_lateral_rels, since partial paths must not be
+	 * parameterized. Similarly, we can't handle JOIN_FULL and JOIN_RIGHT,
+	 * because they can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
-		save_jointype != JOIN_UNIQUE_OUTER &&
-		bms_is_empty(joinrel->lateral_relids))
+	if (!(joinrel->consider_parallel &&
+		  save_jointype != JOIN_UNIQUE_OUTER &&
+		  save_jointype != JOIN_FULL &&
+		  save_jointype != JOIN_RIGHT &&
+		  outerrel->partial_pathlist != NIL &&
+		  bms_is_empty(joinrel->lateral_relids)))
+		return;
+
+	if (nestjoinOK)
 		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
 								   save_jointype, extra);
+
+	/* Can't generate mergejoin path if inner rel is parameterized by outer */
+	if (inner_cheapest_total != NULL)
+		consider_parallel_mergejoin(root, joinrel, outerrel, innerrel,
+									save_jointype, extra,
+									inner_cheapest_total);
+}
+
+/*
+ * consider_parallel_mergejoin
+ *	  Try to build partial paths for a joinrel by joining a partial path
+ *	  for the outer relation to a complete path for the inner relation.
+ *
+ * 'joinrel' is the join relation
+ * 'outerrel' is the outer join relation
+ * 'innerrel' is the inner join relation
+ * 'jointype' is the type of join to do
+ * 'extra' contains additional input values
+ * 'inner_cheapest_total' cheapest total path for innerrel.
+ */
+static void
+consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total)
+{
+	ListCell   *lc1;
+
+	/* generate merge join path for each partial outer path */
+	foreach(lc1, outerrel->partial_pathlist)
+	{
+		Path	   *outerpath = (Path *) lfirst(lc1);
+		List	   *merge_pathkeys;
+
+		/*
+		 * Figure out what useful ordering any paths we create will have.
+		 */
+		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											 outerpath->pathkeys);
+
+		generate_partial_mergejoin_paths(root, joinrel, innerrel,
+										 outerpath, jointype, extra,
+										 inner_cheapest_total,
+										 merge_pathkeys);
+	}
 }
 
 /*
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index a5a2232..75558d0 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -169,6 +169,31 @@ select  count(*) from tenk1 where thousand > 95;
 
 reset enable_seqscan;
 reset enable_bitmapscan;
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ Finalize Aggregate
+   ->  Gather
+         Workers Planned: 4
+         ->  Partial Aggregate
+               ->  Merge Join
+                     Merge Cond: (tenk1.unique1 = tenk2.unique1)
+                     ->  Parallel Index Only Scan using tenk1_unique1 on tenk1
+                     ->  Index Only Scan using tenk2_unique1 on tenk2
+(8 rows)
+
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+ count 
+-------
+ 10000
+(1 row)
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 explain (costs off)
   select stringu1::int2 from tenk1 where unique1 = 1;
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index d72addf..ebdae7e 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -64,6 +64,16 @@ select  count(*) from tenk1 where thousand > 95;
 reset enable_seqscan;
 reset enable_bitmapscan;
 
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 
 explain (costs off)
#29Amit Kapila
amit.kapila16@gmail.com
In reply to: Dilip Kumar (#28)
Re: Proposal : Parallel Merge Join

On Wed, Mar 1, 2017 at 11:24 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Wed, Mar 1, 2017 at 11:13 AM, Amit Kapila <amit.kapila16@gmail.com> wrote:

I think for now we can keep the parallel safety check for cheapest
inner path, though it will be of use only for the very first time we
compare the paths in that loop. I am not sure if there is any other
better way to handle the same.

Done that way.

Thanks, your patch looks good to me.

--
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#30Robert Haas
robertmhaas@gmail.com
In reply to: Amit Kapila (#29)
1 attachment(s)
Re: Proposal : Parallel Merge Join

On Wed, Mar 1, 2017 at 12:01 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:

On Wed, Mar 1, 2017 at 11:24 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Wed, Mar 1, 2017 at 11:13 AM, Amit Kapila <amit.kapila16@gmail.com> wrote:

I think for now we can keep the parallel safety check for cheapest
inner path, though it will be of use only for the very first time we
compare the paths in that loop. I am not sure if there is any other
better way to handle the same.

Done that way.

Thanks, your patch looks good to me.

I'm not happy with the way this patch can just happen to latch on to a
path that's not parallel-safe rather than one that is and then just
give up on a merge join in that case. I already made this argument in
/messages/by-id/CA+TgmobdW2au1Jq5L4ySA2ZhqFmA-qNvD7ZFaZbJWm3c0ysWyw@mail.gmail.com
and my opinion hasn't changed. I've subsequently come to realize that
this problem is more widespread that I initially realized: not only do
sort_inner_and_outer() and generate_partial_mergejoin_paths() give up
without searching for the cheapest parallel-safe path, but the latter
later calls get_cheapest_path_for_pathkeys() and then just pretends it
didn't find anything unless the result is parallel-safe. This makes
no sense to me: the cheapest parallel-safe path could be 1% more
expensive than the cheapest path of any kind, but because it's not the
cheapest we arbitrarily skip it, even though parallelizing more of the
tree might leave us way ahead overall.

I suggest that we add an additional "bool require_parallel_safe"
argument to get_cheapest_path_for_pathkeys(); when false, the function
will behave as at present, but when true, it will skip paths that are
not parallel-safe. And similarly have a function to find the cheapest
parallel-safe path if the first one in the list isn't it. See
attached draft patch. Then this code can search for the correct path
instead of searching using the wrong criteria and then giving up if it
doesn't get the result it wants.

+    if (!(joinrel->consider_parallel &&
+          save_jointype != JOIN_UNIQUE_OUTER &&
+          save_jointype != JOIN_FULL &&
+          save_jointype != JOIN_RIGHT &&
+          outerrel->partial_pathlist != NIL &&
+          bms_is_empty(joinrel->lateral_relids)))

I would distribute the negation, so that this reads if
(!joinrel->consider_parallel || save_jointype == JOIN_UNIQUE_OUTER ||
save_jointype == JOIN_FULL || ...). Or maybe better yet, just drop
the ! and the return that follows and put the
consider_parallel_nestloop and consider_parallel_mergejoin calls
inside the if-block.

You could Assert(nestjoinOK) instead of testing it, although I guess
it doesn't really matter.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

path-search-for-mergejoin-v1.patchapplication/octet-stream; name=path-search-for-mergejoin-v1.patchDownload
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 633b5c1..87a3faf 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -1447,12 +1447,14 @@ generate_mergeappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				get_cheapest_path_for_pathkeys(childrel->pathlist,
 											   pathkeys,
 											   NULL,
-											   STARTUP_COST);
+											   STARTUP_COST,
+											   false);
 			cheapest_total =
 				get_cheapest_path_for_pathkeys(childrel->pathlist,
 											   pathkeys,
 											   NULL,
-											   TOTAL_COST);
+											   TOTAL_COST,
+											   false);
 
 			/*
 			 * If we can't find any paths with the right order just use the
@@ -1517,7 +1519,8 @@ get_cheapest_parameterized_child_path(PlannerInfo *root, RelOptInfo *rel,
 	cheapest = get_cheapest_path_for_pathkeys(rel->pathlist,
 											  NIL,
 											  required_outer,
-											  TOTAL_COST);
+											  TOTAL_COST,
+											  false);
 	Assert(cheapest != NULL);
 	if (bms_equal(PATH_REQ_OUTER(cheapest), required_outer))
 		return cheapest;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 2897245..3f6f233 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -936,7 +936,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
 												   trialsortkeys,
 												   NULL,
-												   TOTAL_COST);
+												   TOTAL_COST,
+												   false);
 		if (innerpath != NULL &&
 			(cheapest_total_inner == NULL ||
 			 compare_path_costs(innerpath, cheapest_total_inner,
@@ -971,7 +972,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
 												   trialsortkeys,
 												   NULL,
-												   STARTUP_COST);
+												   STARTUP_COST,
+												   false);
 		if (innerpath != NULL &&
 			(cheapest_startup_inner == NULL ||
 			 compare_path_costs(innerpath, cheapest_startup_inner,
@@ -1518,19 +1520,11 @@ hash_inner_and_outer(PlannerInfo *root,
 				cheapest_safe_inner = cheapest_total_inner;
 			else if (save_jointype != JOIN_UNIQUE_INNER)
 			{
-				ListCell   *lc;
+				List	   *paths;
 
-				foreach(lc, innerrel->cheapest_parameterized_paths)
-				{
-					Path	   *innerpath = (Path *) lfirst(lc);
-
-					if (innerpath->parallel_safe &&
-						bms_is_empty(PATH_REQ_OUTER(innerpath)))
-					{
-						cheapest_safe_inner = innerpath;
-						break;
-					}
-				}
+				paths = innerrel->cheapest_parameterized_paths;
+				cheapest_safe_inner =
+					get_cheapest_parallel_safe_total_inner(paths);
 			}
 
 			if (cheapest_safe_inner != NULL)
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index 1065b31..846929b 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -341,7 +341,8 @@ pathkeys_contained_in(List *keys1, List *keys2)
 Path *
 get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
 							   Relids required_outer,
-							   CostSelector cost_criterion)
+							   CostSelector cost_criterion,
+							   bool require_parallel_safe)
 {
 	Path	   *matched_path = NULL;
 	ListCell   *l;
@@ -358,6 +359,9 @@ get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
 			compare_path_costs(matched_path, path, cost_criterion) <= 0)
 			continue;
 
+		if (require_parallel_safe && !path->parallel_safe)
+			continue;
+
 		if (pathkeys_contained_in(pathkeys, path->pathkeys) &&
 			bms_is_subset(PATH_REQ_OUTER(path), required_outer))
 			matched_path = path;
@@ -407,6 +411,28 @@ get_cheapest_fractional_path_for_pathkeys(List *paths,
 	return matched_path;
 }
 
+
+/*
+ * get_cheapest_parallel_safe_total_inner
+ *	  Find the unparameterized parallel-safe path with the least total cost.
+ */
+Path *
+get_cheapest_parallel_safe_total_inner(List *paths)
+{
+	ListCell   *l;
+
+	foreach(l, paths)
+	{
+		Path   *innerpath = (Path *) lfirst(l);
+
+		if (innerpath->parallel_safe &&
+			bms_is_empty(PATH_REQ_OUTER(innerpath)))
+			return innerpath;
+	}
+
+	return NULL;
+}
+
 /****************************************************************************
  *		NEW PATHKEY FORMATION
  ****************************************************************************/
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index ebda308..bc0dcf4 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -182,11 +182,13 @@ extern PathKeysComparison compare_pathkeys(List *keys1, List *keys2);
 extern bool pathkeys_contained_in(List *keys1, List *keys2);
 extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
 							   Relids required_outer,
-							   CostSelector cost_criterion);
+							   CostSelector cost_criterion,
+							   bool require_parallel_safe);
 extern Path *get_cheapest_fractional_path_for_pathkeys(List *paths,
 										  List *pathkeys,
 										  Relids required_outer,
 										  double fraction);
+extern Path *get_cheapest_parallel_safe_total_inner(List *paths);
 extern List *build_index_pathkeys(PlannerInfo *root, IndexOptInfo *index,
 					 ScanDirection scandir);
 extern List *build_expression_pathkey(PlannerInfo *root, Expr *expr,
#31Dilip Kumar
dilipbalaut@gmail.com
In reply to: Robert Haas (#30)
Re: Proposal : Parallel Merge Join

On Fri, Mar 3, 2017 at 3:57 PM, Robert Haas <robertmhaas@gmail.com> wrote:

I'm not happy with the way this patch can just happen to latch on to a
path that's not parallel-safe rather than one that is and then just
give up on a merge join in that case. I already made this argument in
/messages/by-id/CA+TgmobdW2au1Jq5L4ySA2ZhqFmA-qNvD7ZFaZbJWm3c0ysWyw@mail.gmail.com
and my opinion hasn't changed.

I think last time I did not understand the depth of the problem
completely and only fixed from one aspect that in
generate_partial_mergejoin_paths if cheapest_total_inner or
cheapest_startup_inner is not parallel safe then consider the current
path if that are parallel safe and now I got it how it was completely
wrong.

I have one question for fixing it in sort_inner_and_outer, Currently,
we don't consider the parameterized paths for merge join except the
case when cheapest total paths itself is parameterized, So IIUC, for
creating partial path we will check if cheapest_total_inner path is
not parallel safe then we will find cheapest inner parallel safe path
using your new API get_cheapest_parallel_safe_total_inner, and we will
proceed with this paths if this is not directly parameterized by
outer?

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#32Robert Haas
robertmhaas@gmail.com
In reply to: Dilip Kumar (#31)
Re: Proposal : Parallel Merge Join

On Fri, Mar 3, 2017 at 9:47 PM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Fri, Mar 3, 2017 at 3:57 PM, Robert Haas <robertmhaas@gmail.com> wrote:

I'm not happy with the way this patch can just happen to latch on to a
path that's not parallel-safe rather than one that is and then just
give up on a merge join in that case. I already made this argument in
/messages/by-id/CA+TgmobdW2au1Jq5L4ySA2ZhqFmA-qNvD7ZFaZbJWm3c0ysWyw@mail.gmail.com
and my opinion hasn't changed.

I think last time I did not understand the depth of the problem
completely and only fixed from one aspect that in
generate_partial_mergejoin_paths if cheapest_total_inner or
cheapest_startup_inner is not parallel safe then consider the current
path if that are parallel safe and now I got it how it was completely
wrong.

I have one question for fixing it in sort_inner_and_outer, Currently,
we don't consider the parameterized paths for merge join except the
case when cheapest total paths itself is parameterized, So IIUC, for
creating partial path we will check if cheapest_total_inner path is
not parallel safe then we will find cheapest inner parallel safe path
using your new API get_cheapest_parallel_safe_total_inner, and we will
proceed with this paths if this is not directly parameterized by
outer?

Remember that partial paths can't be parameterized, and here we're
trying to build a partial mergejoin path. The outer input obviously
won't be parameterized, since it's partial, and it can't satisfy any
parameterization of the inner relation either, because only nested
loops can do that. So it only makes sense to try merge-joining a
partial path against a completely unparameterized, parallel-safe inner
path.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#33Dilip Kumar
dilipbalaut@gmail.com
In reply to: Robert Haas (#30)
2 attachment(s)
Re: Proposal : Parallel Merge Join

On Fri, Mar 3, 2017 at 3:57 PM, Robert Haas <robertmhaas@gmail.com> wrote:

I'm not happy with the way this patch can just happen to latch on to a
path that's not parallel-safe rather than one that is and then just
give up on a merge join in that case. I already made this argument in
/messages/by-id/CA+TgmobdW2au1Jq5L4ySA2ZhqFmA-qNvD7ZFaZbJWm3c0ysWyw@mail.gmail.com
and my opinion hasn't changed. I've subsequently come to realize that
this problem is more widespread that I initially realized: not only do
sort_inner_and_outer() and generate_partial_mergejoin_paths() give up
without searching for the cheapest parallel-safe path, but the latter
later calls get_cheapest_path_for_pathkeys() and then just pretends it
didn't find anything unless the result is parallel-safe. This makes
no sense to me: the cheapest parallel-safe path could be 1% more
expensive than the cheapest path of any kind, but because it's not the
cheapest we arbitrarily skip it, even though parallelizing more of the
tree might leave us way ahead overall.

for sort_inner_and_outer I have followed the same logic what
hash_inner_and_outer is doing. Also moved the logic of selecting the
cheapest_safe_inner out of the pathkey loop.

I suggest that we add an additional "bool require_parallel_safe"
argument to get_cheapest_path_for_pathkeys(); when false, the function
will behave as at present, but when true, it will skip paths that are
not parallel-safe. And similarly have a function to find the cheapest
parallel-safe path if the first one in the list isn't it. See
attached draft patch. Then this code can search for the correct path
instead of searching using the wrong criteria and then giving up if it
doesn't get the result it wants.

Done as suggested. Also, rebased the path_search_for_mergejoin on
head and updated the comments of get_cheapest_path_for_pathkeys for
new argument.

+    if (!(joinrel->consider_parallel &&
+          save_jointype != JOIN_UNIQUE_OUTER &&
+          save_jointype != JOIN_FULL &&
+          save_jointype != JOIN_RIGHT &&
+          outerrel->partial_pathlist != NIL &&
+          bms_is_empty(joinrel->lateral_relids)))

I would distribute the negation, so that this reads if
(!joinrel->consider_parallel || save_jointype == JOIN_UNIQUE_OUTER ||
save_jointype == JOIN_FULL || ...). Or maybe better yet, just drop
the ! and the return that follows and put the
consider_parallel_nestloop and consider_parallel_mergejoin calls
inside the if-block.

Done

You could Assert(nestjoinOK) instead of testing it, although I guess
it doesn't really matter.

left as it is.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

parallel_mergejoin_v9.patchapplication/octet-stream; name=parallel_mergejoin_v9.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 3f6f233..bb6b352 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -40,6 +40,13 @@ static void consider_parallel_nestloop(PlannerInfo *root,
 						   RelOptInfo *innerrel,
 						   JoinType jointype,
 						   JoinPathExtraData *extra);
+static void consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -59,6 +66,14 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
 						 List *merge_pathkeys);
+static void generate_partial_mergejoin_paths(PlannerInfo *root,
+								 RelOptInfo *joinrel,
+								 RelOptInfo *innerrel,
+								 Path *outerpath,
+								 JoinType jointype,
+								 JoinPathExtraData *extra,
+								 Path *inner_cheapest_total,
+								 List *merge_pathkeys);
 
 
 /*
@@ -481,6 +496,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a partial merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_partial_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+						   RelOptInfo *joinrel,
+						   Path *outer_path,
+						   Path *inner_path,
+						   List *pathkeys,
+						   List *mergeclauses,
+						   List *outersortkeys,
+						   List *innersortkeys,
+						   JoinType jointype,
+						   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_hashjoin_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_empty(inner_paramrels))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+					 create_mergejoin_path(root,
+										   joinrel,
+										   jointype,
+										   &workspace,
+										   extra->sjinfo,
+										   outer_path,
+										   inner_path,
+										   extra->restrictlist,
+										   pathkeys,
+										   NULL,
+										   mergeclauses,
+										   outersortkeys,
+										   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -649,8 +734,11 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
+	Path	   *cheapest_partial_outer;
+	Path	   *cheapest_safe_inner;
 	List	   *all_pathkeys;
 	ListCell   *l;
 
@@ -700,6 +788,35 @@ sort_inner_and_outer(PlannerInfo *root,
 	}
 
 	/*
+	 * If the joinrel is parallel-safe, we may be able to consider a partial
+	 * merge join.  However, we can't handle JOIN_UNIQUE_OUTER, because the
+	 * outer path will be partial, and therefore we won't be able to properly
+	 * guarantee uniqueness.  Similarly, we can't handle JOIN_FULL and
+	 * JOIN_RIGHT, because they can produce false null extended rows.  Also,
+	 * the resulting path must not be parameterized.
+	 */
+	if (joinrel->consider_parallel &&
+		save_jointype != JOIN_UNIQUE_OUTER &&
+		save_jointype != JOIN_FULL &&
+		save_jointype != JOIN_RIGHT &&
+		outerrel->partial_pathlist != NIL &&
+		bms_is_empty(joinrel->lateral_relids))
+	{
+		cheapest_partial_outer = (Path *) linitial(outerrel->partial_pathlist);
+
+		if (inner_path->parallel_safe)
+			cheapest_safe_inner = inner_path;
+		else if (save_jointype != JOIN_UNIQUE_INNER)
+		{
+			List	   *paths;
+
+			paths = innerrel->cheapest_parameterized_paths;
+			cheapest_safe_inner =
+				get_cheapest_parallel_safe_total_inner(paths);
+		}
+	}
+
+	/*
 	 * Each possible ordering of the available mergejoin clauses will generate
 	 * a differently-sorted result path at essentially the same cost.  We have
 	 * no basis for choosing one over another at this level of joining, but
@@ -782,6 +899,22 @@ sort_inner_and_outer(PlannerInfo *root,
 						   innerkeys,
 						   jointype,
 						   extra);
+
+		/*
+		 * If we have partial outer and parallel safe inner path then try
+		 * partial mergejoin path.
+		 */
+		if (cheapest_partial_outer && cheapest_safe_inner)
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   cheapest_partial_outer,
+									   cheapest_safe_inner,
+									   merge_pathkeys,
+									   cur_mergeclauses,
+									   outerkeys,
+									   innerkeys,
+									   jointype,
+									   extra);
 	}
 }
 
@@ -1023,6 +1156,199 @@ generate_mergejoin_paths(PlannerInfo *root,
 }
 
 /*
+ * generate_partial_mergejoin_paths
+ *
+ *	As above, but it will generate partial paths.  And, also in this function
+ *	we don't handle JOIN_UNIQUE_OUTER, JOIN_FULL or JOIN_RIGHT join types
+ *	because we don't support these join types for partial paths.
+ */
+static void
+generate_partial_mergejoin_paths(PlannerInfo *root,
+								 RelOptInfo *joinrel,
+								 RelOptInfo *innerrel,
+								 Path *outerpath,
+								 JoinType jointype,
+								 JoinPathExtraData *extra,
+								 Path *inner_cheapest_total,
+								 List *merge_pathkeys)
+{
+	List	   *mergeclauses;
+	List	   *innersortkeys;
+	List	   *trialsortkeys;
+	Path	   *cheapest_startup_inner;
+	Path	   *cheapest_total_inner;
+	JoinType	save_jointype = jointype;
+	int			num_sortkeys;
+	int			sortkeycnt;
+
+	/*
+	 * We should never come here for JOIN_UNIQUE_OUTER, JOIN_FULL or
+	 * JOIN_RIGHT.
+	 */
+	Assert(jointype != JOIN_UNIQUE_OUTER);
+	Assert(jointype != JOIN_FULL);
+	Assert(jointype != JOIN_RIGHT);
+
+	if (jointype == JOIN_UNIQUE_INNER)
+		jointype = JOIN_INNER;
+
+	/* Look for useful mergeclauses (if any) */
+	mergeclauses = find_mergeclauses_for_pathkeys(root,
+												  outerpath->pathkeys,
+												  true,
+												  extra->mergeclause_list);
+
+	/*
+	 * Done with this outer path if no chance for a mergejoin.
+	 */
+	if (mergeclauses == NIL)
+		return;
+
+	/* Compute the required ordering of the inner path */
+	innersortkeys = make_inner_pathkeys_for_merge(root,
+												  mergeclauses,
+												  outerpath->pathkeys);
+
+	/* Generate partial path if inner is parallel safe. */
+	if (inner_cheapest_total->parallel_safe)
+		try_partial_mergejoin_path(root,
+								   joinrel,
+								   outerpath,
+								   inner_cheapest_total,
+								   merge_pathkeys,
+								   mergeclauses,
+								   NIL,
+								   innersortkeys,
+								   jointype,
+								   extra);
+
+	/* Can't do anything else if inner path needs to be unique'd */
+	if (save_jointype == JOIN_UNIQUE_INNER)
+		return;
+
+	/*
+	 * See comments in generate_mergejoin_paths
+	 */
+	if (pathkeys_contained_in(innersortkeys,
+							  inner_cheapest_total->pathkeys))
+	{
+		/* inner_cheapest_total didn't require a sort */
+		cheapest_startup_inner = inner_cheapest_total;
+		cheapest_total_inner = inner_cheapest_total;
+	}
+	else
+	{
+		/* it did require a sort, at least for the full set of keys */
+		cheapest_startup_inner = NULL;
+		cheapest_total_inner = NULL;
+	}
+	num_sortkeys = list_length(innersortkeys);
+	if (num_sortkeys > 1)
+		trialsortkeys = list_copy(innersortkeys);		/* need modifiable copy */
+	else
+		trialsortkeys = innersortkeys;	/* won't really truncate */
+
+	for (sortkeycnt = num_sortkeys; sortkeycnt > 0; sortkeycnt--)
+	{
+		Path	   *innerpath;
+		List	   *newclauses = NIL;
+
+		/*
+		 * Look for an inner path ordered well enough for the first
+		 * 'sortkeycnt' innersortkeys.  NB: trialsortkeys list is modified
+		 * destructively, which is why we made a copy...
+		 */
+		trialsortkeys = list_truncate(trialsortkeys, sortkeycnt);
+
+		/* Consider only parallel safe inner path */
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   TOTAL_COST,
+												   true);
+		if (innerpath != NULL &&
+			(cheapest_total_inner == NULL ||
+			 cheapest_total_inner->parallel_safe == false ||
+			 compare_path_costs(innerpath, cheapest_total_inner,
+								TOTAL_COST) < 0))
+
+		{
+			/* Found a cheap (or even-cheaper) sorted parallel safe path */
+			/* Select the right mergeclauses, if we didn't already */
+			if (sortkeycnt < num_sortkeys)
+			{
+				newclauses = find_mergeclauses_for_pathkeys(root,
+															trialsortkeys,
+															false,
+															mergeclauses);
+				Assert(newclauses != NIL);
+			}
+			else
+				newclauses = mergeclauses;
+
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   outerpath,
+									   innerpath,
+									   merge_pathkeys,
+									   newclauses,
+									   NIL,
+									   NIL,
+									   jointype,
+									   extra);
+
+			cheapest_total_inner = innerpath;
+		}
+		/* Same on the basis of cheapest startup cost ... */
+		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
+												   trialsortkeys,
+												   NULL,
+												   STARTUP_COST,
+												   true);
+		if (innerpath != NULL &&
+			(cheapest_startup_inner == NULL ||
+			 cheapest_startup_inner->parallel_safe == false ||
+			 compare_path_costs(innerpath, cheapest_startup_inner,
+								STARTUP_COST) < 0))
+		{
+			/* Found a cheap (or even-cheaper) sorted parallel safe path */
+			if (innerpath != cheapest_total_inner)
+			{
+				/*
+				 * Avoid rebuilding clause list if we already made one; saves
+				 * memory in big join trees...
+				 */
+				if (newclauses == NIL)
+				{
+					if (sortkeycnt < num_sortkeys)
+					{
+						newclauses =
+							find_mergeclauses_for_pathkeys(root,
+														   trialsortkeys,
+														   false,
+														   mergeclauses);
+						Assert(newclauses != NIL);
+					}
+					else
+						newclauses = mergeclauses;
+				}
+				try_partial_mergejoin_path(root,
+										   joinrel,
+										   outerpath,
+										   innerpath,
+										   merge_pathkeys,
+										   newclauses,
+										   NIL,
+										   NIL,
+										   jointype,
+										   extra);
+			}
+			cheapest_startup_inner = innerpath;
+		}
+	}
+}
+
+/*
  * match_unsorted_outer
  *	  Creates possible join paths for processing a single join relation
  *	  'joinrel' by employing either iterative substitution or
@@ -1225,18 +1551,76 @@ match_unsorted_outer(PlannerInfo *root,
 	}
 
 	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
+	 * Consider partial nestloop and mergejoin plan if outerrel has any
+	 * partial path and the joinrel is parallel-safe.  However, we can't
+	 * handle JOIN_UNIQUE_OUTER, because the outer path will be partial, and
+	 * therefore we won't be able to properly guarantee uniqueness.  Nor can
+	 * we handle extra_lateral_rels, since partial paths must not be
+	 * parameterized. Similarly, we can't handle JOIN_FULL and JOIN_RIGHT,
+	 * because they can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
+	if (joinrel->consider_parallel &&
 		save_jointype != JOIN_UNIQUE_OUTER &&
+		save_jointype != JOIN_FULL &&
+		save_jointype != JOIN_RIGHT &&
+		outerrel->partial_pathlist != NIL &&
 		bms_is_empty(joinrel->lateral_relids))
-		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
-								   save_jointype, extra);
+	{
+		if (nestjoinOK)
+			consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
+									   save_jointype, extra);
+
+		/*
+		 * Can't generate mergejoin path if inner rel is parameterized by
+		 * outer.
+		 */
+		if (inner_cheapest_total != NULL)
+			consider_parallel_mergejoin(root, joinrel, outerrel, innerrel,
+										save_jointype, extra,
+										inner_cheapest_total);
+	}
+}
+
+/*
+ * consider_parallel_mergejoin
+ *	  Try to build partial paths for a joinrel by joining a partial path
+ *	  for the outer relation to a complete path for the inner relation.
+ *
+ * 'joinrel' is the join relation
+ * 'outerrel' is the outer join relation
+ * 'innerrel' is the inner join relation
+ * 'jointype' is the type of join to do
+ * 'extra' contains additional input values
+ * 'inner_cheapest_total' cheapest total path for innerrel
+ */
+static void
+consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total)
+{
+	ListCell   *lc1;
+
+	/* generate merge join path for each partial outer path */
+	foreach(lc1, outerrel->partial_pathlist)
+	{
+		Path	   *outerpath = (Path *) lfirst(lc1);
+		List	   *merge_pathkeys;
+
+		/*
+		 * Figure out what useful ordering any paths we create will have.
+		 */
+		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											 outerpath->pathkeys);
+
+		generate_partial_mergejoin_paths(root, joinrel, innerrel,
+										 outerpath, jointype, extra,
+										 inner_cheapest_total,
+										 merge_pathkeys);
+	}
 }
 
 /*
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index a5a2232..75558d0 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -169,6 +169,31 @@ select  count(*) from tenk1 where thousand > 95;
 
 reset enable_seqscan;
 reset enable_bitmapscan;
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ Finalize Aggregate
+   ->  Gather
+         Workers Planned: 4
+         ->  Partial Aggregate
+               ->  Merge Join
+                     Merge Cond: (tenk1.unique1 = tenk2.unique1)
+                     ->  Parallel Index Only Scan using tenk1_unique1 on tenk1
+                     ->  Index Only Scan using tenk2_unique1 on tenk2
+(8 rows)
+
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+ count 
+-------
+ 10000
+(1 row)
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 explain (costs off)
   select stringu1::int2 from tenk1 where unique1 = 1;
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index d72addf..ebdae7e 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -64,6 +64,16 @@ select  count(*) from tenk1 where thousand > 95;
 reset enable_seqscan;
 reset enable_bitmapscan;
 
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 
 explain (costs off)
path-search-for-mergejoin-v2.patchapplication/octet-stream; name=path-search-for-mergejoin-v2.patchDownload
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 633b5c1..87a3faf 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -1447,12 +1447,14 @@ generate_mergeappend_paths(PlannerInfo *root, RelOptInfo *rel,
 				get_cheapest_path_for_pathkeys(childrel->pathlist,
 											   pathkeys,
 											   NULL,
-											   STARTUP_COST);
+											   STARTUP_COST,
+											   false);
 			cheapest_total =
 				get_cheapest_path_for_pathkeys(childrel->pathlist,
 											   pathkeys,
 											   NULL,
-											   TOTAL_COST);
+											   TOTAL_COST,
+											   false);
 
 			/*
 			 * If we can't find any paths with the right order just use the
@@ -1517,7 +1519,8 @@ get_cheapest_parameterized_child_path(PlannerInfo *root, RelOptInfo *rel,
 	cheapest = get_cheapest_path_for_pathkeys(rel->pathlist,
 											  NIL,
 											  required_outer,
-											  TOTAL_COST);
+											  TOTAL_COST,
+											  false);
 	Assert(cheapest != NULL);
 	if (bms_equal(PATH_REQ_OUTER(cheapest), required_outer))
 		return cheapest;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 2897245..3f6f233 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -936,7 +936,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
 												   trialsortkeys,
 												   NULL,
-												   TOTAL_COST);
+												   TOTAL_COST,
+												   false);
 		if (innerpath != NULL &&
 			(cheapest_total_inner == NULL ||
 			 compare_path_costs(innerpath, cheapest_total_inner,
@@ -971,7 +972,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 		innerpath = get_cheapest_path_for_pathkeys(innerrel->pathlist,
 												   trialsortkeys,
 												   NULL,
-												   STARTUP_COST);
+												   STARTUP_COST,
+												   false);
 		if (innerpath != NULL &&
 			(cheapest_startup_inner == NULL ||
 			 compare_path_costs(innerpath, cheapest_startup_inner,
@@ -1518,19 +1520,11 @@ hash_inner_and_outer(PlannerInfo *root,
 				cheapest_safe_inner = cheapest_total_inner;
 			else if (save_jointype != JOIN_UNIQUE_INNER)
 			{
-				ListCell   *lc;
+				List	   *paths;
 
-				foreach(lc, innerrel->cheapest_parameterized_paths)
-				{
-					Path	   *innerpath = (Path *) lfirst(lc);
-
-					if (innerpath->parallel_safe &&
-						bms_is_empty(PATH_REQ_OUTER(innerpath)))
-					{
-						cheapest_safe_inner = innerpath;
-						break;
-					}
-				}
+				paths = innerrel->cheapest_parameterized_paths;
+				cheapest_safe_inner =
+					get_cheapest_parallel_safe_total_inner(paths);
 			}
 
 			if (cheapest_safe_inner != NULL)
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index 1065b31..fe234b1 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -337,11 +337,13 @@ pathkeys_contained_in(List *keys1, List *keys2)
  * 'pathkeys' represents a required ordering (in canonical form!)
  * 'required_outer' denotes allowable outer relations for parameterized paths
  * 'cost_criterion' is STARTUP_COST or TOTAL_COST
+ * 'require_parallel_safe' consider only parallel safe paths
  */
 Path *
 get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
 							   Relids required_outer,
-							   CostSelector cost_criterion)
+							   CostSelector cost_criterion,
+							   bool require_parallel_safe)
 {
 	Path	   *matched_path = NULL;
 	ListCell   *l;
@@ -358,6 +360,9 @@ get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
 			compare_path_costs(matched_path, path, cost_criterion) <= 0)
 			continue;
 
+		if (require_parallel_safe && !path->parallel_safe)
+			continue;
+
 		if (pathkeys_contained_in(pathkeys, path->pathkeys) &&
 			bms_is_subset(PATH_REQ_OUTER(path), required_outer))
 			matched_path = path;
@@ -407,6 +412,28 @@ get_cheapest_fractional_path_for_pathkeys(List *paths,
 	return matched_path;
 }
 
+
+/*
+ * get_cheapest_parallel_safe_total_inner
+ *	  Find the unparameterized parallel-safe path with the least total cost.
+ */
+Path *
+get_cheapest_parallel_safe_total_inner(List *paths)
+{
+	ListCell   *l;
+
+	foreach(l, paths)
+	{
+		Path   *innerpath = (Path *) lfirst(l);
+
+		if (innerpath->parallel_safe &&
+			bms_is_empty(PATH_REQ_OUTER(innerpath)))
+			return innerpath;
+	}
+
+	return NULL;
+}
+
 /****************************************************************************
  *		NEW PATHKEY FORMATION
  ****************************************************************************/
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index ebda308..bc0dcf4 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -182,11 +182,13 @@ extern PathKeysComparison compare_pathkeys(List *keys1, List *keys2);
 extern bool pathkeys_contained_in(List *keys1, List *keys2);
 extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
 							   Relids required_outer,
-							   CostSelector cost_criterion);
+							   CostSelector cost_criterion,
+							   bool require_parallel_safe);
 extern Path *get_cheapest_fractional_path_for_pathkeys(List *paths,
 										  List *pathkeys,
 										  Relids required_outer,
 										  double fraction);
+extern Path *get_cheapest_parallel_safe_total_inner(List *paths);
 extern List *build_index_pathkeys(PlannerInfo *root, IndexOptInfo *index,
 					 ScanDirection scandir);
 extern List *build_expression_pathkey(PlannerInfo *root, Expr *expr,
#34Robert Haas
robertmhaas@gmail.com
In reply to: Dilip Kumar (#33)
Re: Proposal : Parallel Merge Join

On Mon, Mar 6, 2017 at 8:15 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Fri, Mar 3, 2017 at 3:57 PM, Robert Haas <robertmhaas@gmail.com> wrote:

I'm not happy with the way this patch can just happen to latch on to a
path that's not parallel-safe rather than one that is and then just
give up on a merge join in that case. I already made this argument in
/messages/by-id/CA+TgmobdW2au1Jq5L4ySA2ZhqFmA-qNvD7ZFaZbJWm3c0ysWyw@mail.gmail.com
and my opinion hasn't changed. I've subsequently come to realize that
this problem is more widespread that I initially realized: not only do
sort_inner_and_outer() and generate_partial_mergejoin_paths() give up
without searching for the cheapest parallel-safe path, but the latter
later calls get_cheapest_path_for_pathkeys() and then just pretends it
didn't find anything unless the result is parallel-safe. This makes
no sense to me: the cheapest parallel-safe path could be 1% more
expensive than the cheapest path of any kind, but because it's not the
cheapest we arbitrarily skip it, even though parallelizing more of the
tree might leave us way ahead overall.

for sort_inner_and_outer I have followed the same logic what
hash_inner_and_outer is doing. Also moved the logic of selecting the
cheapest_safe_inner out of the pathkey loop.

+    /* Generate partial path if inner is parallel safe. */
+    if (inner_cheapest_total->parallel_safe)
+        try_partial_mergejoin_path(root,
+                                   joinrel,
+                                   outerpath,
+                                   inner_cheapest_total,
+                                   merge_pathkeys,
+                                   mergeclauses,
+                                   NIL,
+                                   innersortkeys,
+                                   jointype,
+                                   extra);
+
+    /* Can't do anything else if inner path needs to be unique'd */
+    if (save_jointype == JOIN_UNIQUE_INNER)
+        return;

Right after this, you should try_partial_mergejoin_path() with the
result of get_cheapest_parallel_safe_total_inner(), just like
sort_inner_and_outer() already does.

I think with some work you could merge generate_mergejoin_paths with
generate_partial_mergejoin_paths instead of duplicating all the code.
They're not really that different, and you could reduce the
differences further if you made try_mergejoin_path() take an
additional is_partial argument and pass the call to
try_partial_mergejoin_path() if it's true. The various bits of
special handling related to join types that aren't supported in
parallel queries aren't going to help you in parallel mode, but they
won't hurt either. This bit:

+    /* Generate partial path if inner is parallel safe. */
+    if (inner_cheapest_total->parallel_safe)
+        try_partial_mergejoin_path(root,

...is really not right. That value is being passed down from
consider_parallel_mergejoin(), which is getting it from
match_unsorted_outer(), which really shouldn't be calling this code in
the first place with a path that's not parallel-safe. What it ought
to do is (a) pass inner_cheapest_total if that's non-NULL and
parallel-safe, or else (b) call
get_cheapest_parallel_safe_total_inner() and pass that value, (c)
unless that's NULL also in which case it should skip the call
altogether.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#35Dilip Kumar
dilipbalaut@gmail.com
In reply to: Robert Haas (#34)
1 attachment(s)
Re: Proposal : Parallel Merge Join

On Tue, Mar 7, 2017 at 5:21 AM, Robert Haas <robertmhaas@gmail.com> wrote:

+    /* Can't do anything else if inner path needs to be unique'd */
+    if (save_jointype == JOIN_UNIQUE_INNER)
+        return;

Right after this, you should try_partial_mergejoin_path() with the
result of get_cheapest_parallel_safe_total_inner(), just like
sort_inner_and_outer() already does.

I think with some work you could merge generate_mergejoin_paths with
generate_partial_mergejoin_paths instead of duplicating all the code.
They're not really that different, and you could reduce the
differences further if you made try_mergejoin_path() take an
additional is_partial argument and pass the call to
try_partial_mergejoin_path() if it's true. The various bits of
special handling related to join types that aren't supported in
parallel queries aren't going to help you in parallel mode, but they
won't hurt either. This bit:

+    /* Generate partial path if inner is parallel safe. */
+    if (inner_cheapest_total->parallel_safe)
+        try_partial_mergejoin_path(root,

...is really not right. That value is being passed down from
consider_parallel_mergejoin(), which is getting it from
match_unsorted_outer(), which really shouldn't be calling this code in
the first place with a path that's not parallel-safe. What it ought
to do is (a) pass inner_cheapest_total if that's non-NULL and
parallel-safe, or else (b) call
get_cheapest_parallel_safe_total_inner() and pass that value, (c)
unless that's NULL also in which case it should skip the call
altogether.

I have tried to address above comments in the latest patch, But there
is one part of the patch which I am not quite sure yet. So still this
patch is not a final one.

+ *
+ * If inner_cheapest_total is not NULL or not a parallel safe path then
+ * find the cheapest total parallel safe path.
+ */
+ if (inner_cheapest_total == NULL ||
+ inner_cheapest_total->parallel_safe == false)
+ {
+ inner_cheapest_total =
+ get_cheapest_parallel_safe_total_inner(innerrel->pathlist);
+ }
+
+ if (inner_cheapest_total)
+ consider_parallel_mergejoin(root, joinrel, outerrel, innerrel,
+ save_jointype, extra,
+ inner_cheapest_total);

I am confused about whether to call
"get_cheapest_parallel_safe_total_inner" with
innerrel->cheapest_parameterized_paths like we do in case of
hash_inner_and_outer or with
innerrel->pathlist. The reason behind I am calling this with complete
pathlist (innerrel->pathlist) is that inside generate_mergejoin_paths
it calls "get_cheapest_path_for_pathkeys" for complete pathlist and if
we decide not to call consider_parallel_mergejoin if the cheapest
parallel safe path in innerrel->cheapest_parameterized_paths is NULL
then it will not be fair (we might have some parallel safe paths in
innerrel->pathlist).

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

parallel_mergejoin_v10.patchapplication/octet-stream; name=parallel_mergejoin_v10.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 3f6f233..a21a74a 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -28,6 +28,16 @@ set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
 #define PATH_PARAM_BY_REL(path, rel)  \
 	((path)->param_info && bms_overlap(PATH_REQ_OUTER(path), (rel)->relids))
 
+static void try_partial_mergejoin_path(PlannerInfo *root,
+									   RelOptInfo *joinrel,
+									   Path *outer_path,
+									   Path *inner_path,
+									   List *pathkeys,
+									   List *mergeclauses,
+									   List *outersortkeys,
+									   List *innersortkeys,
+									   JoinType jointype,
+									   JoinPathExtraData *extra);
 static void sort_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -40,6 +50,13 @@ static void consider_parallel_nestloop(PlannerInfo *root,
 						   RelOptInfo *innerrel,
 						   JoinType jointype,
 						   JoinPathExtraData *extra);
+static void consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -58,7 +75,8 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 JoinPathExtraData *extra,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
-						 List *merge_pathkeys);
+						 List *merge_pathkeys,
+						 bool is_partial);
 
 
 /*
@@ -416,11 +434,27 @@ try_mergejoin_path(PlannerInfo *root,
 				   List *outersortkeys,
 				   List *innersortkeys,
 				   JoinType jointype,
-				   JoinPathExtraData *extra)
+				   JoinPathExtraData *extra,
+				   bool	is_partial)
 {
 	Relids		required_outer;
 	JoinCostWorkspace workspace;
 
+	if (is_partial)
+	{
+		try_partial_mergejoin_path(root,
+								   joinrel,
+								   outer_path,
+								   inner_path,
+								   pathkeys,
+								   mergeclauses,
+								   outersortkeys,
+								   innersortkeys,
+								   jointype,
+								   extra);
+		return;
+	}
+
 	/*
 	 * Check to see if proposed path is still parameterized, and reject if the
 	 * parameterization wouldn't be sensible.
@@ -481,6 +515,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a partial merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_partial_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+						   RelOptInfo *joinrel,
+						   Path *outer_path,
+						   Path *inner_path,
+						   List *pathkeys,
+						   List *mergeclauses,
+						   List *outersortkeys,
+						   List *innersortkeys,
+						   JoinType jointype,
+						   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_hashjoin_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_empty(inner_paramrels))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+					 create_mergejoin_path(root,
+										   joinrel,
+										   jointype,
+										   &workspace,
+										   extra->sjinfo,
+										   outer_path,
+										   inner_path,
+										   extra->restrictlist,
+										   pathkeys,
+										   NULL,
+										   mergeclauses,
+										   outersortkeys,
+										   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -649,8 +753,11 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
+	Path	   *cheapest_partial_outer;
+	Path	   *cheapest_safe_inner;
 	List	   *all_pathkeys;
 	ListCell   *l;
 
@@ -700,6 +807,35 @@ sort_inner_and_outer(PlannerInfo *root,
 	}
 
 	/*
+	 * If the joinrel is parallel-safe, we may be able to consider a partial
+	 * merge join.  However, we can't handle JOIN_UNIQUE_OUTER, because the
+	 * outer path will be partial, and therefore we won't be able to properly
+	 * guarantee uniqueness.  Similarly, we can't handle JOIN_FULL and
+	 * JOIN_RIGHT, because they can produce false null extended rows.  Also,
+	 * the resulting path must not be parameterized.
+	 */
+	if (joinrel->consider_parallel &&
+		save_jointype != JOIN_UNIQUE_OUTER &&
+		save_jointype != JOIN_FULL &&
+		save_jointype != JOIN_RIGHT &&
+		outerrel->partial_pathlist != NIL &&
+		bms_is_empty(joinrel->lateral_relids))
+	{
+		cheapest_partial_outer = (Path *) linitial(outerrel->partial_pathlist);
+
+		if (inner_path->parallel_safe)
+			cheapest_safe_inner = inner_path;
+		else if (save_jointype != JOIN_UNIQUE_INNER)
+		{
+			List	   *paths;
+
+			paths = innerrel->cheapest_parameterized_paths;
+			cheapest_safe_inner =
+				get_cheapest_parallel_safe_total_inner(paths);
+		}
+	}
+
+	/*
 	 * Each possible ordering of the available mergejoin clauses will generate
 	 * a differently-sorted result path at essentially the same cost.  We have
 	 * no basis for choosing one over another at this level of joining, but
@@ -781,7 +917,24 @@ sort_inner_and_outer(PlannerInfo *root,
 						   outerkeys,
 						   innerkeys,
 						   jointype,
-						   extra);
+						   extra,
+						   false);
+
+		/*
+		 * If we have partial outer and parallel safe inner path then try
+		 * partial mergejoin path.
+		 */
+		if (cheapest_partial_outer && cheapest_safe_inner)
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   cheapest_partial_outer,
+									   cheapest_safe_inner,
+									   merge_pathkeys,
+									   cur_mergeclauses,
+									   outerkeys,
+									   innerkeys,
+									   jointype,
+									   extra);
 	}
 }
 
@@ -808,7 +961,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 						 JoinPathExtraData *extra,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
-						 List *merge_pathkeys)
+						 List *merge_pathkeys,
+						 bool is_partial)
 {
 	List	   *mergeclauses;
 	List	   *innersortkeys;
@@ -868,7 +1022,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 					   NIL,
 					   innersortkeys,
 					   jointype,
-					   extra);
+					   extra,
+					   is_partial);
 
 	/* Can't do anything else if inner path needs to be unique'd */
 	if (save_jointype == JOIN_UNIQUE_INNER)
@@ -937,7 +1092,7 @@ generate_mergejoin_paths(PlannerInfo *root,
 												   trialsortkeys,
 												   NULL,
 												   TOTAL_COST,
-												   false);
+												   is_partial);
 		if (innerpath != NULL &&
 			(cheapest_total_inner == NULL ||
 			 compare_path_costs(innerpath, cheapest_total_inner,
@@ -965,7 +1120,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 							   NIL,
 							   NIL,
 							   jointype,
-							   extra);
+							   extra,
+							   is_partial);
 			cheapest_total_inner = innerpath;
 		}
 		/* Same on the basis of cheapest startup cost ... */
@@ -973,7 +1129,7 @@ generate_mergejoin_paths(PlannerInfo *root,
 												   trialsortkeys,
 												   NULL,
 												   STARTUP_COST,
-												   false);
+												   is_partial);
 		if (innerpath != NULL &&
 			(cheapest_startup_inner == NULL ||
 			 compare_path_costs(innerpath, cheapest_startup_inner,
@@ -1009,7 +1165,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 								   NIL,
 								   NIL,
 								   jointype,
-								   extra);
+								   extra,
+								   is_partial);
 			}
 			cheapest_startup_inner = innerpath;
 		}
@@ -1221,22 +1378,88 @@ match_unsorted_outer(PlannerInfo *root,
 		/* Generate merge join paths */
 		generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
 								 save_jointype, extra, useallclauses,
-								 inner_cheapest_total, merge_pathkeys);
+								 inner_cheapest_total, merge_pathkeys,
+								 false);
 	}
 
 	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
+	 * Consider partial nestloop and mergejoin plan if outerrel has any
+	 * partial path and the joinrel is parallel-safe.  However, we can't
+	 * handle JOIN_UNIQUE_OUTER, because the outer path will be partial, and
+	 * therefore we won't be able to properly guarantee uniqueness.  Nor can
+	 * we handle extra_lateral_rels, since partial paths must not be
+	 * parameterized. Similarly, we can't handle JOIN_FULL and JOIN_RIGHT,
+	 * because they can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
+	if (joinrel->consider_parallel &&
 		save_jointype != JOIN_UNIQUE_OUTER &&
+		save_jointype != JOIN_FULL &&
+		save_jointype != JOIN_RIGHT &&
+		outerrel->partial_pathlist != NIL &&
 		bms_is_empty(joinrel->lateral_relids))
-		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
-								   save_jointype, extra);
+	{
+		if (nestjoinOK)
+			consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
+									   save_jointype, extra);
+
+		/*
+		 *
+		 * If inner_cheapest_total is not NULL or not a parallel safe path then
+		 * find the cheapest total parallel safe path.
+		 */
+		if (inner_cheapest_total == NULL ||
+			inner_cheapest_total->parallel_safe == false)
+		{
+			inner_cheapest_total =
+				get_cheapest_parallel_safe_total_inner(innerrel->pathlist);
+		}
+
+		if (inner_cheapest_total)
+			consider_parallel_mergejoin(root, joinrel, outerrel, innerrel,
+										save_jointype, extra,
+										inner_cheapest_total);
+	}
+}
+
+/*
+ * consider_parallel_mergejoin
+ *	  Try to build partial paths for a joinrel by joining a partial path
+ *	  for the outer relation to a complete path for the inner relation.
+ *
+ * 'joinrel' is the join relation
+ * 'outerrel' is the outer join relation
+ * 'innerrel' is the inner join relation
+ * 'jointype' is the type of join to do
+ * 'extra' contains additional input values
+ * 'inner_cheapest_total' cheapest total path for innerrel
+ */
+static void
+consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total)
+{
+	ListCell   *lc1;
+
+	/* generate merge join path for each partial outer path */
+	foreach(lc1, outerrel->partial_pathlist)
+	{
+		Path	   *outerpath = (Path *) lfirst(lc1);
+		List	   *merge_pathkeys;
+
+		/*
+		 * Figure out what useful ordering any paths we create will have.
+		 */
+		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											 outerpath->pathkeys);
+
+		generate_mergejoin_paths(root, joinrel, innerrel, outerpath, jointype,
+								 extra, false,  inner_cheapest_total,
+								 merge_pathkeys, true);
+	}
 }
 
 /*
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index a5a2232..75558d0 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -169,6 +169,31 @@ select  count(*) from tenk1 where thousand > 95;
 
 reset enable_seqscan;
 reset enable_bitmapscan;
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ Finalize Aggregate
+   ->  Gather
+         Workers Planned: 4
+         ->  Partial Aggregate
+               ->  Merge Join
+                     Merge Cond: (tenk1.unique1 = tenk2.unique1)
+                     ->  Parallel Index Only Scan using tenk1_unique1 on tenk1
+                     ->  Index Only Scan using tenk2_unique1 on tenk2
+(8 rows)
+
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+ count 
+-------
+ 10000
+(1 row)
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 explain (costs off)
   select stringu1::int2 from tenk1 where unique1 = 1;
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index d72addf..ebdae7e 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -64,6 +64,16 @@ select  count(*) from tenk1 where thousand > 95;
 reset enable_seqscan;
 reset enable_bitmapscan;
 
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 
 explain (costs off)
#36Robert Haas
robertmhaas@gmail.com
In reply to: Dilip Kumar (#35)
Re: Proposal : Parallel Merge Join

On Tue, Mar 7, 2017 at 5:16 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

I am confused about whether to call
"get_cheapest_parallel_safe_total_inner" with
innerrel->cheapest_parameterized_paths like we do in case of
hash_inner_and_outer or with
innerrel->pathlist. The reason behind I am calling this with complete
pathlist (innerrel->pathlist) is that inside generate_mergejoin_paths
it calls "get_cheapest_path_for_pathkeys" for complete pathlist and if
we decide not to call consider_parallel_mergejoin if the cheapest
parallel safe path in innerrel->cheapest_parameterized_paths is NULL
then it will not be fair (we might have some parallel safe paths in
innerrel->pathlist).

You're right to be confused, because that seems to be a bug in the
existing code. There seems to be no guarantee that the cheapest
parallel-safe path will be in the cheapest_parameterized_paths list.
I'll go fix that.

As a matter of style, when testing a value of type "bool", write if
(x) or if (!x). When testing a variable of some other type, say int,
write if (x == 0) or if (x != 0) or whatever.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#37Dilip Kumar
dilipbalaut@gmail.com
In reply to: Robert Haas (#36)
1 attachment(s)
Re: Proposal : Parallel Merge Join

On Tue, Mar 7, 2017 at 7:47 PM, Robert Haas <robertmhaas@gmail.com> wrote:

You're right to be confused, because that seems to be a bug in the
existing code. There seems to be no guarantee that the cheapest
parallel-safe path will be in the cheapest_parameterized_paths list.
I'll go fix that.

Okay, Done the simmilar changes in sort_inner_and_outer as well.

As a matter of style, when testing a value of type "bool", write if
(x) or if (!x). When testing a variable of some other type, say int,
write if (x == 0) or if (x != 0) or whatever.

Done

Apart from this, there was one problem in match_unsorted_outer (in
v10), Basically, if inner_cheapest_total was not parallel_safe then I
was always getting parallel safe inner. But, we should not do anything
if jointype was JOIN_UNIQUE_INNER, so fixed that also.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

Attachments:

parallel_mergejoin_v11.patchapplication/octet-stream; name=parallel_mergejoin_v11.patchDownload
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 8bb5473..11f02dd 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -28,6 +28,16 @@ set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
 #define PATH_PARAM_BY_REL(path, rel)  \
 	((path)->param_info && bms_overlap(PATH_REQ_OUTER(path), (rel)->relids))
 
+static void try_partial_mergejoin_path(PlannerInfo *root,
+						   RelOptInfo *joinrel,
+						   Path *outer_path,
+						   Path *inner_path,
+						   List *pathkeys,
+						   List *mergeclauses,
+						   List *outersortkeys,
+						   List *innersortkeys,
+						   JoinType jointype,
+						   JoinPathExtraData *extra);
 static void sort_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -40,6 +50,13 @@ static void consider_parallel_nestloop(PlannerInfo *root,
 						   RelOptInfo *innerrel,
 						   JoinType jointype,
 						   JoinPathExtraData *extra);
+static void consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total);
 static void hash_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
 					 RelOptInfo *outerrel, RelOptInfo *innerrel,
 					 JoinType jointype, JoinPathExtraData *extra);
@@ -58,7 +75,8 @@ static void generate_mergejoin_paths(PlannerInfo *root,
 						 JoinPathExtraData *extra,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
-						 List *merge_pathkeys);
+						 List *merge_pathkeys,
+						 bool is_partial);
 
 
 /*
@@ -416,11 +434,27 @@ try_mergejoin_path(PlannerInfo *root,
 				   List *outersortkeys,
 				   List *innersortkeys,
 				   JoinType jointype,
-				   JoinPathExtraData *extra)
+				   JoinPathExtraData *extra,
+				   bool is_partial)
 {
 	Relids		required_outer;
 	JoinCostWorkspace workspace;
 
+	if (is_partial)
+	{
+		try_partial_mergejoin_path(root,
+								   joinrel,
+								   outer_path,
+								   inner_path,
+								   pathkeys,
+								   mergeclauses,
+								   outersortkeys,
+								   innersortkeys,
+								   jointype,
+								   extra);
+		return;
+	}
+
 	/*
 	 * Check to see if proposed path is still parameterized, and reject if the
 	 * parameterization wouldn't be sensible.
@@ -481,6 +515,76 @@ try_mergejoin_path(PlannerInfo *root,
 }
 
 /*
+ * try_partial_mergejoin_path
+ *	  Consider a partial merge join path; if it appears useful, push it into
+ *	  the joinrel's pathlist via add_partial_path().
+ */
+static void
+try_partial_mergejoin_path(PlannerInfo *root,
+						   RelOptInfo *joinrel,
+						   Path *outer_path,
+						   Path *inner_path,
+						   List *pathkeys,
+						   List *mergeclauses,
+						   List *outersortkeys,
+						   List *innersortkeys,
+						   JoinType jointype,
+						   JoinPathExtraData *extra)
+{
+	JoinCostWorkspace workspace;
+
+	/*
+	 * See comments in try_partial_hashjoin_path().
+	 */
+	Assert(bms_is_empty(joinrel->lateral_relids));
+	if (inner_path->param_info != NULL)
+	{
+		Relids		inner_paramrels = inner_path->param_info->ppi_req_outer;
+
+		if (!bms_is_empty(inner_paramrels))
+			return;
+	}
+
+	/*
+	 * If the given paths are already well enough ordered, we can skip doing
+	 * an explicit sort.
+	 */
+	if (outersortkeys &&
+		pathkeys_contained_in(outersortkeys, outer_path->pathkeys))
+		outersortkeys = NIL;
+	if (innersortkeys &&
+		pathkeys_contained_in(innersortkeys, inner_path->pathkeys))
+		innersortkeys = NIL;
+
+	/*
+	 * See comments in try_partial_nestloop_path().
+	 */
+	initial_cost_mergejoin(root, &workspace, jointype, mergeclauses,
+						   outer_path, inner_path,
+						   outersortkeys, innersortkeys,
+						   extra->sjinfo);
+
+	if (!add_partial_path_precheck(joinrel, workspace.total_cost, pathkeys))
+		return;
+
+	/* Might be good enough to be worth trying, so let's try it. */
+	add_partial_path(joinrel, (Path *)
+					 create_mergejoin_path(root,
+										   joinrel,
+										   jointype,
+										   &workspace,
+										   extra->sjinfo,
+										   outer_path,
+										   inner_path,
+										   extra->restrictlist,
+										   pathkeys,
+										   NULL,
+										   mergeclauses,
+										   outersortkeys,
+										   innersortkeys));
+}
+
+/*
  * try_hashjoin_path
  *	  Consider a hash join path; if it appears useful, push it into
  *	  the joinrel's pathlist via add_path().
@@ -649,8 +753,11 @@ sort_inner_and_outer(PlannerInfo *root,
 					 JoinType jointype,
 					 JoinPathExtraData *extra)
 {
+	JoinType	save_jointype = jointype;
 	Path	   *outer_path;
 	Path	   *inner_path;
+	Path	   *cheapest_partial_outer;
+	Path	   *cheapest_safe_inner = NULL;
 	List	   *all_pathkeys;
 	ListCell   *l;
 
@@ -700,6 +807,30 @@ sort_inner_and_outer(PlannerInfo *root,
 	}
 
 	/*
+	 * If the joinrel is parallel-safe, we may be able to consider a partial
+	 * merge join.  However, we can't handle JOIN_UNIQUE_OUTER, because the
+	 * outer path will be partial, and therefore we won't be able to properly
+	 * guarantee uniqueness.  Similarly, we can't handle JOIN_FULL and
+	 * JOIN_RIGHT, because they can produce false null extended rows.  Also,
+	 * the resulting path must not be parameterized.
+	 */
+	if (joinrel->consider_parallel &&
+		save_jointype != JOIN_UNIQUE_OUTER &&
+		save_jointype != JOIN_FULL &&
+		save_jointype != JOIN_RIGHT &&
+		outerrel->partial_pathlist != NIL &&
+		bms_is_empty(joinrel->lateral_relids))
+	{
+		cheapest_partial_outer = (Path *) linitial(outerrel->partial_pathlist);
+
+		if (inner_path->parallel_safe)
+			cheapest_safe_inner = inner_path;
+		else if (save_jointype != JOIN_UNIQUE_INNER)
+			cheapest_safe_inner =
+				get_cheapest_parallel_safe_total_inner(innerrel->pathlist);
+	}
+
+	/*
 	 * Each possible ordering of the available mergejoin clauses will generate
 	 * a differently-sorted result path at essentially the same cost.  We have
 	 * no basis for choosing one over another at this level of joining, but
@@ -781,7 +912,24 @@ sort_inner_and_outer(PlannerInfo *root,
 						   outerkeys,
 						   innerkeys,
 						   jointype,
-						   extra);
+						   extra,
+						   false);
+
+		/*
+		 * If we have partial outer and parallel safe inner path then try
+		 * partial mergejoin path.
+		 */
+		if (cheapest_partial_outer && cheapest_safe_inner)
+			try_partial_mergejoin_path(root,
+									   joinrel,
+									   cheapest_partial_outer,
+									   cheapest_safe_inner,
+									   merge_pathkeys,
+									   cur_mergeclauses,
+									   outerkeys,
+									   innerkeys,
+									   jointype,
+									   extra);
 	}
 }
 
@@ -808,7 +956,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 						 JoinPathExtraData *extra,
 						 bool useallclauses,
 						 Path *inner_cheapest_total,
-						 List *merge_pathkeys)
+						 List *merge_pathkeys,
+						 bool is_partial)
 {
 	List	   *mergeclauses;
 	List	   *innersortkeys;
@@ -868,7 +1017,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 					   NIL,
 					   innersortkeys,
 					   jointype,
-					   extra);
+					   extra,
+					   is_partial);
 
 	/* Can't do anything else if inner path needs to be unique'd */
 	if (save_jointype == JOIN_UNIQUE_INNER)
@@ -937,7 +1087,7 @@ generate_mergejoin_paths(PlannerInfo *root,
 												   trialsortkeys,
 												   NULL,
 												   TOTAL_COST,
-												   false);
+												   is_partial);
 		if (innerpath != NULL &&
 			(cheapest_total_inner == NULL ||
 			 compare_path_costs(innerpath, cheapest_total_inner,
@@ -965,7 +1115,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 							   NIL,
 							   NIL,
 							   jointype,
-							   extra);
+							   extra,
+							   is_partial);
 			cheapest_total_inner = innerpath;
 		}
 		/* Same on the basis of cheapest startup cost ... */
@@ -973,7 +1124,7 @@ generate_mergejoin_paths(PlannerInfo *root,
 												   trialsortkeys,
 												   NULL,
 												   STARTUP_COST,
-												   false);
+												   is_partial);
 		if (innerpath != NULL &&
 			(cheapest_startup_inner == NULL ||
 			 compare_path_costs(innerpath, cheapest_startup_inner,
@@ -1009,7 +1160,8 @@ generate_mergejoin_paths(PlannerInfo *root,
 								   NIL,
 								   NIL,
 								   jointype,
-								   extra);
+								   extra,
+								   is_partial);
 			}
 			cheapest_startup_inner = innerpath;
 		}
@@ -1221,22 +1373,91 @@ match_unsorted_outer(PlannerInfo *root,
 		/* Generate merge join paths */
 		generate_mergejoin_paths(root, joinrel, innerrel, outerpath,
 								 save_jointype, extra, useallclauses,
-								 inner_cheapest_total, merge_pathkeys);
+								 inner_cheapest_total, merge_pathkeys,
+								 false);
 	}
 
 	/*
-	 * If the joinrel is parallel-safe and the join type supports nested
-	 * loops, we may be able to consider a partial nestloop plan.  However, we
-	 * can't handle JOIN_UNIQUE_OUTER, because the outer path will be partial,
-	 * and therefore we won't be able to properly guarantee uniqueness.  Nor
-	 * can we handle extra_lateral_rels, since partial paths must not be
-	 * parameterized.
+	 * Consider partial nestloop and mergejoin plan if outerrel has any
+	 * partial path and the joinrel is parallel-safe.  However, we can't
+	 * handle JOIN_UNIQUE_OUTER, because the outer path will be partial, and
+	 * therefore we won't be able to properly guarantee uniqueness.  Nor can
+	 * we handle extra_lateral_rels, since partial paths must not be
+	 * parameterized. Similarly, we can't handle JOIN_FULL and JOIN_RIGHT,
+	 * because they can produce false null extended rows.
 	 */
-	if (joinrel->consider_parallel && nestjoinOK &&
+	if (joinrel->consider_parallel &&
 		save_jointype != JOIN_UNIQUE_OUTER &&
+		save_jointype != JOIN_FULL &&
+		save_jointype != JOIN_RIGHT &&
+		outerrel->partial_pathlist != NIL &&
 		bms_is_empty(joinrel->lateral_relids))
-		consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
-								   save_jointype, extra);
+	{
+		if (nestjoinOK)
+			consider_parallel_nestloop(root, joinrel, outerrel, innerrel,
+									   save_jointype, extra);
+
+		/*
+		 * If inner_cheapest_total is NULL or non parallel-safe then find the
+		 * cheapest total parallel safe path.  If doing JOIN_UNIQUE_INNER, we
+		 * can't use any alternative inner path.
+		 */
+		if (inner_cheapest_total == NULL ||
+			!inner_cheapest_total->parallel_safe)
+		{
+			if (save_jointype == JOIN_UNIQUE_INNER)
+				return;
+
+			inner_cheapest_total = get_cheapest_parallel_safe_total_inner(
+														 innerrel->pathlist);
+		}
+
+		if (inner_cheapest_total)
+			consider_parallel_mergejoin(root, joinrel, outerrel, innerrel,
+										save_jointype, extra,
+										inner_cheapest_total);
+	}
+}
+
+/*
+ * consider_parallel_mergejoin
+ *	  Try to build partial paths for a joinrel by joining a partial path
+ *	  for the outer relation to a complete path for the inner relation.
+ *
+ * 'joinrel' is the join relation
+ * 'outerrel' is the outer join relation
+ * 'innerrel' is the inner join relation
+ * 'jointype' is the type of join to do
+ * 'extra' contains additional input values
+ * 'inner_cheapest_total' cheapest total path for innerrel
+ */
+static void
+consider_parallel_mergejoin(PlannerInfo *root,
+							RelOptInfo *joinrel,
+							RelOptInfo *outerrel,
+							RelOptInfo *innerrel,
+							JoinType jointype,
+							JoinPathExtraData *extra,
+							Path *inner_cheapest_total)
+{
+	ListCell   *lc1;
+
+	/* generate merge join path for each partial outer path */
+	foreach(lc1, outerrel->partial_pathlist)
+	{
+		Path	   *outerpath = (Path *) lfirst(lc1);
+		List	   *merge_pathkeys;
+
+		/*
+		 * Figure out what useful ordering any paths we create will have.
+		 */
+		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
+											 outerpath->pathkeys);
+
+		generate_mergejoin_paths(root, joinrel, innerrel, outerpath, jointype,
+								 extra, false, inner_cheapest_total,
+								 merge_pathkeys, true);
+	}
 }
 
 /*
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index a5a2232..75558d0 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -169,6 +169,31 @@ select  count(*) from tenk1 where thousand > 95;
 
 reset enable_seqscan;
 reset enable_bitmapscan;
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ Finalize Aggregate
+   ->  Gather
+         Workers Planned: 4
+         ->  Partial Aggregate
+               ->  Merge Join
+                     Merge Cond: (tenk1.unique1 = tenk2.unique1)
+                     ->  Parallel Index Only Scan using tenk1_unique1 on tenk1
+                     ->  Index Only Scan using tenk2_unique1 on tenk2
+(8 rows)
+
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+ count 
+-------
+ 10000
+(1 row)
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 explain (costs off)
   select stringu1::int2 from tenk1 where unique1 = 1;
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index d72addf..ebdae7e 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -64,6 +64,16 @@ select  count(*) from tenk1 where thousand > 95;
 reset enable_seqscan;
 reset enable_bitmapscan;
 
+-- test parallel merge join path.
+set enable_hashjoin to off;
+set enable_nestloop to off;
+
+explain (costs off)
+	select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+select  count(*) from tenk1, tenk2 where tenk1.unique1 = tenk2.unique1;
+
+reset enable_hashjoin;
+reset enable_nestloop;
 set force_parallel_mode=1;
 
 explain (costs off)
#38Robert Haas
robertmhaas@gmail.com
In reply to: Dilip Kumar (#37)
Re: Proposal : Parallel Merge Join

On Tue, Mar 7, 2017 at 11:38 AM, Dilip Kumar <dilipbalaut@gmail.com> wrote:

On Tue, Mar 7, 2017 at 7:47 PM, Robert Haas <robertmhaas@gmail.com> wrote:

You're right to be confused, because that seems to be a bug in the
existing code. There seems to be no guarantee that the cheapest
parallel-safe path will be in the cheapest_parameterized_paths list.
I'll go fix that.

Okay, Done the simmilar changes in sort_inner_and_outer as well.

As a matter of style, when testing a value of type "bool", write if
(x) or if (!x). When testing a variable of some other type, say int,
write if (x == 0) or if (x != 0) or whatever.

Done

Apart from this, there was one problem in match_unsorted_outer (in
v10), Basically, if inner_cheapest_total was not parallel_safe then I
was always getting parallel safe inner. But, we should not do anything
if jointype was JOIN_UNIQUE_INNER, so fixed that also.

This version looks fine to me, so committed.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#39Dilip Kumar
dilipbalaut@gmail.com
In reply to: Robert Haas (#38)
Re: Proposal : Parallel Merge Join

On Tue, Mar 7, 2017 at 10:28 PM, Robert Haas <robertmhaas@gmail.com> wrote:

Apart from this, there was one problem in match_unsorted_outer (in
v10), Basically, if inner_cheapest_total was not parallel_safe then I
was always getting parallel safe inner. But, we should not do anything
if jointype was JOIN_UNIQUE_INNER, so fixed that also.

This version looks fine to me, so committed.

Thank you.

--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers